Skip to main content

trustformers_core/quantization/
fp8.rs

1//! FP8 quantization for modern GPU architectures
2//!
3//! This module implements FP8 (8-bit floating point) quantization, which is natively
4//! supported on modern GPUs like NVIDIA H100, AMD MI300, and future accelerators.
5//!
6//! FP8 comes in two main formats:
7//! - **E4M3** (4-bit exponent, 3-bit mantissa): Better dynamic range for forward pass
8//! - **E5M2** (5-bit exponent, 2-bit mantissa): Better precision for gradients
9//!
10//! # Features
11//! - Native FP8 tensor quantization and dequantization
12//! - Per-tensor and per-channel scaling
13//! - Delayed scaling for training stability
14//! - Automatic format selection based on use case
15//! - Hardware-accelerated operations when available
16//! - Integration with mixed-precision training
17//!
18//! # Examples
19//!
20//! ```rust,no_run
21//! use trustformers_core::quantization::{FP8Config, FP8Quantizer, FP8Format};
22//! use trustformers_core::tensor::Tensor;
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let config = FP8Config {
26//!     format: FP8Format::E4M3,
27//!     ..Default::default()
28//! };
29//!
30//! let mut quantizer = FP8Quantizer::new(config)?;
31//! let tensor = Tensor::randn(&[1024, 768])?;
32//!
33//! // Quantize to FP8
34//! let quantized = quantizer.quantize(&tensor)?;
35//!
36//! // Dequantize back
37//! let dequantized = quantizer.dequantize(&quantized)?;
38//! # Ok(())
39//! # }
40//! ```
41
42use crate::errors::{Result, TrustformersError};
43use crate::tensor::Tensor;
44use serde::{Deserialize, Serialize};
45
46/// FP8 data format specification
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum FP8Format {
49    /// E4M3: 4-bit exponent, 3-bit mantissa (sign: 1, exp: 4, mantissa: 3)
50    /// Range: ±448, better dynamic range
51    /// Best for: Forward pass, activations, weights
52    E4M3,
53
54    /// E5M2: 5-bit exponent, 2-bit mantissa (sign: 1, exp: 5, mantissa: 2)
55    /// Range: ±57344, wider range but less precision
56    /// Best for: Gradients, loss scaling
57    E5M2,
58}
59
60impl FP8Format {
61    /// Maximum representable value for this format
62    pub fn max_value(&self) -> f32 {
63        match self {
64            FP8Format::E4M3 => 448.0,
65            FP8Format::E5M2 => 57344.0,
66        }
67    }
68
69    /// Minimum positive normal value
70    pub fn min_positive_normal(&self) -> f32 {
71        match self {
72            FP8Format::E4M3 => 2.0f32.powi(-9),  // 2^-9
73            FP8Format::E5M2 => 2.0f32.powi(-16), // 2^-16
74        }
75    }
76
77    /// Number of mantissa bits
78    pub fn mantissa_bits(&self) -> u8 {
79        match self {
80            FP8Format::E4M3 => 3,
81            FP8Format::E5M2 => 2,
82        }
83    }
84
85    /// Number of exponent bits
86    pub fn exponent_bits(&self) -> u8 {
87        match self {
88            FP8Format::E4M3 => 4,
89            FP8Format::E5M2 => 5,
90        }
91    }
92
93    /// Exponent bias
94    pub fn exponent_bias(&self) -> i32 {
95        match self {
96            FP8Format::E4M3 => 7,  // 2^(4-1) - 1
97            FP8Format::E5M2 => 15, // 2^(5-1) - 1
98        }
99    }
100}
101
102/// Scaling strategy for FP8 quantization
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104pub enum ScalingStrategy {
105    /// Per-tensor scaling: single scale factor for entire tensor
106    PerTensor,
107
108    /// Per-channel scaling: scale factor per output channel
109    PerChannel,
110
111    /// Per-token scaling: scale factor per token (for sequence models)
112    PerToken,
113
114    /// Block-wise scaling: scale factor per fixed-size block
115    BlockWise { block_size: usize },
116}
117
118/// Delayed scaling configuration for training
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct DelayedScalingConfig {
121    /// Enable delayed scaling
122    pub enabled: bool,
123
124    /// Number of intervals before updating scale
125    pub interval: usize,
126
127    /// Margin factor (multiplier for scale to prevent overflow)
128    pub margin: f32,
129
130    /// Update threshold (fraction of max value to trigger update)
131    pub update_threshold: f32,
132
133    /// History window for statistics
134    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/// FP8 quantization configuration
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct FP8Config {
152    /// FP8 format to use
153    pub format: FP8Format,
154
155    /// Scaling strategy
156    pub scaling: ScalingStrategy,
157
158    /// Delayed scaling configuration
159    pub delayed_scaling: DelayedScalingConfig,
160
161    /// Enable stochastic rounding for better accuracy
162    pub stochastic_rounding: bool,
163
164    /// Clipping strategy (clip to max or saturate)
165    pub clip_to_max: bool,
166
167    /// Use hardware FP8 operations if available
168    pub use_hardware_ops: bool,
169
170    /// Calibration samples for initial scale estimation
171    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/// FP8 quantized tensor representation
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct FP8Tensor {
191    /// Quantized data stored as u8 (bitwise FP8 representation)
192    pub data: Vec<u8>,
193
194    /// Original tensor shape
195    pub shape: Vec<usize>,
196
197    /// FP8 format used
198    pub format: FP8Format,
199
200    /// Scale factors (shape depends on scaling strategy)
201    pub scales: ScaleFactors,
202
203    /// Zero points (if using asymmetric quantization)
204    pub zero_points: Option<Vec<f32>>,
205}
206
207/// Scale factors for FP8 quantization
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum ScaleFactors {
210    /// Single scale for entire tensor
211    PerTensor(f32),
212
213    /// Per-channel scales
214    PerChannel(Vec<f32>),
215
216    /// Per-token scales
217    PerToken(Vec<f32>),
218
219    /// Block-wise scales
220    BlockWise { scales: Vec<f32>, block_size: usize },
221}
222
223/// FP8 quantization statistics for delayed scaling
224#[derive(Debug, Clone)]
225struct QuantStats {
226    /// Maximum absolute values history
227    max_history: Vec<f32>,
228
229    /// Current iteration counter
230    iteration: usize,
231
232    /// Current scale factor
233    current_scale: f32,
234
235    /// Number of overflow events
236    overflow_count: usize,
237
238    /// Number of underflow events
239    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        // Use percentile instead of max to be robust to outliers
267        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/// Linear congruential generator for deterministic pseudo-random numbers.
276///
277/// Uses the constants from Knuth's MMIX:
278/// - `a = 6364136223846793005`
279/// - `c = 1442695040888963407`
280#[derive(Debug, Clone)]
281pub struct Lcg64 {
282    state: u64,
283}
284
285impl Lcg64 {
286    /// LCG multiplier (Knuth MMIX)
287    const A: u64 = 6_364_136_223_846_793_005;
288    /// LCG increment (Knuth MMIX)
289    const C: u64 = 1_442_695_040_888_963_407;
290
291    /// Create a new LCG with the given seed.
292    pub fn new(seed: u64) -> Self {
293        Self { state: seed }
294    }
295
296    /// Return the next pseudo-random `u64` and advance the state.
297    #[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    /// Return a pseudo-random `f32` in `[0, 1)`.
304    #[inline]
305    pub fn next_f32(&mut self) -> f32 {
306        // Use upper 24 bits for best quality, map to [0, 1).
307        (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
308    }
309}
310
311/// FP8 quantizer with delayed scaling support
312pub struct FP8Quantizer {
313    /// Configuration
314    config: FP8Config,
315
316    /// Statistics for delayed scaling (per channel or per tensor)
317    stats: Option<Vec<QuantStats>>,
318
319    /// Pseudo-random number generator for stochastic rounding
320    rng: Lcg64,
321}
322
323impl FP8Quantizer {
324    /// Create a new FP8 quantizer with a default seed of 42.
325    pub fn new(config: FP8Config) -> Result<Self> {
326        Ok(Self {
327            config,
328            stats: None,
329            rng: Lcg64::new(42),
330        })
331    }
332
333    /// Create a new FP8 quantizer with a specific seed for reproducibility.
334    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    /// Initialize statistics for delayed scaling
343    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    /// Quantize a tensor to FP8
353    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    /// Per-tensor quantization
368    fn quantize_per_tensor(&mut self, data: &[f32], shape: &[usize]) -> Result<FP8Tensor> {
369        self.init_stats(1);
370
371        // Compute max absolute value
372        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        // Compute or update scale
379        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        // Quantize data
395        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    /// Per-channel quantization
407    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        // Compute per-channel scales
420        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    /// Per-token quantization (for sequence models)
463    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        // Assume shape is [batch, seq_len, hidden_dim] or similar
472        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    /// Block-wise quantization
508    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    /// Core quantization logic: convert f32 values to FP8 representation
549    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            // Clip to FP8 range
557            let clipped = if self.config.clip_to_max {
558                scaled.clamp(-max_value, max_value)
559            } else {
560                scaled
561            };
562
563            // Convert to FP8 (simplified - actual implementation would use proper IEEE conversion)
564            let fp8_val = self.f32_to_fp8(clipped)?;
565            quantized.push(fp8_val);
566        }
567
568        Ok(quantized)
569    }
570
571    /// Convert f32 to FP8 bitwise representation
572    fn f32_to_fp8(&mut self, value: f32) -> Result<u8> {
573        // Extract sign, exponent, and mantissa from f32
574        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        // Handle special cases
580        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            // Map to max FP8 value
586            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        // Rebias exponent
594        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        // Check bounds
599        let max_exp = (1 << self.config.format.exponent_bits()) - 1;
600        if exp <= 0 {
601            // Subnormal or underflow - map to zero
602            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            // Overflow - saturate to max
609            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        // Extract mantissa bits
620        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            // Stochastic rounding: probability of rounding up = remainder / max_remainder
628            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            // Round to nearest even (RNE)
635            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        // Combine sign, exponent, mantissa
643        let fp8 =
644            ((sign as u8) << 7) | ((exp as u8) << mant_bits) | (mant & ((1 << mant_bits) - 1));
645
646        Ok(fp8)
647    }
648
649    /// Convert FP8 bitwise representation to f32
650    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        // Handle zero
659        if exp == 0 && mant == 0 {
660            return if sign == 1 { -0.0 } else { 0.0 };
661        }
662
663        // Rebias to f32 exponent
664        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        // Handle special cases
669        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        // Construct f32 mantissa
679        let mant_shift = 23 - mant_bits;
680        let mant_f32 = (mant << mant_shift) | (1 << 23); // Add implicit leading 1
681
682        // Construct f32
683        let bits = ((sign as u32) << 31) | ((exp_f32 as u32) << 23) | (mant_f32 & 0x7F_FFFF);
684        f32::from_bits(bits)
685    }
686
687    /// Dequantize FP8 tensor back to f32
688    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    /// Get quantization statistics
739    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    /// Reset statistics
746    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
756/// Utility functions for FP8 quantization
757/// Automatic format selection based on tensor characteristics
758pub 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            // Analyze tensor statistics
764            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 range is large, use E5M2, otherwise E4M3
772            if max_abs > 448.0 {
773                FP8Format::E5M2
774            } else {
775                FP8Format::E4M3
776            }
777        },
778    }
779}
780
781/// Estimate quantization error
782pub fn estimate_quantization_error(_original: &Tensor, _quantized: &FP8Tensor) -> Result<f32> {
783    // This would require dequantization and comparison
784    // Simplified implementation
785    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        // Check that scales are per-tensor
823        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        // Create a simple tensor with known values
842        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        // Check that values are approximately preserved
851        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); // 4 channels
880            },
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        // Quantize multiple times to test delayed scaling
915        for _ in 0..5 {
916            let tensor = Tensor::randn(&[10, 10])?;
917            let _fp8_tensor = quantizer.quantize(&tensor)?;
918        }
919
920        // Check that stats are being tracked
921        assert!(quantizer.stats.is_some());
922
923        Ok(())
924    }
925
926    // ── Stochastic rounding tests ──────────────────────────────────────
927
928    #[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        // Use many elements to make collision astronomically unlikely.
993        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        // Quantize/dequantize many times with different seeds. The mean of
1018        // the dequantized values should converge to the original value.
1019        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        // NaN is mapped to max FP8 value; dequantization must not panic.
1120        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        // Many values: SR and RNE should sometimes disagree on rounding direction.
1158        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        // They should not be identical (SR introduces randomness).
1179        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        // Dequantize should succeed without error.
1208        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        // shape [2, 3, 8]: 2 batches, 3 tokens each, hidden=8
1243        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), // 2*3 tokens
1257            _ => 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        // Verify the quantizer was created and the rng state is set.
1300        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        // With SR, repeated quantization with different seeds should produce variance.
1315        // Use a value that is NOT exactly representable in E4M3 to ensure non-zero remainder.
1316        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        // There should be at least 2 distinct quantized byte values.
1332        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        // RNE (non-stochastic) should always produce the same result.
1345        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}