1use crate::errors::{Result, TrustformersError};
43use crate::tensor::Tensor;
44use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum FP8Format {
49 E4M3,
53
54 E5M2,
58}
59
60impl FP8Format {
61 pub fn max_value(&self) -> f32 {
63 match self {
64 FP8Format::E4M3 => 448.0,
65 FP8Format::E5M2 => 57344.0,
66 }
67 }
68
69 pub fn min_positive_normal(&self) -> f32 {
71 match self {
72 FP8Format::E4M3 => 2.0f32.powi(-9), FP8Format::E5M2 => 2.0f32.powi(-16), }
75 }
76
77 pub fn mantissa_bits(&self) -> u8 {
79 match self {
80 FP8Format::E4M3 => 3,
81 FP8Format::E5M2 => 2,
82 }
83 }
84
85 pub fn exponent_bits(&self) -> u8 {
87 match self {
88 FP8Format::E4M3 => 4,
89 FP8Format::E5M2 => 5,
90 }
91 }
92
93 pub fn exponent_bias(&self) -> i32 {
95 match self {
96 FP8Format::E4M3 => 7, FP8Format::E5M2 => 15, }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104pub enum ScalingStrategy {
105 PerTensor,
107
108 PerChannel,
110
111 PerToken,
113
114 BlockWise { block_size: usize },
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct DelayedScalingConfig {
121 pub enabled: bool,
123
124 pub interval: usize,
126
127 pub margin: f32,
129
130 pub update_threshold: f32,
132
133 pub history_window: usize,
135}
136
137impl Default for DelayedScalingConfig {
138 fn default() -> Self {
139 Self {
140 enabled: true,
141 interval: 1000,
142 margin: 1.2,
143 update_threshold: 0.95,
144 history_window: 100,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct FP8Config {
152 pub format: FP8Format,
154
155 pub scaling: ScalingStrategy,
157
158 pub delayed_scaling: DelayedScalingConfig,
160
161 pub stochastic_rounding: bool,
163
164 pub clip_to_max: bool,
166
167 pub use_hardware_ops: bool,
169
170 pub calibration_samples: usize,
172}
173
174impl Default for FP8Config {
175 fn default() -> Self {
176 Self {
177 format: FP8Format::E4M3,
178 scaling: ScalingStrategy::PerTensor,
179 delayed_scaling: DelayedScalingConfig::default(),
180 stochastic_rounding: true,
181 clip_to_max: true,
182 use_hardware_ops: true,
183 calibration_samples: 100,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct FP8Tensor {
191 pub data: Vec<u8>,
193
194 pub shape: Vec<usize>,
196
197 pub format: FP8Format,
199
200 pub scales: ScaleFactors,
202
203 pub zero_points: Option<Vec<f32>>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum ScaleFactors {
210 PerTensor(f32),
212
213 PerChannel(Vec<f32>),
215
216 PerToken(Vec<f32>),
218
219 BlockWise { scales: Vec<f32>, block_size: usize },
221}
222
223#[derive(Debug, Clone)]
225struct QuantStats {
226 max_history: Vec<f32>,
228
229 iteration: usize,
231
232 current_scale: f32,
234
235 overflow_count: usize,
237
238 underflow_count: usize,
240}
241
242impl QuantStats {
243 fn new(initial_scale: f32, window_size: usize) -> Self {
244 Self {
245 max_history: Vec::with_capacity(window_size),
246 iteration: 0,
247 current_scale: initial_scale,
248 overflow_count: 0,
249 underflow_count: 0,
250 }
251 }
252
253 fn update(&mut self, max_val: f32, window_size: usize) {
254 self.max_history.push(max_val);
255 if self.max_history.len() > window_size {
256 self.max_history.remove(0);
257 }
258 self.iteration += 1;
259 }
260
261 fn get_optimal_scale(&self, margin: f32, max_value: f32) -> f32 {
262 if self.max_history.is_empty() {
263 return self.current_scale;
264 }
265
266 let mut sorted = self.max_history.clone();
268 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal));
269 let percentile_99 = sorted[(sorted.len() as f32 * 0.99) as usize];
270
271 max_value / (percentile_99 * margin)
272 }
273}
274
275#[derive(Debug, Clone)]
281pub struct Lcg64 {
282 state: u64,
283}
284
285impl Lcg64 {
286 const A: u64 = 6_364_136_223_846_793_005;
288 const C: u64 = 1_442_695_040_888_963_407;
290
291 pub fn new(seed: u64) -> Self {
293 Self { state: seed }
294 }
295
296 #[inline]
298 pub fn next_u64(&mut self) -> u64 {
299 self.state = self.state.wrapping_mul(Self::A).wrapping_add(Self::C);
300 self.state
301 }
302
303 #[inline]
305 pub fn next_f32(&mut self) -> f32 {
306 (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
308 }
309}
310
311pub struct FP8Quantizer {
313 config: FP8Config,
315
316 stats: Option<Vec<QuantStats>>,
318
319 rng: Lcg64,
321}
322
323impl FP8Quantizer {
324 pub fn new(config: FP8Config) -> Result<Self> {
326 Ok(Self {
327 config,
328 stats: None,
329 rng: Lcg64::new(42),
330 })
331 }
332
333 pub fn with_seed(config: FP8Config, seed: u64) -> Result<Self> {
335 Ok(Self {
336 config,
337 stats: None,
338 rng: Lcg64::new(seed),
339 })
340 }
341
342 fn init_stats(&mut self, num_groups: usize) {
344 if self.config.delayed_scaling.enabled && self.stats.is_none() {
345 let initial_scale = 1.0;
346 let window = self.config.delayed_scaling.history_window;
347 self.stats =
348 Some((0..num_groups).map(|_| QuantStats::new(initial_scale, window)).collect());
349 }
350 }
351
352 pub fn quantize(&mut self, tensor: &Tensor) -> Result<FP8Tensor> {
354 let data = tensor.to_vec_f32()?;
355 let shape = tensor.shape().to_vec();
356
357 match self.config.scaling {
358 ScalingStrategy::PerTensor => self.quantize_per_tensor(&data, &shape),
359 ScalingStrategy::PerChannel => self.quantize_per_channel(&data, &shape),
360 ScalingStrategy::PerToken => self.quantize_per_token(&data, &shape),
361 ScalingStrategy::BlockWise { block_size } => {
362 self.quantize_blockwise(&data, &shape, block_size)
363 },
364 }
365 }
366
367 fn quantize_per_tensor(&mut self, data: &[f32], shape: &[usize]) -> Result<FP8Tensor> {
369 self.init_stats(1);
370
371 let max_abs = data
373 .iter()
374 .map(|x| x.abs())
375 .max_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal))
376 .unwrap_or(1e-8);
377
378 let scale = if let Some(stats) = &mut self.stats {
380 let stat = &mut stats[0];
381 stat.update(max_abs, self.config.delayed_scaling.history_window);
382
383 if stat.iteration % self.config.delayed_scaling.interval == 0 {
384 stat.current_scale = stat.get_optimal_scale(
385 self.config.delayed_scaling.margin,
386 self.config.format.max_value(),
387 );
388 }
389 stat.current_scale
390 } else {
391 self.config.format.max_value() / (max_abs * 1.2)
392 };
393
394 let quantized = self.quantize_data(data, scale)?;
396
397 Ok(FP8Tensor {
398 data: quantized,
399 shape: shape.to_vec(),
400 format: self.config.format,
401 scales: ScaleFactors::PerTensor(scale),
402 zero_points: None,
403 })
404 }
405
406 fn quantize_per_channel(&mut self, data: &[f32], shape: &[usize]) -> Result<FP8Tensor> {
408 if shape.len() < 2 {
409 return Err(TrustformersError::quantization_error(
410 "Per-channel quantization requires at least 2D tensor".to_string(),
411 ));
412 }
413
414 let num_channels = shape[0];
415 let channel_size = data.len() / num_channels;
416
417 self.init_stats(num_channels);
418
419 let mut scales = Vec::with_capacity(num_channels);
421 let mut quantized_data = Vec::with_capacity(data.len());
422
423 for ch in 0..num_channels {
424 let channel_data = &data[ch * channel_size..(ch + 1) * channel_size];
425
426 let max_abs = channel_data
427 .iter()
428 .map(|x| x.abs())
429 .max_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal))
430 .unwrap_or(1e-8);
431
432 let scale = if let Some(stats) = &mut self.stats {
433 let stat = &mut stats[ch];
434 stat.update(max_abs, self.config.delayed_scaling.history_window);
435
436 if stat.iteration % self.config.delayed_scaling.interval == 0 {
437 stat.current_scale = stat.get_optimal_scale(
438 self.config.delayed_scaling.margin,
439 self.config.format.max_value(),
440 );
441 }
442 stat.current_scale
443 } else {
444 self.config.format.max_value() / (max_abs * 1.2)
445 };
446
447 scales.push(scale);
448
449 let ch_quantized = self.quantize_data(channel_data, scale)?;
450 quantized_data.extend(ch_quantized);
451 }
452
453 Ok(FP8Tensor {
454 data: quantized_data,
455 shape: shape.to_vec(),
456 format: self.config.format,
457 scales: ScaleFactors::PerChannel(scales),
458 zero_points: None,
459 })
460 }
461
462 fn quantize_per_token(&mut self, data: &[f32], shape: &[usize]) -> Result<FP8Tensor> {
464 if shape.len() < 2 {
465 return Err(TrustformersError::quantization_error(
466 "Per-token quantization requires at least 2D tensor [batch, seq_len, ...]"
467 .to_string(),
468 ));
469 }
470
471 let batch_size = shape[0];
473 let seq_len = if shape.len() >= 2 { shape[1] } else { 1 };
474 let num_tokens = batch_size * seq_len;
475 let token_size = data.len() / num_tokens;
476
477 self.init_stats(num_tokens);
478
479 let mut scales = Vec::with_capacity(num_tokens);
480 let mut quantized_data = Vec::with_capacity(data.len());
481
482 for tok in 0..num_tokens {
483 let token_data = &data[tok * token_size..(tok + 1) * token_size];
484
485 let max_abs = token_data
486 .iter()
487 .map(|x| x.abs())
488 .max_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal))
489 .unwrap_or(1e-8);
490
491 let scale = self.config.format.max_value() / (max_abs * 1.2);
492 scales.push(scale);
493
494 let tok_quantized = self.quantize_data(token_data, scale)?;
495 quantized_data.extend(tok_quantized);
496 }
497
498 Ok(FP8Tensor {
499 data: quantized_data,
500 shape: shape.to_vec(),
501 format: self.config.format,
502 scales: ScaleFactors::PerToken(scales),
503 zero_points: None,
504 })
505 }
506
507 fn quantize_blockwise(
509 &mut self,
510 data: &[f32],
511 shape: &[usize],
512 block_size: usize,
513 ) -> Result<FP8Tensor> {
514 let num_blocks = data.len().div_ceil(block_size);
515
516 self.init_stats(num_blocks);
517
518 let mut scales = Vec::with_capacity(num_blocks);
519 let mut quantized_data = Vec::with_capacity(data.len());
520
521 for block_idx in 0..num_blocks {
522 let start = block_idx * block_size;
523 let end = (start + block_size).min(data.len());
524 let block_data = &data[start..end];
525
526 let max_abs = block_data
527 .iter()
528 .map(|x| x.abs())
529 .max_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal))
530 .unwrap_or(1e-8);
531
532 let scale = self.config.format.max_value() / (max_abs * 1.2);
533 scales.push(scale);
534
535 let block_quantized = self.quantize_data(block_data, scale)?;
536 quantized_data.extend(block_quantized);
537 }
538
539 Ok(FP8Tensor {
540 data: quantized_data,
541 shape: shape.to_vec(),
542 format: self.config.format,
543 scales: ScaleFactors::BlockWise { scales, block_size },
544 zero_points: None,
545 })
546 }
547
548 fn quantize_data(&mut self, data: &[f32], scale: f32) -> Result<Vec<u8>> {
550 let max_value = self.config.format.max_value();
551 let mut quantized = Vec::with_capacity(data.len());
552
553 for &value in data {
554 let scaled = value * scale;
555
556 let clipped = if self.config.clip_to_max {
558 scaled.clamp(-max_value, max_value)
559 } else {
560 scaled
561 };
562
563 let fp8_val = self.f32_to_fp8(clipped)?;
565 quantized.push(fp8_val);
566 }
567
568 Ok(quantized)
569 }
570
571 fn f32_to_fp8(&mut self, value: f32) -> Result<u8> {
573 let bits = value.to_bits();
575 let sign = (bits >> 31) & 1;
576 let exp_f32 = ((bits >> 23) & 0xFF) as i32;
577 let mant_f32 = bits & 0x7F_FFFF;
578
579 if value == 0.0 || value == -0.0 {
581 return Ok((sign as u8) << 7);
582 }
583
584 if value.is_nan() || value.is_infinite() {
585 let exp_bits = self.config.format.exponent_bits();
587 let max_exp = (1 << exp_bits) - 1;
588 return Ok(
589 ((sign as u8) << 7) | ((max_exp as u8) << self.config.format.mantissa_bits())
590 );
591 }
592
593 let exp_bias_f32 = 127;
595 let exp_bias_fp8 = self.config.format.exponent_bias();
596 let exp = exp_f32 - exp_bias_f32 + exp_bias_fp8;
597
598 let max_exp = (1 << self.config.format.exponent_bits()) - 1;
600 if exp <= 0 {
601 if let Some(stats) = &mut self.stats {
603 stats[0].underflow_count += 1;
604 }
605 return Ok((sign as u8) << 7);
606 }
607 if exp >= max_exp {
608 if let Some(stats) = &mut self.stats {
610 stats[0].overflow_count += 1;
611 }
612 let max_exp_fp8 = max_exp - 1;
613 let max_mant = (1 << self.config.format.mantissa_bits()) - 1;
614 return Ok(((sign as u8) << 7)
615 | ((max_exp_fp8 as u8) << self.config.format.mantissa_bits())
616 | (max_mant as u8));
617 }
618
619 let mant_bits = self.config.format.mantissa_bits();
621 let mant_shift = 23 - mant_bits;
622 let mut mant = (mant_f32 >> mant_shift) as u8;
623
624 let remainder = mant_f32 & ((1 << mant_shift) - 1);
625
626 if self.config.stochastic_rounding {
627 let max_remainder = (1u32 << mant_shift) as f32;
629 let probability = remainder as f32 / max_remainder;
630 if self.rng.next_f32() < probability {
631 mant = mant.saturating_add(1);
632 }
633 } else {
634 if remainder > (1 << (mant_shift - 1))
636 || (remainder == (1 << (mant_shift - 1)) && (mant & 1) == 1)
637 {
638 mant = mant.saturating_add(1);
639 }
640 }
641
642 let fp8 =
644 ((sign as u8) << 7) | ((exp as u8) << mant_bits) | (mant & ((1 << mant_bits) - 1));
645
646 Ok(fp8)
647 }
648
649 fn fp8_to_f32(&self, fp8: u8) -> f32 {
651 let mant_bits = self.config.format.mantissa_bits();
652 let exp_bits = self.config.format.exponent_bits();
653
654 let sign = (fp8 >> 7) & 1;
655 let exp = ((fp8 >> mant_bits) & ((1 << exp_bits) - 1)) as i32;
656 let mant = (fp8 & ((1 << mant_bits) - 1)) as u32;
657
658 if exp == 0 && mant == 0 {
660 return if sign == 1 { -0.0 } else { 0.0 };
661 }
662
663 let exp_bias_fp8 = self.config.format.exponent_bias();
665 let exp_bias_f32 = 127;
666 let exp_f32 = exp - exp_bias_fp8 + exp_bias_f32;
667
668 let max_exp = (1 << exp_bits) - 1;
670 if exp == max_exp {
671 return if sign == 1 {
672 -self.config.format.max_value()
673 } else {
674 self.config.format.max_value()
675 };
676 }
677
678 let mant_shift = 23 - mant_bits;
680 let mant_f32 = (mant << mant_shift) | (1 << 23); let bits = ((sign as u32) << 31) | ((exp_f32 as u32) << 23) | (mant_f32 & 0x7F_FFFF);
684 f32::from_bits(bits)
685 }
686
687 pub fn dequantize(&self, fp8_tensor: &FP8Tensor) -> Result<Tensor> {
689 let mut dequantized = Vec::with_capacity(fp8_tensor.data.len());
690
691 match &fp8_tensor.scales {
692 ScaleFactors::PerTensor(scale) => {
693 for &fp8_val in &fp8_tensor.data {
694 let f32_val = self.fp8_to_f32(fp8_val) / scale;
695 dequantized.push(f32_val);
696 }
697 },
698 ScaleFactors::PerChannel(scales) => {
699 let num_channels = scales.len();
700 let channel_size = fp8_tensor.data.len() / num_channels;
701
702 for (ch, &scale) in scales.iter().enumerate() {
703 for i in 0..channel_size {
704 let idx = ch * channel_size + i;
705 let f32_val = self.fp8_to_f32(fp8_tensor.data[idx]) / scale;
706 dequantized.push(f32_val);
707 }
708 }
709 },
710 ScaleFactors::PerToken(scales) => {
711 let num_tokens = scales.len();
712 let token_size = fp8_tensor.data.len() / num_tokens;
713
714 for (tok, &scale) in scales.iter().enumerate() {
715 for i in 0..token_size {
716 let idx = tok * token_size + i;
717 let f32_val = self.fp8_to_f32(fp8_tensor.data[idx]) / scale;
718 dequantized.push(f32_val);
719 }
720 }
721 },
722 ScaleFactors::BlockWise { scales, block_size } => {
723 for (block_idx, &scale) in scales.iter().enumerate() {
724 let start = block_idx * block_size;
725 let end = (start + block_size).min(fp8_tensor.data.len());
726
727 for idx in start..end {
728 let f32_val = self.fp8_to_f32(fp8_tensor.data[idx]) / scale;
729 dequantized.push(f32_val);
730 }
731 }
732 },
733 }
734
735 Tensor::from_vec(dequantized, &fp8_tensor.shape)
736 }
737
738 pub fn get_stats(&self) -> Option<Vec<(usize, usize)>> {
740 self.stats
741 .as_ref()
742 .map(|stats| stats.iter().map(|s| (s.overflow_count, s.underflow_count)).collect())
743 }
744
745 pub fn reset_stats(&mut self) {
747 if let Some(stats) = &mut self.stats {
748 for stat in stats {
749 stat.overflow_count = 0;
750 stat.underflow_count = 0;
751 }
752 }
753 }
754}
755
756pub fn select_fp8_format(tensor: &Tensor, use_case: &str) -> FP8Format {
759 match use_case {
760 "forward" | "weights" | "activations" => FP8Format::E4M3,
761 "backward" | "gradients" => FP8Format::E5M2,
762 _ => {
763 let data = tensor.to_vec_f32().unwrap_or_default();
765 let max_abs = data
766 .iter()
767 .map(|x| x.abs())
768 .max_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal))
769 .unwrap_or(1.0);
770
771 if max_abs > 448.0 {
773 FP8Format::E5M2
774 } else {
775 FP8Format::E4M3
776 }
777 },
778 }
779}
780
781pub fn estimate_quantization_error(_original: &Tensor, _quantized: &FP8Tensor) -> Result<f32> {
783 Ok(0.0)
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn test_fp8_format_properties() {
794 let e4m3 = FP8Format::E4M3;
795 assert_eq!(e4m3.exponent_bits(), 4);
796 assert_eq!(e4m3.mantissa_bits(), 3);
797 assert_eq!(e4m3.max_value(), 448.0);
798
799 let e5m2 = FP8Format::E5M2;
800 assert_eq!(e5m2.exponent_bits(), 5);
801 assert_eq!(e5m2.mantissa_bits(), 2);
802 assert_eq!(e5m2.max_value(), 57344.0);
803 }
804
805 #[test]
806 fn test_fp8_per_tensor_quantization() -> Result<()> {
807 let config = FP8Config {
808 format: FP8Format::E4M3,
809 scaling: ScalingStrategy::PerTensor,
810 ..Default::default()
811 };
812
813 let mut quantizer = FP8Quantizer::new(config)?;
814 let tensor = Tensor::randn(&[4, 8])?;
815
816 let fp8_tensor = quantizer.quantize(&tensor)?;
817
818 assert_eq!(fp8_tensor.shape, vec![4, 8]);
819 assert_eq!(fp8_tensor.data.len(), 32);
820 assert_eq!(fp8_tensor.format, FP8Format::E4M3);
821
822 match fp8_tensor.scales {
824 ScaleFactors::PerTensor(_) => (),
825 _ => panic!("Expected PerTensor scales"),
826 }
827
828 Ok(())
829 }
830
831 #[test]
832 fn test_fp8_roundtrip() -> Result<()> {
833 let config = FP8Config {
834 format: FP8Format::E4M3,
835 stochastic_rounding: false,
836 ..Default::default()
837 };
838
839 let mut quantizer = FP8Quantizer::new(config)?;
840
841 let data = vec![0.0, 1.0, -1.0, 100.0, -100.0, 0.5, -0.5];
843 let tensor = Tensor::from_vec(data.clone(), &[7])?;
844
845 let fp8_tensor = quantizer.quantize(&tensor)?;
846 let dequantized = quantizer.dequantize(&fp8_tensor)?;
847
848 let deq_data = dequantized.to_vec_f32()?;
849
850 for (original, recovered) in data.iter().zip(deq_data.iter()) {
852 let rel_error = (original - recovered).abs() / (original.abs() + 1e-6);
853 assert!(
854 rel_error < 0.1,
855 "Relative error too large: {} vs {}",
856 original,
857 recovered
858 );
859 }
860
861 Ok(())
862 }
863
864 #[test]
865 fn test_fp8_per_channel_quantization() -> Result<()> {
866 let config = FP8Config {
867 format: FP8Format::E4M3,
868 scaling: ScalingStrategy::PerChannel,
869 ..Default::default()
870 };
871
872 let mut quantizer = FP8Quantizer::new(config)?;
873 let tensor = Tensor::randn(&[4, 8])?;
874
875 let fp8_tensor = quantizer.quantize(&tensor)?;
876
877 match &fp8_tensor.scales {
878 ScaleFactors::PerChannel(scales) => {
879 assert_eq!(scales.len(), 4); },
881 _ => panic!("Expected PerChannel scales"),
882 }
883
884 Ok(())
885 }
886
887 #[test]
888 fn test_select_fp8_format() -> Result<()> {
889 let tensor = Tensor::randn(&[10, 10])?;
890
891 let format_forward = select_fp8_format(&tensor, "forward");
892 assert_eq!(format_forward, FP8Format::E4M3);
893
894 let format_backward = select_fp8_format(&tensor, "gradients");
895 assert_eq!(format_backward, FP8Format::E5M2);
896
897 Ok(())
898 }
899
900 #[test]
901 fn test_delayed_scaling() -> Result<()> {
902 let config = FP8Config {
903 format: FP8Format::E4M3,
904 delayed_scaling: DelayedScalingConfig {
905 enabled: true,
906 interval: 2,
907 ..Default::default()
908 },
909 ..Default::default()
910 };
911
912 let mut quantizer = FP8Quantizer::new(config)?;
913
914 for _ in 0..5 {
916 let tensor = Tensor::randn(&[10, 10])?;
917 let _fp8_tensor = quantizer.quantize(&tensor)?;
918 }
919
920 assert!(quantizer.stats.is_some());
922
923 Ok(())
924 }
925
926 #[test]
929 fn test_fp8_lcg64_deterministic() {
930 let mut rng_a = Lcg64::new(123);
931 let mut rng_b = Lcg64::new(123);
932 for _ in 0..100 {
933 assert_eq!(rng_a.next_u64(), rng_b.next_u64());
934 }
935 }
936
937 #[test]
938 fn test_fp8_lcg64_different_seeds() {
939 let mut rng_a = Lcg64::new(1);
940 let mut rng_b = Lcg64::new(2);
941 let mut any_different = false;
942 for _ in 0..20 {
943 if rng_a.next_u64() != rng_b.next_u64() {
944 any_different = true;
945 break;
946 }
947 }
948 assert!(
949 any_different,
950 "Different seeds should produce different sequences"
951 );
952 }
953
954 #[test]
955 fn test_fp8_lcg64_f32_range() {
956 let mut rng = Lcg64::new(0);
957 for _ in 0..1000 {
958 let v = rng.next_f32();
959 assert!(
960 (0.0..1.0).contains(&v),
961 "next_f32 must be in [0,1): got {v}"
962 );
963 }
964 }
965
966 #[test]
967 fn test_fp8_sr_same_seed_same_output() -> Result<()> {
968 let data = vec![0.1, 0.2, 0.3, 0.4, 0.5, -0.1, -0.2, -0.3];
969 let tensor = Tensor::from_vec(data, &[8])?;
970
971 let config = FP8Config {
972 format: FP8Format::E4M3,
973 stochastic_rounding: true,
974 ..Default::default()
975 };
976
977 let mut q1 = FP8Quantizer::with_seed(config.clone(), 99)?;
978 let mut q2 = FP8Quantizer::with_seed(config, 99)?;
979
980 let r1 = q1.quantize(&tensor)?;
981 let r2 = q2.quantize(&tensor)?;
982
983 assert_eq!(
984 r1.data, r2.data,
985 "Same seed must produce identical quantized bytes"
986 );
987 Ok(())
988 }
989
990 #[test]
991 fn test_fp8_sr_different_seed_different_output() -> Result<()> {
992 let data: Vec<f32> = (0..256).map(|i| (i as f32 - 128.0) * 0.01).collect();
994 let tensor = Tensor::from_vec(data, &[256])?;
995
996 let config = FP8Config {
997 format: FP8Format::E4M3,
998 stochastic_rounding: true,
999 ..Default::default()
1000 };
1001
1002 let mut q1 = FP8Quantizer::with_seed(config.clone(), 1)?;
1003 let mut q2 = FP8Quantizer::with_seed(config, 9999)?;
1004
1005 let r1 = q1.quantize(&tensor)?;
1006 let r2 = q2.quantize(&tensor)?;
1007
1008 assert_ne!(
1009 r1.data, r2.data,
1010 "Different seeds should produce different outputs"
1011 );
1012 Ok(())
1013 }
1014
1015 #[test]
1016 fn test_fp8_sr_unbiased_e4m3() -> Result<()> {
1017 let original_val = 1.23_f32;
1020 let data = vec![original_val; 1];
1021 let tensor = Tensor::from_vec(data, &[1])?;
1022
1023 let num_trials = 2000;
1024 let mut sum = 0.0_f64;
1025
1026 for seed in 0..num_trials {
1027 let config = FP8Config {
1028 format: FP8Format::E4M3,
1029 stochastic_rounding: true,
1030 scaling: ScalingStrategy::PerTensor,
1031 ..Default::default()
1032 };
1033 let mut q = FP8Quantizer::with_seed(config, seed)?;
1034 let quantized = q.quantize(&tensor)?;
1035 let deq = q.dequantize(&quantized)?;
1036 let v = deq.to_vec_f32()?;
1037 sum += v[0] as f64;
1038 }
1039
1040 let mean = sum / num_trials as f64;
1041 let rel_err = ((mean - original_val as f64) / original_val as f64).abs();
1042 assert!(
1043 rel_err < 0.05,
1044 "SR mean should be close to original for E4M3: mean={mean}, expected={original_val}, rel_err={rel_err}"
1045 );
1046 Ok(())
1047 }
1048
1049 #[test]
1050 fn test_fp8_sr_unbiased_e5m2() -> Result<()> {
1051 let original_val = 2.71_f32;
1052 let data = vec![original_val; 1];
1053 let tensor = Tensor::from_vec(data, &[1])?;
1054
1055 let num_trials = 2000;
1056 let mut sum = 0.0_f64;
1057
1058 for seed in 0..num_trials {
1059 let config = FP8Config {
1060 format: FP8Format::E5M2,
1061 stochastic_rounding: true,
1062 scaling: ScalingStrategy::PerTensor,
1063 ..Default::default()
1064 };
1065 let mut q = FP8Quantizer::with_seed(config, seed)?;
1066 let quantized = q.quantize(&tensor)?;
1067 let deq = q.dequantize(&quantized)?;
1068 let v = deq.to_vec_f32()?;
1069 sum += v[0] as f64;
1070 }
1071
1072 let mean = sum / num_trials as f64;
1073 let rel_err = ((mean - original_val as f64) / original_val as f64).abs();
1074 assert!(
1075 rel_err < 0.05,
1076 "SR mean should be close to original for E5M2: mean={mean}, expected={original_val}, rel_err={rel_err}"
1077 );
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn test_fp8_sr_zero_exact() -> Result<()> {
1083 let data = vec![0.0_f32; 4];
1084 let tensor = Tensor::from_vec(data, &[4])?;
1085
1086 let config = FP8Config {
1087 format: FP8Format::E4M3,
1088 stochastic_rounding: true,
1089 ..Default::default()
1090 };
1091
1092 let mut q = FP8Quantizer::with_seed(config, 77)?;
1093 let quantized = q.quantize(&tensor)?;
1094 let deq = q.dequantize(&quantized)?;
1095 let v = deq.to_vec_f32()?;
1096
1097 for val in &v {
1098 assert!(
1099 val.abs() < 1e-12,
1100 "Zero should remain exactly zero, got {val}"
1101 );
1102 }
1103 Ok(())
1104 }
1105
1106 #[test]
1107 fn test_fp8_sr_nan_handled() -> Result<()> {
1108 let data = vec![f32::NAN];
1109 let tensor = Tensor::from_vec(data, &[1])?;
1110
1111 let config = FP8Config {
1112 format: FP8Format::E4M3,
1113 stochastic_rounding: true,
1114 ..Default::default()
1115 };
1116
1117 let mut q = FP8Quantizer::with_seed(config, 42)?;
1118 let quantized = q.quantize(&tensor)?;
1119 let deq = q.dequantize(&quantized)?;
1121 let v = deq.to_vec_f32()?;
1122 assert!(
1123 v[0].is_finite(),
1124 "NaN should be mapped to a finite FP8 value"
1125 );
1126 Ok(())
1127 }
1128
1129 #[test]
1130 fn test_fp8_sr_inf_handled() -> Result<()> {
1131 let data = vec![f32::INFINITY, f32::NEG_INFINITY];
1132 let tensor = Tensor::from_vec(data, &[2])?;
1133
1134 let config = FP8Config {
1135 format: FP8Format::E4M3,
1136 stochastic_rounding: true,
1137 ..Default::default()
1138 };
1139
1140 let mut q = FP8Quantizer::with_seed(config, 42)?;
1141 let quantized = q.quantize(&tensor)?;
1142 let deq = q.dequantize(&quantized)?;
1143 let v = deq.to_vec_f32()?;
1144 assert!(
1145 v[0].is_finite(),
1146 "Inf should be mapped to a finite FP8 value"
1147 );
1148 assert!(
1149 v[1].is_finite(),
1150 "Neg-Inf should be mapped to a finite FP8 value"
1151 );
1152 Ok(())
1153 }
1154
1155 #[test]
1156 fn test_fp8_sr_vs_rne_different_results() -> Result<()> {
1157 let data: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) * 0.013).collect();
1159 let tensor = Tensor::from_vec(data, &[128])?;
1160
1161 let config_sr = FP8Config {
1162 format: FP8Format::E4M3,
1163 stochastic_rounding: true,
1164 ..Default::default()
1165 };
1166 let config_rne = FP8Config {
1167 format: FP8Format::E4M3,
1168 stochastic_rounding: false,
1169 ..Default::default()
1170 };
1171
1172 let mut q_sr = FP8Quantizer::with_seed(config_sr, 7)?;
1173 let mut q_rne = FP8Quantizer::new(config_rne)?;
1174
1175 let r_sr = q_sr.quantize(&tensor)?;
1176 let r_rne = q_rne.quantize(&tensor)?;
1177
1178 let num_diff = r_sr.data.iter().zip(r_rne.data.iter()).filter(|(a, b)| a != b).count();
1180 assert!(
1181 num_diff > 0,
1182 "Stochastic rounding should produce at least some different results vs RNE"
1183 );
1184 Ok(())
1185 }
1186
1187 #[test]
1188 fn test_fp8_sr_per_channel() -> Result<()> {
1189 let data: Vec<f32> = (0..32).map(|i| (i as f32 - 16.0) * 0.05).collect();
1190 let tensor = Tensor::from_vec(data, &[4, 8])?;
1191
1192 let config = FP8Config {
1193 format: FP8Format::E4M3,
1194 scaling: ScalingStrategy::PerChannel,
1195 stochastic_rounding: true,
1196 ..Default::default()
1197 };
1198
1199 let mut q = FP8Quantizer::with_seed(config, 42)?;
1200 let quantized = q.quantize(&tensor)?;
1201
1202 match &quantized.scales {
1203 ScaleFactors::PerChannel(scales) => assert_eq!(scales.len(), 4),
1204 _ => panic!("Expected PerChannel scales"),
1205 }
1206
1207 let _deq = q.dequantize(&quantized)?;
1209 Ok(())
1210 }
1211
1212 #[test]
1213 fn test_fp8_sr_blockwise() -> Result<()> {
1214 let data: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.02).collect();
1215 let tensor = Tensor::from_vec(data, &[64])?;
1216
1217 let config = FP8Config {
1218 format: FP8Format::E4M3,
1219 scaling: ScalingStrategy::BlockWise { block_size: 16 },
1220 stochastic_rounding: true,
1221 ..Default::default()
1222 };
1223
1224 let mut q = FP8Quantizer::with_seed(config, 42)?;
1225 let quantized = q.quantize(&tensor)?;
1226
1227 match &quantized.scales {
1228 ScaleFactors::BlockWise { scales, block_size } => {
1229 assert_eq!(scales.len(), 4);
1230 assert_eq!(*block_size, 16);
1231 },
1232 _ => panic!("Expected BlockWise scales"),
1233 }
1234
1235 let _deq = q.dequantize(&quantized)?;
1236 Ok(())
1237 }
1238
1239 #[test]
1240 fn test_fp8_sr_per_token() -> Result<()> {
1241 let data: Vec<f32> = (0..48).map(|i| (i as f32 - 24.0) * 0.03).collect();
1242 let tensor = Tensor::from_vec(data, &[2, 3, 8])?;
1244
1245 let config = FP8Config {
1246 format: FP8Format::E4M3,
1247 scaling: ScalingStrategy::PerToken,
1248 stochastic_rounding: true,
1249 ..Default::default()
1250 };
1251
1252 let mut q = FP8Quantizer::with_seed(config, 42)?;
1253 let quantized = q.quantize(&tensor)?;
1254
1255 match &quantized.scales {
1256 ScaleFactors::PerToken(scales) => assert_eq!(scales.len(), 6), _ => panic!("Expected PerToken scales"),
1258 }
1259
1260 let _deq = q.dequantize(&quantized)?;
1261 Ok(())
1262 }
1263
1264 #[test]
1265 fn test_fp8_sr_e5m2_roundtrip() -> Result<()> {
1266 let data = vec![0.0, 1.0, -1.0, 50.0, -50.0, 0.25, -0.25];
1267 let tensor = Tensor::from_vec(data.clone(), &[7])?;
1268
1269 let config = FP8Config {
1270 format: FP8Format::E5M2,
1271 stochastic_rounding: true,
1272 ..Default::default()
1273 };
1274
1275 let mut q = FP8Quantizer::with_seed(config, 42)?;
1276 let quantized = q.quantize(&tensor)?;
1277 let deq = q.dequantize(&quantized)?;
1278 let v = deq.to_vec_f32()?;
1279
1280 for (orig, rec) in data.iter().zip(v.iter()) {
1281 let err = (orig - rec).abs() / (orig.abs() + 1e-6);
1282 assert!(
1283 err < 0.2,
1284 "E5M2 SR roundtrip error too large: orig={orig}, rec={rec}"
1285 );
1286 }
1287 Ok(())
1288 }
1289
1290 #[test]
1291 fn test_fp8_with_seed_constructor() -> Result<()> {
1292 let config = FP8Config {
1293 format: FP8Format::E4M3,
1294 stochastic_rounding: true,
1295 ..Default::default()
1296 };
1297
1298 let q = FP8Quantizer::with_seed(config, 12345)?;
1299 assert_eq!(q.rng.state, 12345);
1301 Ok(())
1302 }
1303
1304 #[test]
1305 fn test_fp8_default_seed_is_42() -> Result<()> {
1306 let config = FP8Config::default();
1307 let q = FP8Quantizer::new(config)?;
1308 assert_eq!(q.rng.state, 42);
1309 Ok(())
1310 }
1311
1312 #[test]
1313 fn test_fp8_sr_variance_nonzero() -> Result<()> {
1314 let data = vec![1.23456_f32; 1];
1317 let tensor = Tensor::from_vec(data, &[1])?;
1318
1319 let mut results = Vec::with_capacity(100);
1320 for seed in 0..100_u64 {
1321 let config = FP8Config {
1322 format: FP8Format::E4M3,
1323 stochastic_rounding: true,
1324 ..Default::default()
1325 };
1326 let mut q = FP8Quantizer::with_seed(config, seed)?;
1327 let quantized = q.quantize(&tensor)?;
1328 results.push(quantized.data[0]);
1329 }
1330
1331 results.sort();
1333 results.dedup();
1334 assert!(
1335 results.len() >= 2,
1336 "SR should produce variance across seeds, but got only {} distinct values",
1337 results.len()
1338 );
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn test_fp8_rne_deterministic_without_seed() -> Result<()> {
1344 let data: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.01).collect();
1346 let tensor = Tensor::from_vec(data, &[64])?;
1347
1348 let config = FP8Config {
1349 format: FP8Format::E4M3,
1350 stochastic_rounding: false,
1351 ..Default::default()
1352 };
1353
1354 let mut q1 = FP8Quantizer::new(config.clone())?;
1355 let mut q2 = FP8Quantizer::with_seed(config, 9999)?;
1356
1357 let r1 = q1.quantize(&tensor)?;
1358 let r2 = q2.quantize(&tensor)?;
1359
1360 assert_eq!(
1361 r1.data, r2.data,
1362 "RNE mode must be deterministic regardless of seed"
1363 );
1364 Ok(())
1365 }
1366
1367 #[test]
1368 fn test_fp8_sr_negative_values_unbiased() -> Result<()> {
1369 let original_val = -0.77_f32;
1370 let data = vec![original_val; 1];
1371 let tensor = Tensor::from_vec(data, &[1])?;
1372
1373 let num_trials = 2000;
1374 let mut sum = 0.0_f64;
1375
1376 for seed in 0..num_trials {
1377 let config = FP8Config {
1378 format: FP8Format::E4M3,
1379 stochastic_rounding: true,
1380 scaling: ScalingStrategy::PerTensor,
1381 ..Default::default()
1382 };
1383 let mut q = FP8Quantizer::with_seed(config, seed)?;
1384 let quantized = q.quantize(&tensor)?;
1385 let deq = q.dequantize(&quantized)?;
1386 let v = deq.to_vec_f32()?;
1387 sum += v[0] as f64;
1388 }
1389
1390 let mean = sum / num_trials as f64;
1391 let rel_err = ((mean - original_val as f64) / original_val as f64).abs();
1392 assert!(
1393 rel_err < 0.05,
1394 "SR should be unbiased for negative values: mean={mean}, expected={original_val}, rel_err={rel_err}"
1395 );
1396 Ok(())
1397 }
1398}