1use crate::common::{OptimizerState, StateMemoryStats};
14use crate::traits::StatefulOptimizer;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use trustformers_core::errors::{Result, TrustformersError};
18use trustformers_core::tensor::Tensor;
19use trustformers_core::traits::Optimizer;
20
21const NF4_VALUES: [f32; 16] = [
23 -1.0,
24 -0.696_192_8,
25 -0.525_073_05,
26 -0.394_917_5,
27 -0.284_441_38,
28 -0.184_773_43,
29 -0.091_050_036,
30 0.0,
31 0.079_580_3,
32 0.160_930_2,
33 0.246_112_3,
34 0.337_915_24,
35 0.440_709_83,
36 0.562_617,
37 0.722_956_84,
38 1.0,
39];
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AdvancedQuantizationConfig {
44 pub method: QuantizationMethod,
46 pub block_size: usize,
48 pub adaptation_rate: f32,
50 pub min_scale: f32,
52 pub max_scale: f32,
54 pub double_quantization: bool,
56}
57
58impl Default for AdvancedQuantizationConfig {
59 fn default() -> Self {
60 Self {
61 method: QuantizationMethod::NF4,
62 block_size: 64,
63 adaptation_rate: 0.01,
64 min_scale: 1e-8,
65 max_scale: 1e8,
66 double_quantization: true,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub enum QuantizationMethod {
74 Int4,
76 NF4,
78 Int8,
80 Dynamic,
82 BlockWise,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct QuantizedTensor {
89 pub data: Vec<f32>,
91 pub scales: Vec<f32>,
93 pub zero_points: Vec<f32>,
95 pub shape: Vec<usize>,
97 pub method: QuantizationMethod,
99 pub block_size: usize,
101}
102
103impl QuantizedTensor {
104 pub fn new(
106 data: Vec<f32>,
107 scales: Vec<f32>,
108 zero_points: Vec<f32>,
109 shape: Vec<usize>,
110 method: QuantizationMethod,
111 block_size: usize,
112 ) -> Self {
113 Self {
114 data,
115 scales,
116 zero_points,
117 shape,
118 method,
119 block_size,
120 }
121 }
122
123 pub fn memory_usage(&self) -> usize {
125 self.data.len() * 4 + self.scales.len() * 4 + self.zero_points.len() * 4
127 }
128
129 pub fn compression_ratio(&self) -> f32 {
131 let original_size = self.shape.iter().product::<usize>() * 4; match self.method {
135 QuantizationMethod::NF4 | QuantizationMethod::Int4 => 8.0, QuantizationMethod::Int8 => 4.0, _ => {
138 let compressed_size = self.memory_usage();
139 if compressed_size > 0 {
140 original_size as f32 / compressed_size as f32
141 } else {
142 1.0
143 }
144 },
145 }
146 }
147}
148
149pub struct QuantizationUtils;
151
152impl QuantizationUtils {
153 pub fn quantize_nf4(tensor: &Tensor, block_size: usize) -> Result<QuantizedTensor> {
155 let data = tensor.data()?;
156 let shape = tensor.shape();
157 let num_elements = data.len();
158 let num_blocks = num_elements.div_ceil(block_size);
159
160 let mut quantized_data = Vec::new();
161 let mut scales = Vec::with_capacity(num_blocks);
162 let mut zero_points = Vec::with_capacity(num_blocks);
163
164 for block_idx in 0..num_blocks {
165 let start = block_idx * block_size;
166 let end = (start + block_size).min(num_elements);
167 let block = &data[start..end];
168
169 let min_val = block.iter().fold(f32::INFINITY, |a, &b| a.min(b));
172 let max_val = block.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
173
174 let span = max_val - min_val;
178 let scale = if span.is_finite() && span > 0.0 { span } else { 0.0 };
179
180 scales.push(scale);
181 zero_points.push(min_val);
182
183 for &value in block {
184 if scale == 0.0 {
185 quantized_data.push(-1.0);
188 continue;
189 }
190 let normalized = 2.0 * (value - min_val) / scale - 1.0;
193 quantized_data.push(Self::find_closest_nf4(normalized));
194 }
195 }
196
197 Ok(QuantizedTensor::new(
198 quantized_data,
199 scales,
200 zero_points,
201 shape,
202 QuantizationMethod::NF4,
203 block_size,
204 ))
205 }
206
207 fn find_closest_nf4(value: f32) -> f32 {
209 let clamped = value.clamp(-1.0, 1.0);
210 let mut best_val = NF4_VALUES[0];
211 let mut best_diff = (NF4_VALUES[0] - clamped).abs();
212
213 for &nf4_val in NF4_VALUES.iter() {
214 let diff = (nf4_val - clamped).abs();
215 if diff < best_diff {
216 best_diff = diff;
217 best_val = nf4_val;
218 }
219 }
220
221 best_val
222 }
223
224 pub fn dequantize_nf4(quantized: &QuantizedTensor) -> Result<Tensor> {
231 let num_elements: usize = quantized.shape.iter().product();
232 let mut data = Vec::with_capacity(num_elements);
233 let block_size = quantized.block_size;
234 let num_blocks = num_elements.div_ceil(block_size);
235
236 let mut data_idx = 0;
237
238 for block_idx in 0..num_blocks {
239 let start = block_idx * block_size;
240 let end = (start + block_size).min(num_elements);
241 let block_len = end - start;
242
243 let scale = quantized.scales[block_idx];
244 let zero_point = quantized.zero_points[block_idx];
245
246 for _ in 0..block_len {
247 if data_idx < quantized.data.len() {
248 let nf4_val = quantized.data[data_idx];
249 let dequantized = zero_point + (nf4_val + 1.0) * 0.5 * scale;
251 data.push(dequantized);
252 data_idx += 1;
253 }
254 }
255 }
256
257 Tensor::new(data)
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct GradientStatistics {
264 pub mean: f32,
265 pub variance: f32,
266 pub skewness: f32,
267 pub kurtosis: f32,
268 pub l2_norm: f32,
269}
270
271impl GradientStatistics {
272 pub fn compute(data: &[f32]) -> Self {
274 let n = data.len() as f32;
275 let mean = data.iter().sum::<f32>() / n;
276
277 let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / n;
278
279 let std_dev = variance.sqrt();
280
281 let skewness = if std_dev > 1e-8 {
282 data.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f32>() / n
283 } else {
284 0.0
285 };
286
287 let kurtosis = if std_dev > 1e-8 {
288 data.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f32>() / n - 3.0
289 } else {
291 0.0
292 };
293
294 let l2_norm = data.iter().map(|x| x * x).sum::<f32>().sqrt();
295
296 Self {
297 mean,
298 variance,
299 skewness,
300 kurtosis,
301 l2_norm,
302 }
303 }
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Adam4bitOptimizerConfig {
309 pub learning_rate: f32,
310 pub beta1: f32,
311 pub beta2: f32,
312 pub epsilon: f32,
313 pub weight_decay: f32,
314 pub decoupled_weight_decay: bool,
319}
320
321impl Default for Adam4bitOptimizerConfig {
322 fn default() -> Self {
323 Self {
324 learning_rate: 1e-3,
325 beta1: 0.9,
326 beta2: 0.999,
327 epsilon: 1e-8,
328 weight_decay: 0.0,
329 decoupled_weight_decay: false,
330 }
331 }
332}
333
334#[derive(Debug)]
336pub struct Adam4bit {
337 config: AdvancedQuantizationConfig,
338 optimizer_config: Adam4bitOptimizerConfig,
339 state: OptimizerState,
340 momentum_quantized: HashMap<String, QuantizedTensor>,
342 variance_quantized: HashMap<String, QuantizedTensor>,
344 gradient_stats: HashMap<String, GradientStatistics>,
345}
346
347impl Adam4bit {
348 pub fn new(
350 learning_rate: f32,
351 beta1: f32,
352 beta2: f32,
353 epsilon: f32,
354 weight_decay: f32,
355 ) -> Self {
356 let optimizer_config = Adam4bitOptimizerConfig {
357 learning_rate,
358 beta1,
359 beta2,
360 epsilon,
361 weight_decay,
362 decoupled_weight_decay: false,
363 };
364
365 Self {
366 config: AdvancedQuantizationConfig::default(),
367 optimizer_config,
368 state: OptimizerState::new(),
369 momentum_quantized: HashMap::new(),
370 variance_quantized: HashMap::new(),
371 gradient_stats: HashMap::new(),
372 }
373 }
374
375 pub fn with_quantization_config(
377 optimizer_config: Adam4bitOptimizerConfig,
378 quantization_config: AdvancedQuantizationConfig,
379 ) -> Self {
380 Self {
381 config: quantization_config,
382 optimizer_config,
383 state: OptimizerState::new(),
384 momentum_quantized: HashMap::new(),
385 variance_quantized: HashMap::new(),
386 gradient_stats: HashMap::new(),
387 }
388 }
389
390 pub fn memory_savings(&self) -> f32 {
392 0.75
394 }
395
396 pub fn set_decoupled_weight_decay(&mut self, decoupled: bool) {
398 self.optimizer_config.decoupled_weight_decay = decoupled;
399 }
400
401 fn update_gradient_stats(&mut self, param_id: &str, gradient_data: &[f32]) {
403 let stats = GradientStatistics::compute(gradient_data);
404
405 if let Some(existing_stats) = self.gradient_stats.get_mut(param_id) {
407 let alpha = self.config.adaptation_rate;
408 existing_stats.mean = (1.0 - alpha) * existing_stats.mean + alpha * stats.mean;
409 existing_stats.variance =
410 (1.0 - alpha) * existing_stats.variance + alpha * stats.variance;
411 } else {
412 self.gradient_stats.insert(param_id.to_string(), stats);
413 }
414 }
415}
416
417impl Optimizer for Adam4bit {
418 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
419 match (parameter, grad) {
420 (Tensor::F32(param), Tensor::F32(grad_arr)) => {
421 let param_id = self.state.param_key(param.as_ptr() as usize, param.len())?;
422 let size = grad_arr.len();
423
424 self.update_gradient_stats(
426 ¶m_id,
427 &grad_arr.iter().cloned().collect::<Vec<f32>>(),
428 );
429
430 if !self.momentum_quantized.contains_key(¶m_id) {
432 let zeros = vec![0.0; size];
433 let zero_tensor = Tensor::new(zeros)?;
434 let momentum_q =
435 QuantizationUtils::quantize_nf4(&zero_tensor, self.config.block_size)?;
436 let variance_q =
437 QuantizationUtils::quantize_nf4(&zero_tensor, self.config.block_size)?;
438
439 self.momentum_quantized.insert(param_id.clone(), momentum_q);
440 self.variance_quantized.insert(param_id.clone(), variance_q);
441 }
442
443 let momentum_q = self.momentum_quantized.get(¶m_id).ok_or_else(|| {
445 TrustformersError::invalid_state(
446 "momentum_quantized should exist after insert".to_string(),
447 )
448 })?;
449 let variance_q = self.variance_quantized.get(¶m_id).ok_or_else(|| {
450 TrustformersError::invalid_state(
451 "variance_quantized should exist after insert".to_string(),
452 )
453 })?;
454
455 let momentum_tensor = QuantizationUtils::dequantize_nf4(momentum_q)?;
457 let variance_tensor = QuantizationUtils::dequantize_nf4(variance_q)?;
458
459 let momentum_data = momentum_tensor.data()?;
460 let variance_data = variance_tensor.data()?;
461
462 let mut new_momentum = Vec::with_capacity(size);
463 let mut new_variance = Vec::with_capacity(size);
464
465 let step = (self.state.step + 1) as f32;
466 let bias_correction1 = 1.0 - self.optimizer_config.beta1.powf(step);
467 let bias_correction2 = 1.0 - self.optimizer_config.beta2.powf(step);
468
469 for i in 0..size {
471 let mut g = grad_arr[i];
472
473 if self.optimizer_config.weight_decay > 0.0
477 && !self.optimizer_config.decoupled_weight_decay
478 {
479 g += self.optimizer_config.weight_decay * param[i];
480 }
481
482 let m = self.optimizer_config.beta1 * momentum_data[i]
484 + (1.0 - self.optimizer_config.beta1) * g;
485 let v = self.optimizer_config.beta2 * variance_data[i]
486 + (1.0 - self.optimizer_config.beta2) * g * g;
487
488 new_momentum.push(m);
489 new_variance.push(v);
490
491 let m_hat = m / bias_correction1;
493 let v_hat = v / bias_correction2;
494
495 param[i] -= self.optimizer_config.learning_rate * m_hat
497 / (v_hat.sqrt() + self.optimizer_config.epsilon);
498
499 if self.optimizer_config.weight_decay > 0.0
502 && self.optimizer_config.decoupled_weight_decay
503 {
504 param[i] -= self.optimizer_config.learning_rate
505 * self.optimizer_config.weight_decay
506 * param[i];
507 }
508 }
509
510 let new_momentum_tensor = Tensor::new(new_momentum)?;
512 let new_variance_tensor = Tensor::new(new_variance)?;
513
514 let momentum_q_new =
515 QuantizationUtils::quantize_nf4(&new_momentum_tensor, self.config.block_size)?;
516 let variance_q_new =
517 QuantizationUtils::quantize_nf4(&new_variance_tensor, self.config.block_size)?;
518
519 self.momentum_quantized.insert(param_id.clone(), momentum_q_new);
520 self.variance_quantized.insert(param_id, variance_q_new);
521
522 Ok(())
523 },
524 _ => Err(TrustformersError::tensor_op_error(
525 "Unsupported tensor types for Adam4bit",
526 "Adam4bit::update",
527 )),
528 }
529 }
530
531 fn zero_grad(&mut self) {
532 }
534
535 fn step(&mut self) {
536 self.state.step();
537 }
538
539 fn get_lr(&self) -> f32 {
540 self.optimizer_config.learning_rate
541 }
542
543 fn set_lr(&mut self, lr: f32) {
544 self.optimizer_config.learning_rate = lr;
545 }
546}
547
548impl StatefulOptimizer for Adam4bit {
549 type Config = Adam4bitOptimizerConfig;
550 type State = OptimizerState;
551
552 fn config(&self) -> &Self::Config {
553 &self.optimizer_config
554 }
555
556 fn state(&self) -> &Self::State {
557 &self.state
558 }
559
560 fn state_mut(&mut self) -> &mut Self::State {
561 &mut self.state
562 }
563
564 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
565 let mut state_dict = HashMap::new();
566
567 state_dict.insert(
569 "learning_rate".to_string(),
570 Tensor::new(vec![self.optimizer_config.learning_rate])?,
571 );
572 state_dict.insert(
573 "beta1".to_string(),
574 Tensor::new(vec![self.optimizer_config.beta1])?,
575 );
576 state_dict.insert(
577 "beta2".to_string(),
578 Tensor::new(vec![self.optimizer_config.beta2])?,
579 );
580 state_dict.insert(
581 "epsilon".to_string(),
582 Tensor::new(vec![self.optimizer_config.epsilon])?,
583 );
584 state_dict.insert(
585 "weight_decay".to_string(),
586 Tensor::new(vec![self.optimizer_config.weight_decay])?,
587 );
588 state_dict.insert(
589 "step".to_string(),
590 Tensor::new(vec![self.state.step as f32])?,
591 );
592
593 for (param_id, momentum_q) in &self.momentum_quantized {
595 state_dict.insert(
596 format!("momentum_q_{}", param_id),
597 Tensor::new(momentum_q.data.clone())?,
598 );
599 }
600
601 for (param_id, variance_q) in &self.variance_quantized {
602 state_dict.insert(
603 format!("variance_q_{}", param_id),
604 Tensor::new(variance_q.data.clone())?,
605 );
606 }
607
608 Ok(state_dict)
609 }
610
611 fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
612 if let Some(lr_tensor) = state.get("learning_rate") {
614 if let Ok(lr_vec) = lr_tensor.data() {
615 if !lr_vec.is_empty() {
616 self.optimizer_config.learning_rate = lr_vec[0];
617 }
618 }
619 }
620 Ok(())
624 }
625
626 fn memory_usage(&self) -> StateMemoryStats {
627 let total_memory =
628 self.momentum_quantized.values().map(|q| q.memory_usage()).sum::<usize>()
629 + self.variance_quantized.values().map(|q| q.memory_usage()).sum::<usize>();
630
631 StateMemoryStats {
632 momentum_elements: self.momentum_quantized.values().map(|q| q.data.len()).sum(),
633 variance_elements: self.variance_quantized.values().map(|q| q.data.len()).sum(),
634 third_moment_elements: 0,
635 total_bytes: total_memory,
636 num_parameters: self.momentum_quantized.len(),
637 }
638 }
639
640 fn reset_state(&mut self) {
641 self.state.clear();
642 self.momentum_quantized.clear();
643 self.variance_quantized.clear();
644 self.gradient_stats.clear();
645 }
646
647 fn num_parameters(&self) -> usize {
648 self.momentum_quantized.values().map(|q| q.data.len()).sum()
649 }
650}
651
652#[derive(Debug)]
659pub struct AdamW4bit {
660 inner: Adam4bit,
661}
662
663impl AdamW4bit {
664 pub fn new(
666 learning_rate: f32,
667 beta1: f32,
668 beta2: f32,
669 epsilon: f32,
670 weight_decay: f32,
671 ) -> Self {
672 let mut inner = Adam4bit::new(learning_rate, beta1, beta2, epsilon, weight_decay);
673 inner.set_decoupled_weight_decay(true);
674 Self { inner }
675 }
676
677 pub fn with_quantization_config(
679 mut optimizer_config: Adam4bitOptimizerConfig,
680 quantization_config: AdvancedQuantizationConfig,
681 ) -> Self {
682 optimizer_config.decoupled_weight_decay = true;
683 Self {
684 inner: Adam4bit::with_quantization_config(optimizer_config, quantization_config),
685 }
686 }
687
688 pub fn memory_savings(&self) -> f32 {
690 self.inner.memory_savings()
691 }
692}
693
694impl Optimizer for AdamW4bit {
695 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
696 self.inner.update(parameter, grad)
697 }
698
699 fn zero_grad(&mut self) {
700 self.inner.zero_grad()
701 }
702
703 fn step(&mut self) {
704 self.inner.step()
705 }
706
707 fn get_lr(&self) -> f32 {
708 self.inner.get_lr()
709 }
710
711 fn set_lr(&mut self, lr: f32) {
712 self.inner.set_lr(lr)
713 }
714}
715
716impl StatefulOptimizer for AdamW4bit {
717 type Config = <Adam4bit as StatefulOptimizer>::Config;
718 type State = <Adam4bit as StatefulOptimizer>::State;
719
720 fn config(&self) -> &Self::Config {
721 self.inner.config()
722 }
723
724 fn state(&self) -> &Self::State {
725 self.inner.state()
726 }
727
728 fn state_mut(&mut self) -> &mut Self::State {
729 self.inner.state_mut()
730 }
731
732 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
733 self.inner.state_dict()
734 }
735
736 fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
737 self.inner.load_state_dict(state)
738 }
739
740 fn memory_usage(&self) -> StateMemoryStats {
741 self.inner.memory_usage()
742 }
743
744 fn reset_state(&mut self) {
745 self.inner.reset_state()
746 }
747
748 fn num_parameters(&self) -> usize {
749 self.inner.num_parameters()
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn test_nf4_quantization() {
759 let data = vec![1.0, -0.5, 0.0, 0.8, -1.2];
760 let tensor = Tensor::new(data.clone()).expect("Failed to create tensor");
761
762 let quantized =
763 QuantizationUtils::quantize_nf4(&tensor, 64).expect("Operation failed in test");
764 assert_eq!(quantized.method, QuantizationMethod::NF4);
765 assert!(quantized.compression_ratio() >= 1.0);
766 }
767
768 #[test]
769 fn test_gradient_statistics() {
770 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
771 let stats = GradientStatistics::compute(&data);
772
773 assert!((stats.mean - 3.0).abs() < 1e-6);
774 assert!(stats.variance > 0.0);
775 assert!(stats.l2_norm > 0.0);
776 }
777
778 #[test]
779 fn test_adam4bit_creation() {
780 let optimizer = Adam4bit::new(0.001, 0.9, 0.999, 1e-8, 0.01);
781 assert_eq!(optimizer.get_lr(), 0.001);
782 assert!(optimizer.memory_savings() > 0.5); }
784
785 #[test]
786 fn test_quantized_tensor_memory() {
787 let quantized = QuantizedTensor::new(
788 vec![0.0, 1.0, 2.0, 3.0],
789 vec![1.0],
790 vec![0.0],
791 vec![4],
792 QuantizationMethod::NF4,
793 64,
794 );
795
796 assert!(quantized.memory_usage() > 0);
797 assert!(quantized.compression_ratio() >= 1.0);
798 }
799}
800
801#[cfg(test)]
802mod adamw4bit_tests {
803 use super::*;
804
805 fn tensor(values: &[f32]) -> Tensor {
806 Tensor::from_vec(values.to_vec(), &[values.len()]).expect("tensor")
807 }
808
809 #[test]
812 fn adamw4bit_applies_decoupled_weight_decay() {
813 let mut optimizer = AdamW4bit::new(0.1, 0.9, 0.999, 1e-8, 0.5);
814 let mut param = tensor(&[10.0]);
815 optimizer.update(&mut param, &tensor(&[0.0])).expect("update");
816
817 let after = param.data_f32().expect("data")[0];
818 assert!((after - 9.5).abs() < 1e-4, "expected 9.5, got {after}");
820 }
821
822 #[test]
825 fn adam4bit_and_adamw4bit_differ_on_weight_decay() {
826 let mut coupled = Adam4bit::new(0.1, 0.9, 0.999, 1e-8, 0.5);
827 let mut decoupled = AdamW4bit::new(0.1, 0.9, 0.999, 1e-8, 0.5);
828
829 let mut a = tensor(&[10.0]);
830 let mut b = tensor(&[10.0]);
831 coupled.update(&mut a, &tensor(&[0.0])).expect("coupled");
832 decoupled.update(&mut b, &tensor(&[0.0])).expect("decoupled");
833
834 let coupled_value = a.data_f32().expect("data")[0];
835 let decoupled_value = b.data_f32().expect("data")[0];
836 assert!(
837 (coupled_value - decoupled_value).abs() > 1e-3,
838 "the two decay styles must differ: {coupled_value} vs {decoupled_value}"
839 );
840 }
841
842 #[test]
844 fn adamw4bit_descends_a_quadratic_bowl() {
845 let mut optimizer = AdamW4bit::new(0.05, 0.9, 0.999, 1e-8, 0.0);
846 let mut param = tensor(&[3.0, -4.0]);
847 let initial: f32 = param.data_f32().expect("data").iter().map(|v| v * v).sum();
848
849 for _ in 0..400 {
850 let values = param.data_f32().expect("data");
851 let grad = tensor(&values.iter().map(|v| 2.0 * v).collect::<Vec<f32>>());
852 optimizer.update(&mut param, &grad).expect("step");
853 optimizer.step();
854 }
855
856 let final_loss: f32 = param.data_f32().expect("data").iter().map(|v| v * v).sum();
857 assert!(
858 final_loss < initial * 0.2,
859 "loss must fall: {initial} -> {final_loss}"
860 );
861 }
862}
863
864#[cfg(test)]
865mod nf4_round_trip_tests {
866 use super::*;
867
868 #[test]
872 fn constant_block_round_trips_without_nan() {
873 for value in [0.0_f32, 1.5, -2.25] {
874 let tensor = Tensor::from_vec(vec![value; 8], &[8]).expect("tensor");
875 let quantized = QuantizationUtils::quantize_nf4(&tensor, 4).expect("quantize");
876 let restored = QuantizationUtils::dequantize_nf4(&quantized).expect("dequantize");
877
878 for restored_value in restored.data_f32().expect("data") {
879 assert!(
880 restored_value.is_finite(),
881 "NaN for a constant block of {value}"
882 );
883 assert!(
884 (restored_value - value).abs() < 1e-6,
885 "a constant block must round-trip exactly: {restored_value} vs {value}"
886 );
887 }
888 }
889 }
890
891 #[test]
894 fn shifted_block_round_trips_close() {
895 let values: Vec<f32> = (0..16).map(|i| 10.0 + i as f32 * 0.5).collect();
896 let tensor = Tensor::from_vec(values.clone(), &[16]).expect("tensor");
897 let quantized = QuantizationUtils::quantize_nf4(&tensor, 16).expect("quantize");
898 let restored = QuantizationUtils::dequantize_nf4(&quantized).expect("dequantize");
899
900 let span = 7.5_f32; for (restored_value, original) in
902 restored.data_f32().expect("data").iter().zip(values.iter())
903 {
904 assert!(
905 (restored_value - original).abs() < span * 0.2,
906 "round trip drifted: {restored_value} vs {original}"
907 );
908 }
909
910 let restored_values = restored.data_f32().expect("data");
912 assert!((restored_values[0] - 10.0).abs() < 1e-4);
913 assert!((restored_values[15] - 17.5).abs() < 1e-4);
914 }
915}