1use alloc::format;
15use alloc::string::{String, ToString};
16
17use burn_pack::{Error as PackError, Tensor as PackTensor};
18
19use burn_core::tensor::kind::Basic;
20use burn_core::tensor::quantization::quantized_data_len;
21use burn_core::tensor::{Bytes, DType, Shape, Tensor, TensorData};
22
23pub fn data_len(dtype: DType, shape: &Shape) -> usize {
30 match dtype {
31 DType::QFloat(scheme) => quantized_data_len(&scheme, shape),
32 _ => shape.iter().product::<usize>() * dtype.size(),
33 }
34}
35
36#[cfg(feature = "std")]
47fn guarded(f: impl Fn() -> Result<TensorData, PackError>) -> Result<TensorData, PackError> {
48 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|payload| {
51 let cause = payload
55 .downcast_ref::<&str>()
56 .map(|s| (*s).to_string())
57 .or_else(|| payload.downcast_ref::<String>().cloned())
58 .unwrap_or_else(|| "unknown panic payload".to_string());
59
60 Err(PackError::ValidationError(format!(
61 "panic while producing tensor data: {cause}"
62 )))
63 })
64}
65
66#[cfg(not(feature = "std"))]
70fn guarded(f: impl Fn() -> Result<TensorData, PackError>) -> Result<TensorData, PackError> {
71 f()
72}
73
74#[cfg(target_has_atomic = "ptr")]
86pub trait MaybeSendSync: Send + Sync {}
87#[cfg(target_has_atomic = "ptr")]
88impl<T: Send + Sync> MaybeSendSync for T {}
89
90#[cfg(not(target_has_atomic = "ptr"))]
93pub trait MaybeSendSync {}
94#[cfg(not(target_has_atomic = "ptr"))]
95impl<T> MaybeSendSync for T {}
96
97pub fn deferred(
105 name: String,
106 dtype: DType,
107 shape: Shape,
108 param_id: Option<u64>,
109 data_fn: impl Fn() -> Result<TensorData, PackError> + MaybeSendSync + 'static,
110) -> PackTensor {
111 let byte_len = data_len(dtype, &shape);
112 let declared = shape.clone();
113
114 PackTensor::deferred(name, dtype, shape, param_id, byte_len, move || {
115 let data = guarded(&data_fn)?;
116
117 if data.dtype() != dtype || *data.shape() != declared {
121 return Err(PackError::ValidationError(format!(
122 "provider produced {:?} {:?}, but the tensor declared {:?} {:?}",
123 data.dtype(),
124 data.shape(),
125 dtype,
126 declared
127 )));
128 }
129
130 Ok(data.into_bytes())
131 })
132}
133
134pub fn from_tensor<const D: usize, K: Basic + 'static>(
140 tensor: &Tensor<D, K>,
141 name: String,
142 param_id: Option<u64>,
143) -> PackTensor {
144 let (dtype, shape) = (tensor.dtype(), tensor.shape());
145 let tensor = tensor.clone();
146
147 deferred(name, dtype, shape, param_id, move || Ok(tensor.to_data()))
148}
149
150pub fn from_data(data: TensorData, name: String, param_id: Option<u64>) -> PackTensor {
152 let (bytes, shape, dtype) = data.into_parts();
153 PackTensor::new(name, dtype, shape, param_id, bytes)
154}
155
156pub fn to_data(tensor: &PackTensor) -> Result<TensorData, PackError> {
162 tensor_data(tensor.to_bytes()?, tensor.shape.clone(), tensor.dtype)
163}
164
165pub fn into_data(tensor: PackTensor) -> Result<TensorData, PackError> {
167 let (_, dtype, shape, _, bytes) = tensor.into_parts()?;
168
169 tensor_data(bytes, shape, dtype)
170}
171
172pub(crate) fn tensor_data(
175 bytes: Bytes,
176 shape: Shape,
177 dtype: DType,
178) -> Result<TensorData, PackError> {
179 TensorData::try_from_bytes(bytes, shape, dtype)
180 .map_err(|err| PackError::TensorBytesSizeMismatch(err.to_string()))
181}
182
183pub fn map_data(
193 tensor: PackTensor,
194 name: String,
195 dtype: DType,
196 shape: Shape,
197 f: impl Fn(TensorData) -> TensorData + MaybeSendSync + 'static,
198) -> PackTensor {
199 let param_id = tensor.param_id;
200
201 deferred(name, dtype, shape, param_id, move || {
202 to_data(&tensor).map(&f)
203 })
204}
205
206#[cfg(feature = "pytorch")]
211pub fn from_pytorch(tensor: pytorch_reader::Tensor) -> PackTensor {
212 use std::io::ErrorKind;
213
214 let (name, dtype, shape, read) = tensor.into_parts();
215 let dtype = pytorch_dtype(dtype);
216 let shape = Shape::from(shape);
217 let declared = shape.clone();
218
219 deferred(name, dtype, shape, None, move || {
220 let bytes = read().map_err(|err| match err.kind() {
223 ErrorKind::InvalidData | ErrorKind::UnexpectedEof => {
224 PackError::ValidationError(err.to_string())
225 }
226 _ => PackError::IoError(err.to_string()),
227 })?;
228 tensor_data(Bytes::from_bytes_vec(bytes), declared.clone(), dtype)
229 })
230}
231
232#[cfg(feature = "pytorch")]
233fn pytorch_dtype(dtype: pytorch_reader::DType) -> DType {
234 use burn_core::tensor::BoolStore;
235 use pytorch_reader::DType as Pt;
236
237 match dtype {
238 Pt::F64 => DType::F64,
239 Pt::F32 => DType::F32,
240 Pt::F16 => DType::F16,
241 Pt::BF16 => DType::BF16,
242 Pt::I64 => DType::I64,
243 Pt::I32 => DType::I32,
244 Pt::I16 => DType::I16,
245 Pt::I8 => DType::I8,
246 Pt::U64 => DType::U64,
247 Pt::U32 => DType::U32,
248 Pt::U16 => DType::U16,
249 Pt::U8 => DType::U8,
250 Pt::Bool => DType::Bool(BoolStore::Native),
251 }
252}
253
254#[cfg(all(test, feature = "std"))]
255mod tests {
256 use super::*;
257 use alloc::format;
258 use alloc::string::ToString;
259 use alloc::vec;
260 use burn_core::tensor::quantization::{QuantScheme, QuantStore, QuantValue, ScaleDtype};
261 use burn_core::tensor::{Bool, Device, Distribution, Int, shape};
262
263 fn assert_byte_len_matches(tensor: PackTensor) {
270 let (declared, name) = (tensor.byte_len(), tensor.name.clone());
271
272 match to_data(&tensor) {
273 Ok(data) => assert_eq!(
274 declared,
275 data.bytes().len(),
276 "byte_len disagrees with the materialized bytes for {name}"
277 ),
278 Err(e) => panic!("byte_len disagrees with the materialized bytes for {name}: {e}"),
279 }
280 }
281
282 #[test]
283 fn data_len_covers_every_dtype_family() {
284 let device = Device::default();
285
286 let floats = Tensor::<2>::from_data([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], &device);
287 let ints = Tensor::<2, Int>::from_data([[1, 2], [3, 4]], &device);
288 let bools = Tensor::<2, Bool>::from_data([[true, false], [false, true]], &device);
289
290 assert_byte_len_matches(from_tensor(&floats, "float".to_string(), None));
291 assert_byte_len_matches(from_tensor(&ints, "int".to_string(), None));
292 assert_byte_len_matches(from_tensor(&bools, "bool".to_string(), None));
293 }
294
295 #[test]
309 fn data_len_matches_materialized_bytes_when_quantized() {
310 let device = Device::default();
311
312 let schemes = [
313 ("q8", QuantScheme::default()),
314 ("q4", QuantScheme::default().with_value(QuantValue::Q4S)),
315 ("q2", QuantScheme::default().with_value(QuantValue::Q2S)),
316 ];
317 let shapes = [shape![32, 32], shape![3, 3], shape![5, 5], shape![2, 3]];
319
320 for (name, scheme) in schemes {
321 for shape in &shapes {
322 let tensor = Tensor::<2>::random(shape.clone(), Distribution::Default, &device)
323 .quantize_dynamic(&scheme);
324 assert_byte_len_matches(from_tensor(&tensor, format!("{name}-{shape:?}"), None));
325 }
326 }
327 }
328
329 #[test]
333 fn data_len_matches_quantized_bytes() {
334 let base = QuantScheme::default()
335 .with_value(QuantValue::Q8S)
336 .with_store(QuantStore::Native);
337
338 for (values, block) in [(8usize, 4usize), (6, 3), (10, 5)] {
341 let scales = vec![0.5f32; values / block];
342 let one_level = base.per_block([block as u8], ScaleDtype::UE4M3);
343 let two_level = base
344 .per_block([block as u8], ScaleDtype::UE4M3)
345 .per_tensor(ScaleDtype::F32);
346
347 for (scheme, global) in [(one_level, None), (two_level, Some(3.0f32))] {
348 let data =
349 TensorData::quantized(vec![0i8; values], [values], scheme, &scales, global);
350
351 assert_eq!(
352 data_len(data.dtype(), data.shape()),
353 data.bytes().len(),
354 "predicted size disagrees with the written bytes for {values} values \
355 in blocks of {block}, {scheme:?}"
356 );
357 }
358 }
359 }
360
361 #[test]
366 fn data_len_packs_per_line_for_packed_stores() {
367 let scheme = QuantScheme::default().with_value(QuantValue::Q4S);
369 let packed = |shape: Shape| data_len(DType::QFloat(scheme), &shape);
370
371 assert_eq!(packed(shape![3, 3]), 12 + 4);
374
375 assert_eq!(packed(shape![2, 5, 9]), 20 * 4 + 4);
377
378 assert_eq!(packed(shape![4, 8]), 4 * 4 + 4);
380 }
381
382 #[test]
386 fn a_panicking_provider_becomes_an_error() {
387 let tensor = deferred("weight".to_string(), DType::F32, shape![2, 2], None, || {
388 panic!("device readback panicked")
389 });
390
391 let err = to_data(&tensor).expect_err("a panicking provider must not unwind");
392 assert!(
393 matches!(&err, PackError::ValidationError(m) if m.contains("panic")),
394 "expected a validation error naming the panic, got {err:?}"
395 );
396 }
397
398 #[test]
403 fn a_write_time_failure_keeps_its_class_and_names_its_tensor() {
404 let tensor = deferred(
405 "encoder.weight".to_string(),
406 DType::F32,
407 shape![1],
408 None,
409 || panic!("device readback panicked"),
410 );
411
412 let err = burn_pack::Writer::new(vec![tensor])
413 .into_bytes()
414 .expect_err("a panicking provider must fail the write");
415
416 assert!(
417 matches!(&err, PackError::ValidationError(m)
418 if m.contains("tensor 'encoder.weight'") && m.contains("device readback panicked")),
419 "expected a named ValidationError carrying the panic message, got {err:?}"
420 );
421 }
422
423 #[test]
425 fn a_provider_error_passes_through() {
426 let tensor = deferred("weight".to_string(), DType::F32, shape![2, 2], None, || {
427 Err(PackError::IoError("simulated IO error".to_string()))
428 });
429
430 let err = to_data(&tensor).unwrap_err();
431 assert!(
432 matches!(&err, PackError::IoError(m) if m == "simulated IO error"),
433 "expected the provider's own error, got {err:?}"
434 );
435 }
436
437 #[test]
441 fn deferred_runs_its_provider_only_when_asked() {
442 use core::sync::atomic::{AtomicUsize, Ordering};
443 use alloc::sync::Arc;
445
446 let calls = Arc::new(AtomicUsize::new(0));
447 let counter = calls.clone();
448 let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
449
450 let tensor = deferred(
451 "weight".to_string(),
452 DType::F32,
453 shape![4],
454 None,
455 move || {
456 counter.fetch_add(1, Ordering::Relaxed);
457 Ok(data.clone())
458 },
459 );
460
461 assert_eq!(tensor.byte_len(), 16);
463 assert_eq!(tensor.shape, shape![4]);
464 assert_eq!(calls.load(Ordering::Relaxed), 0);
465
466 to_data(&tensor).unwrap();
467 assert_eq!(calls.load(Ordering::Relaxed), 1);
468 }
469
470 #[test]
473 fn from_tensor_covers_every_kind() {
474 let device = Device::default();
475
476 let floats = from_tensor(
477 &Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device),
478 "float".to_string(),
479 Some(7),
480 );
481 assert_eq!(floats.name, "float");
482 assert_eq!(floats.shape, shape![2, 2]);
483 assert_eq!(floats.param_id, Some(7));
484 assert_eq!(*to_data(&floats).unwrap().shape(), shape![2, 2]);
485
486 let ints = from_tensor(
487 &Tensor::<2, Int>::from_data([[1, 2], [3, 4]], &device),
488 "int".to_string(),
489 None,
490 );
491 assert_eq!(ints.dtype, device.settings().int_dtype.into());
492
493 let bools = from_tensor(
494 &Tensor::<2, Bool>::from_data([[true, false], [false, true]], &device),
495 "bool".to_string(),
496 None,
497 );
498 assert_eq!(*to_data(&bools).unwrap().shape(), shape![2, 2]);
499 }
500
501 #[test]
505 fn a_provider_contradicting_its_declared_dtype_is_refused() {
506 let tensor = deferred("weight".to_string(), DType::F32, shape![4], None, || {
507 Ok(TensorData::from([1i32, 2, 3, 4]))
508 });
509
510 let err = to_data(&tensor).unwrap_err();
511 assert!(
512 matches!(&err, PackError::ValidationError(m) if m.contains("I32") && m.contains("F32")),
513 "expected a validation error naming both dtypes, got {err:?}"
514 );
515 }
516
517 #[test]
519 fn a_provider_contradicting_its_declared_shape_is_refused() {
520 let tensor = deferred("weight".to_string(), DType::F32, shape![2, 3], None, || {
521 Ok(TensorData::new(vec![0.0f32; 6], shape![3, 2]))
522 });
523
524 assert!(matches!(
525 to_data(&tensor).unwrap_err(),
526 PackError::ValidationError(_)
527 ));
528 }
529
530 #[test]
532 fn into_data_matches_to_data() {
533 let device = Device::default();
534 let tensor = from_tensor(
535 &Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device),
536 "weight".to_string(),
537 None,
538 );
539
540 let borrowed = to_data(&tensor).unwrap();
541 let taken = into_data(tensor).unwrap();
542 assert_eq!(borrowed.shape(), taken.shape());
543 assert_eq!(borrowed.dtype(), taken.dtype());
544 assert_eq!(borrowed.as_bytes(), taken.as_bytes());
545 }
546
547 #[test]
551 fn map_data_declares_the_transformed_length() {
552 let device = Device::default();
553 let source = from_tensor(
554 &Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device),
555 "weight".to_string(),
556 None,
557 );
558 assert_eq!(source.byte_len(), 16);
559
560 let shape = source.shape.clone();
561 let cast = map_data(source, "weight".to_string(), DType::F16, shape, |data| {
562 data.convert_dtype(DType::F16)
563 });
564
565 assert_eq!(cast.byte_len(), 8);
566 assert_byte_len_matches(cast);
567 }
568
569 #[cfg(feature = "pytorch")]
573 #[test]
574 fn from_pytorch_maps_dtype_and_reads_bytes() {
575 use burn_core::tensor::BoolStore;
576
577 let path = concat!(
578 env!("CARGO_MANIFEST_DIR"),
579 "/pytorch-tests/tests/boolean/boolean.pt"
580 );
581 let mut tensors = pytorch_reader::PytorchReader::new(path)
582 .unwrap()
583 .into_tensors();
584 let tensor = from_pytorch(tensors.remove("buffer").unwrap());
585
586 assert_eq!(tensor.name, "buffer");
587 assert_eq!(tensor.dtype, DType::Bool(BoolStore::Native));
588 assert_eq!(tensor.shape, shape![3]);
589 assert_byte_len_matches(tensor.clone());
590
591 let data = into_data(tensor).unwrap();
592 assert_eq!(data.as_slice::<bool>().unwrap(), &[true, false, true]);
593 }
594
595 #[cfg(feature = "pytorch")]
599 #[test]
600 fn pytorch_dtype_maps_every_variant_by_name() {
601 use pytorch_reader::DType as Pt;
602
603 let all = [
604 Pt::F64,
605 Pt::F32,
606 Pt::F16,
607 Pt::BF16,
608 Pt::I64,
609 Pt::I32,
610 Pt::I16,
611 Pt::I8,
612 Pt::U64,
613 Pt::U32,
614 Pt::U16,
615 Pt::U8,
616 Pt::Bool,
617 ];
618 for pt in all {
619 let mapped = pytorch_dtype(pt);
620 assert_eq!(mapped.size(), pt.size(), "{pt:?}");
621 let expected = match pt {
622 Pt::Bool => "Bool(Native)".to_string(),
623 other => format!("{other:?}"),
624 };
625 assert_eq!(format!("{mapped:?}"), expected);
626 }
627 }
628}