1use crate::errors::{QuantizeError, Result};
9#[cfg(feature = "calibration")]
10use std::path::Path;
11
12#[cfg(feature = "calibration")]
13pub mod inference;
14pub mod methods;
15pub mod stats;
16
17#[cfg(feature = "calibration")]
18pub use inference::ActivationEstimator;
19
20#[derive(Clone)]
22pub struct CalibrationDataset {
23 pub samples: Vec<Vec<f32>>,
25
26 pub shape: Vec<usize>,
28}
29
30impl std::fmt::Debug for CalibrationDataset {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("CalibrationDataset")
33 .field("num_samples", &self.samples.len())
34 .field("shape", &self.shape)
35 .finish()
36 }
37}
38
39impl CalibrationDataset {
40 #[cfg(feature = "calibration")]
51 pub fn from_numpy(path: impl AsRef<Path>) -> Result<Self> {
52 use ndarray::{Array, IxDyn};
53
54 let path = path.as_ref();
55
56 if !path.exists() {
57 return Err(QuantizeError::Calibration {
58 reason: format!("File not found: {}", path.display()),
59 });
60 }
61
62 let array: Array<f32, IxDyn> = if path.extension().and_then(|s| s.to_str()) == Some("npy") {
63 ndarray_npy::read_npy(path).map_err(|e| QuantizeError::Calibration {
64 reason: format!("Failed to read NPY file '{}': {e}", path.display()),
65 })?
66 } else {
67 return Err(QuantizeError::Calibration {
68 reason: "Only .npy files supported currently".into(),
69 });
70 };
71
72 let shape: Vec<usize> = array.shape().to_vec();
73
74 if shape.is_empty() {
75 return Err(QuantizeError::Calibration {
76 reason: "Invalid array shape".into(),
77 });
78 }
79
80 if shape.len() < 2 {
81 return Err(QuantizeError::Calibration {
82 reason: format!(
83 "Calibration data must be at least 2-dimensional (batch, ...). Got shape {:?}",
84 shape
85 ),
86 });
87 }
88
89 let num_samples = shape[0];
90 let sample_size: usize = shape[1..].iter().product();
91
92 let data = if array.is_standard_layout() {
97 array.into_raw_vec()
98 } else {
99 array.as_standard_layout().into_owned().into_raw_vec()
100 };
101 let mut samples = Vec::with_capacity(num_samples);
102
103 for i in 0..num_samples {
104 let start = i * sample_size;
105 let end = start + sample_size;
106 samples.push(data[start..end].to_vec());
107 }
108
109 Ok(Self {
110 samples,
111 shape: shape[1..].to_vec(),
112 })
113 }
114
115 #[cfg(feature = "safetensors-input")]
124 pub fn from_safetensors(path: impl AsRef<Path>) -> Result<Self> {
125 let path = path.as_ref();
126 let buffer = std::fs::read(path).map_err(|e| QuantizeError::Calibration {
127 reason: format!("Failed to read safetensors file '{}': {e}", path.display()),
128 })?;
129 let tensors = safetensors::SafeTensors::deserialize(&buffer).map_err(|e| {
130 QuantizeError::Calibration {
131 reason: format!("Failed to parse safetensors file: {e}"),
132 }
133 })?;
134 let names: Vec<String> = tensors.names().into_iter().map(|s| s.to_string()).collect();
135 if names.is_empty() {
136 return Err(QuantizeError::Calibration {
137 reason: "safetensors file contains no tensors".into(),
138 });
139 }
140 if names.len() > 1 {
141 return Err(QuantizeError::Calibration {
142 reason: format!(
143 "safetensors file contains {} tensors; pass one explicitly via \
144 from_safetensors_named(). Available tensors: {}",
145 names.len(),
146 names.join(", ")
147 ),
148 });
149 }
150 Self::from_safetensors_view(&tensors, &names[0])
151 }
152
153 #[cfg(feature = "safetensors-input")]
158 pub fn from_safetensors_named(path: impl AsRef<Path>, tensor_name: &str) -> Result<Self> {
159 let path = path.as_ref();
160 let buffer = std::fs::read(path).map_err(|e| QuantizeError::Calibration {
161 reason: format!("Failed to read safetensors file '{}': {e}", path.display()),
162 })?;
163 let tensors = safetensors::SafeTensors::deserialize(&buffer).map_err(|e| {
164 QuantizeError::Calibration {
165 reason: format!("Failed to parse safetensors file: {e}"),
166 }
167 })?;
168 Self::from_safetensors_view(&tensors, tensor_name)
169 }
170
171 #[cfg(feature = "safetensors-input")]
172 fn from_safetensors_view(
173 tensors: &safetensors::SafeTensors<'_>,
174 tensor_name: &str,
175 ) -> Result<Self> {
176 use safetensors::Dtype;
177
178 let view = tensors
179 .tensor(tensor_name)
180 .map_err(|e| QuantizeError::Calibration {
181 reason: format!(
182 "Tensor '{}' not found in safetensors file: {e}",
183 tensor_name
184 ),
185 })?;
186
187 if view.dtype() != Dtype::F32 {
188 return Err(QuantizeError::Calibration {
189 reason: format!(
190 "Tensor '{}' has dtype {:?}; only F32 is supported for calibration input",
191 tensor_name,
192 view.dtype()
193 ),
194 });
195 }
196
197 let shape: Vec<usize> = view.shape().to_vec();
198 if shape.len() < 2 {
199 return Err(QuantizeError::Calibration {
200 reason: format!(
201 "Calibration tensor must be at least 2-dimensional (batch, ...). \
202 Got shape {:?}",
203 shape
204 ),
205 });
206 }
207 let expected_bytes: usize = shape.iter().product::<usize>() * std::mem::size_of::<f32>();
208 let raw = view.data();
209 if raw.len() != expected_bytes {
210 return Err(QuantizeError::Calibration {
211 reason: format!(
212 "Tensor '{}' data size {} bytes does not match shape {:?} \
213 × 4 = {} bytes",
214 tensor_name,
215 raw.len(),
216 shape,
217 expected_bytes
218 ),
219 });
220 }
221
222 let data: Vec<f32> = raw
226 .chunks_exact(4)
227 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
228 .collect();
229
230 let num_samples = shape[0];
231 let sample_size: usize = shape[1..].iter().product();
232 let mut samples = Vec::with_capacity(num_samples);
233 for i in 0..num_samples {
234 let start = i * sample_size;
235 let end = start + sample_size;
236 samples.push(data[start..end].to_vec());
237 }
238
239 Ok(Self {
240 samples,
241 shape: shape[1..].to_vec(),
242 })
243 }
244
245 pub fn random(shape: Vec<usize>, num_samples: usize, range: (f32, f32)) -> Result<Self> {
252 if shape.is_empty() || shape.contains(&0) {
253 return Err(QuantizeError::Calibration {
254 reason: format!("Invalid shape: {:?} - all dimensions must be > 0", shape),
255 });
256 }
257 if num_samples == 0 {
258 return Err(QuantizeError::Calibration {
259 reason: "num_samples must be > 0".into(),
260 });
261 }
262 if range.0 >= range.1 {
263 return Err(QuantizeError::Calibration {
264 reason: format!(
265 "Invalid range: ({}, {}) - min must be less than max",
266 range.0, range.1
267 ),
268 });
269 }
270 use rand::Rng;
271 let mut rng = rand::thread_rng();
272
273 let sample_size: usize = shape.iter().product();
274 let mut samples = Vec::with_capacity(num_samples);
275
276 for _ in 0..num_samples {
277 let sample: Vec<f32> = (0..sample_size)
278 .map(|_| rng.gen_range(range.0..range.1))
279 .collect();
280 samples.push(sample);
281 }
282
283 Ok(Self { samples, shape })
284 }
285
286 pub fn from_samples(samples: Vec<Vec<f32>>, shape: Vec<usize>) -> Result<Self> {
293 let num_samples = samples.len();
294
295 if num_samples == 0 {
296 return Err(QuantizeError::Calibration {
297 reason: "No samples provided".into(),
298 });
299 }
300
301 let expected_size: usize = shape.iter().product();
302
303 for (i, sample) in samples.iter().enumerate() {
304 if sample.len() != expected_size {
305 return Err(QuantizeError::Calibration {
306 reason: format!(
307 "Sample {} has size {} but expected {} (shape: {:?})",
308 i,
309 sample.len(),
310 expected_size,
311 shape
312 ),
313 });
314 }
315 }
316
317 Ok(Self { samples, shape })
318 }
319
320 pub fn sample_shape(&self) -> &[usize] {
322 &self.shape
323 }
324
325 pub fn len(&self) -> usize {
327 self.samples.len()
328 }
329
330 pub fn is_empty(&self) -> bool {
332 self.samples.is_empty()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_random_dataset() {
342 let dataset = CalibrationDataset::random(vec![3, 224, 224], 10, (-1.0, 1.0)).unwrap();
343
344 assert_eq!(dataset.len(), 10);
345 assert_eq!(dataset.sample_shape(), &[3, 224, 224]);
346 assert_eq!(dataset.samples[0].len(), 3 * 224 * 224);
347 }
348
349 #[test]
350 fn test_from_samples() {
351 let samples = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
352
353 let dataset = CalibrationDataset::from_samples(samples, vec![3]).unwrap();
354 assert_eq!(dataset.len(), 2);
355 }
356
357 #[cfg(feature = "calibration")]
358 #[test]
359 fn test_from_numpy_fortran_order_slices_by_logical_samples() {
360 use ndarray::{Array2, ShapeBuilder};
361
362 let f_memory: Vec<f32> = vec![
370 10., 20., 30., 11., 21., 31., 12., 22., 32., 13., 23., 33., ];
375 let arr = Array2::from_shape_vec((3, 4).f(), f_memory).unwrap();
376 assert!(
377 !arr.is_standard_layout(),
378 "test setup: array should be Fortran-ordered"
379 );
380
381 let tmp = tempfile::NamedTempFile::with_suffix(".npy").unwrap();
382 ndarray_npy::write_npy(tmp.path(), &arr).unwrap();
383
384 let dataset = CalibrationDataset::from_numpy(tmp.path()).unwrap();
385 assert_eq!(dataset.len(), 3);
386 assert_eq!(dataset.sample_shape(), &[4]);
387 assert_eq!(dataset.samples[0], vec![10., 11., 12., 13.]);
388 assert_eq!(dataset.samples[1], vec![20., 21., 22., 23.]);
389 assert_eq!(dataset.samples[2], vec![30., 31., 32., 33.]);
390 }
391
392 #[cfg(feature = "safetensors-input")]
393 #[test]
394 fn test_from_safetensors_round_trip() {
395 use safetensors::{serialize, tensor::TensorView, Dtype};
396 use std::collections::HashMap;
397
398 let data: Vec<f32> = (0..24).map(|i| i as f32 * 0.1).collect();
400 let raw: Vec<u8> = data.iter().flat_map(|&f| f.to_le_bytes()).collect();
401 let view = TensorView::new(Dtype::F32, vec![3, 2, 4], &raw).unwrap();
402 let mut tensors = HashMap::new();
403 tensors.insert("input".to_string(), view);
404 let bytes = serialize(&tensors, &None).unwrap();
405
406 let tmp = tempfile::NamedTempFile::with_suffix(".safetensors").unwrap();
407 std::fs::write(tmp.path(), &bytes).unwrap();
408
409 let dataset = CalibrationDataset::from_safetensors(tmp.path()).unwrap();
410 assert_eq!(dataset.len(), 3);
411 assert_eq!(dataset.sample_shape(), &[2, 4]);
412 assert_eq!(dataset.samples[0].len(), 8);
414 assert!((dataset.samples[0][0] - 0.0).abs() < 1e-6);
416 assert!((dataset.samples[1][0] - 0.8).abs() < 1e-6);
417 }
418
419 #[cfg(feature = "safetensors-input")]
420 #[test]
421 fn test_from_safetensors_multi_tensor_errors_without_name() {
422 use safetensors::{serialize, tensor::TensorView, Dtype};
423 use std::collections::HashMap;
424
425 let data: Vec<f32> = (0..8).map(|i| i as f32).collect();
426 let raw: Vec<u8> = data.iter().flat_map(|&f| f.to_le_bytes()).collect();
427 let v1 = TensorView::new(Dtype::F32, vec![2, 4], &raw).unwrap();
428 let v2 = TensorView::new(Dtype::F32, vec![2, 4], &raw).unwrap();
429 let mut tensors = HashMap::new();
430 tensors.insert("a".to_string(), v1);
431 tensors.insert("b".to_string(), v2);
432 let bytes = serialize(&tensors, &None).unwrap();
433
434 let tmp = tempfile::NamedTempFile::with_suffix(".safetensors").unwrap();
435 std::fs::write(tmp.path(), &bytes).unwrap();
436
437 let err = CalibrationDataset::from_safetensors(tmp.path()).unwrap_err();
438 assert!(err.to_string().contains("contains 2 tensors"));
439
440 let dataset = CalibrationDataset::from_safetensors_named(tmp.path(), "a").unwrap();
442 assert_eq!(dataset.len(), 2);
443 assert_eq!(dataset.sample_shape(), &[4]);
444 }
445}