1use crate::nested::{adapter::DefaultAdapter, data::NestedValue, de::Deserializer};
6use crate::{DType, Tensor};
7use serde::de::DeserializeOwned;
8use std::collections::HashMap;
9use std::fs::File;
10use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
11use std::path::Path;
12use std::sync::Arc;
13
14use crate::pickle_reader::{
15 Object, PersistentIds, PickleError, StorageRef, build_tensor, extract_tensors, key_string,
16 non_negative, read_pickle, storage_type_to_dtype,
17};
18use crate::storage::{LegacySource, StorageSource, TarSource, ZipSource};
19use byteorder::{LittleEndian, ReadBytesExt};
20use thiserror::Error;
21
22#[derive(Debug, Error)]
24#[non_exhaustive]
25pub enum PytorchError {
26 #[error("IO error: {0}")]
28 Io(#[from] std::io::Error),
29 #[error("pickle: {0}")]
31 Pickle(#[from] PickleError),
32 #[error("zip archive error: {0}")]
34 Zip(#[from] zip::result::ZipError),
35 #[error("TAR archive error: {0}")]
37 Tar(#[source] std::io::Error),
38 #[error("invalid PyTorch file format: {0}")]
40 InvalidFormat(String),
41 #[error("key not found in PyTorch file: {0}")]
43 KeyNotFound(String),
44 #[error("serde deserialization error: {0}")]
46 Serde(#[from] crate::nested::error::Error),
47}
48
49type Result<T> = std::result::Result<T, PytorchError>;
50
51#[derive(Debug, Clone)]
56pub struct PytorchMetadata {
57 pub format_version: Option<String>,
59 pub format_type: FileFormat,
61 pub byte_order: ByteOrder,
63 pub has_storage_alignment: bool,
65 pub pytorch_version: Option<String>,
68 pub tensor_count: usize,
70 pub total_data_size: Option<usize>,
72}
73
74impl PytorchMetadata {
75 pub fn is_modern_format(&self) -> bool {
77 matches!(self.format_type, FileFormat::Zip)
78 }
79
80 pub fn is_legacy_format(&self) -> bool {
82 matches!(self.format_type, FileFormat::Legacy)
83 }
84
85 fn for_format(format_type: FileFormat) -> Self {
86 Self {
87 format_version: None,
88 format_type,
89 byte_order: ByteOrder::LittleEndian,
90 has_storage_alignment: false,
91 pytorch_version: None,
92 tensor_count: 0,
93 total_data_size: None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub enum FileFormat {
101 Zip,
103 Tar,
105 Legacy,
107 Pickle,
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub enum ByteOrder {
114 LittleEndian,
115 BigEndian,
116}
117
118#[derive(Debug)]
146pub struct PytorchReader {
147 tensors: HashMap<String, Tensor>,
148 metadata: PytorchMetadata,
149}
150
151impl PytorchReader {
152 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
160 Self::open(path.as_ref(), None)
161 }
162
163 pub fn with_top_level_key<P: AsRef<Path>>(path: P, key: &str) -> Result<Self> {
181 Self::open(path.as_ref(), Some(key))
182 }
183
184 fn open(path: &Path, top_level_key: Option<&str>) -> Result<Self> {
185 let Loaded { root, mut metadata } = load_file(path)?;
186 let tensors = extract_tensors_at(root, top_level_key)?;
187 metadata.tensor_count = tensors.len();
188 Ok(Self { tensors, metadata })
189 }
190
191 pub fn from_reader<R: Read>(reader: R, top_level_key: Option<&str>) -> Result<Self> {
201 let root = read_pickle(&mut BufReader::new(reader), &PersistentIds::Unavailable)?;
202 let tensors = extract_tensors_at(root, top_level_key)?;
203 let mut metadata = PytorchMetadata::for_format(FileFormat::Pickle);
204 metadata.tensor_count = tensors.len();
205 Ok(Self { tensors, metadata })
206 }
207
208 pub fn keys(&self) -> Vec<String> {
210 self.tensors.keys().cloned().collect()
211 }
212
213 pub fn get(&self, name: &str) -> Option<&Tensor> {
215 self.tensors.get(name)
216 }
217
218 pub fn tensors(&self) -> &HashMap<String, Tensor> {
220 &self.tensors
221 }
222
223 pub fn into_tensors(self) -> HashMap<String, Tensor> {
225 self.tensors
226 }
227
228 pub fn metadata(&self) -> &PytorchMetadata {
232 &self.metadata
233 }
234
235 pub fn len(&self) -> usize {
237 self.tensors.len()
238 }
239
240 pub fn is_empty(&self) -> bool {
242 self.tensors.is_empty()
243 }
244
245 pub fn read_pickle_data<P: AsRef<Path>>(
259 path: P,
260 top_level_key: Option<&str>,
261 ) -> Result<PickleValue> {
262 let Loaded { root, .. } = load_file(path.as_ref())?;
263 let value = select_top_level(root, top_level_key)?;
264 Ok(to_pickle_value(value))
265 }
266
267 pub fn load_config<D, P>(path: P, top_level_key: Option<&str>) -> Result<D>
304 where
305 D: DeserializeOwned,
306 P: AsRef<Path>,
307 {
308 let pickle_value = Self::read_pickle_data(path, top_level_key)?;
309 let nested_value = to_nested_value(pickle_value);
310 let deserializer = Deserializer::<DefaultAdapter>::new(nested_value, false);
311 Ok(D::deserialize(deserializer)?)
312 }
313}
314
315#[derive(Debug, Clone, PartialEq)]
320pub enum PickleValue {
321 None,
323 Bool(bool),
325 Int(i64),
327 Float(f64),
329 String(String),
331 List(Vec<PickleValue>),
333 Dict(HashMap<String, PickleValue>),
335 Bytes(Vec<u8>),
337 Unsupported(String),
341}
342
343struct Loaded {
349 root: Object,
350 metadata: PytorchMetadata,
352}
353
354const LEGACY_MAGIC_BYTES: [u8; 10] = [0x6c, 0xfc, 0x9c, 0x46, 0xf9, 0x20, 0x6a, 0xa8, 0x50, 0x19];
357
358const LEGACY_MAGIC_TEXT: &[u8] = b"119547037146038801333356";
360
361const LEGACY_MAGIC_SEARCH: usize = 64;
365
366fn starts_with_legacy_magic(header: &[u8]) -> bool {
368 let window = &header[..header.len().min(LEGACY_MAGIC_SEARCH)];
369 window
370 .windows(LEGACY_MAGIC_BYTES.len())
371 .any(|bytes| bytes == LEGACY_MAGIC_BYTES)
372 || window
373 .windows(LEGACY_MAGIC_TEXT.len())
374 .any(|bytes| bytes == LEGACY_MAGIC_TEXT)
375}
376
377const TAR_MAGIC_OFFSET: usize = 257;
379
380fn detect_format(path: &Path) -> Result<FileFormat> {
381 let mut header = Vec::new();
382 File::open(path)?
383 .take((TAR_MAGIC_OFFSET + 5) as u64)
384 .read_to_end(&mut header)?;
385
386 if header.starts_with(b"PK\x03\x04") || header.starts_with(b"PK\x05\x06") {
387 Ok(FileFormat::Zip)
388 } else if header.get(TAR_MAGIC_OFFSET..TAR_MAGIC_OFFSET + 5) == Some(b"ustar") {
389 Ok(FileFormat::Tar)
390 } else if starts_with_legacy_magic(&header) {
391 Ok(FileFormat::Legacy)
392 } else {
393 match header.first() {
394 Some(0x80) | Some(b'(') | Some(b'}') | Some(b']') | Some(b'c')
395 if !starts_like_safetensors(&header) =>
396 {
397 Ok(FileFormat::Pickle)
398 }
399 _ => Err(PytorchError::InvalidFormat(
400 "not a PyTorch checkpoint: no ZIP, TAR, legacy or pickle header found".to_string(),
401 )),
402 }
403 }
404}
405
406const SAFETENSORS_MAX_HEADER: u64 = 100_000_000;
408
409fn starts_like_safetensors(header: &[u8]) -> bool {
414 let Some(len) = header.first_chunk::<8>().map(|b| u64::from_le_bytes(*b)) else {
415 return false;
416 };
417 len <= SAFETENSORS_MAX_HEADER && header.get(8) == Some(&b'{')
418}
419
420fn load_file(path: &Path) -> Result<Loaded> {
421 match detect_format(path)? {
422 FileFormat::Zip => load_zip(path),
423 FileFormat::Tar => load_tar(path),
424 FileFormat::Legacy => load_legacy(path),
425 FileFormat::Pickle => load_plain_pickle(path),
426 }
427}
428
429fn load_zip(path: &Path) -> Result<Loaded> {
430 let source = ZipSource::open(path)?;
431
432 match source.read_text("byteorder")?.as_deref() {
433 None | Some("little") => {}
434 Some("big") => return Err(big_endian_error()),
435 Some(other) => {
436 return Err(PytorchError::InvalidFormat(format!(
437 "Unrecognized byteorder entry '{other}', expected 'little' or 'big'"
438 )));
439 }
440 }
441 let mut metadata = PytorchMetadata::for_format(FileFormat::Zip);
442 metadata.format_version = source.read_text(".format_version")?;
443 metadata.pytorch_version = source.read_text("version")?;
444 metadata.has_storage_alignment = source.has_entry(".storage_alignment");
445 metadata.total_data_size = Some(source.data_size()? as usize);
446
447 let pickle = source.pickle()?;
448 let ids = PersistentIds::Storages(Arc::new(StorageSource::Zip(source)));
449 let root = read_pickle(&mut Cursor::new(pickle), &ids)?;
450
451 Ok(Loaded { root, metadata })
452}
453
454fn load_legacy(path: &Path) -> Result<Loaded> {
455 let mut reader = BufReader::new(File::open(path)?);
456
457 let read_header = |reader: &mut BufReader<File>, what: &str| {
458 read_pickle(reader, &PersistentIds::Unavailable).map_err(|e| {
459 PytorchError::InvalidFormat(format!("Failed to read {what} from legacy format: {e}"))
460 })
461 };
462 read_header(&mut reader, "magic number")?;
466 let protocol_version = read_header(&mut reader, "protocol version")?;
468 if !matches!(protocol_version, Object::Int(1001)) {
469 return Err(PytorchError::InvalidFormat(format!(
470 "Unsupported legacy protocol version {protocol_version:?}, expected 1001"
471 )));
472 }
473 let sys_info = read_header(&mut reader, "system info")?;
474 check_little_endian(&sys_info)?;
475
476 let source = Arc::new(StorageSource::Legacy(LegacySource::new(
481 reader.get_ref().try_clone()?,
482 )));
483 let root = read_pickle(&mut reader, &PersistentIds::Storages(source.clone()))?;
484
485 let storage_keys = match read_header(&mut reader, "storage key list")? {
487 Object::List(keys) => keys
488 .iter()
489 .map(|key| key_string(key, "legacy storage key"))
490 .collect::<std::result::Result<Vec<_>, _>>()?,
491 other => {
492 return Err(PytorchError::InvalidFormat(format!(
493 "legacy storage key list must be a list, got {other:?}"
494 )));
495 }
496 };
497
498 let data_start = reader.stream_position()?;
499 let file_len = reader.seek(SeekFrom::End(0))?;
500 if let StorageSource::Legacy(legacy) = &*source {
501 legacy
502 .finish(&storage_keys, data_start, file_len)
503 .map_err(|e| PytorchError::InvalidFormat(e.to_string()))?;
504 }
505
506 let mut metadata = PytorchMetadata::for_format(FileFormat::Legacy);
507 metadata.total_data_size = Some((file_len - data_start) as usize);
508 Ok(Loaded { root, metadata })
509}
510
511fn load_plain_pickle(path: &Path) -> Result<Loaded> {
512 let mut reader = BufReader::new(File::open(path)?);
513 let root = read_pickle(&mut reader, &PersistentIds::Unavailable)?;
514 Ok(Loaded {
515 root,
516 metadata: PytorchMetadata::for_format(FileFormat::Pickle),
517 })
518}
519
520fn load_tar(path: &Path) -> Result<Loaded> {
521 let mut archive = tar::Archive::new(BufReader::new(File::open(path)?));
522 let mut entries: HashMap<String, Vec<u8>> = HashMap::new();
523
524 for entry in archive.entries().map_err(PytorchError::Tar)? {
525 let mut entry = entry.map_err(PytorchError::Tar)?;
526 let name = entry
527 .path()
528 .map_err(PytorchError::Tar)?
529 .to_string_lossy()
530 .trim_start_matches("./")
531 .to_string();
532 if matches!(
533 name.as_str(),
534 "sys_info" | "storages" | "tensors" | "pickle"
535 ) {
536 let mut data = Vec::new();
537 entry.read_to_end(&mut data).map_err(PytorchError::Tar)?;
538 entries.insert(name, data);
539 }
540 }
541
542 if let Some(sys_info) = entries.remove("sys_info") {
543 let sys_info = read_pickle(&mut Cursor::new(sys_info), &PersistentIds::Unavailable)?;
544 check_little_endian(&sys_info)?;
545 }
546 let mut take = |name: &str| {
547 entries
548 .remove(name)
549 .ok_or_else(|| PytorchError::InvalidFormat(format!("TAR file missing '{name}' entry")))
550 };
551 let storages = take("storages")?;
552 let tensors = take("tensors")?;
553 let pickle = take("pickle")?;
554
555 let total_data_size = storages.len();
556 let (tar_source, storage_info) = parse_tar_storages(storages)?;
557 let source = Arc::new(StorageSource::Tar(tar_source));
558 let tensors = parse_tar_tensors(&tensors, &storage_info, &source)?;
559 let root = read_pickle(&mut Cursor::new(pickle), &PersistentIds::Tensors(tensors))?;
560
561 let mut metadata = PytorchMetadata::for_format(FileFormat::Tar);
562 metadata.total_data_size = Some(total_data_size);
563 Ok(Loaded { root, metadata })
564}
565
566type TarStorageInfo = HashMap<String, (DType, usize)>;
568
569fn parse_tar_storages(blob: Vec<u8>) -> Result<(TarSource, TarStorageInfo)> {
572 let mut cursor = Cursor::new(blob.as_slice());
573 let count = read_tar_count(&mut cursor, "storage")?;
574
575 let plausible = count.min(blob.len() / 8);
577 let mut layout: HashMap<String, (usize, usize)> = HashMap::with_capacity(plausible);
578 let mut info = TarStorageInfo::with_capacity(plausible);
579
580 for _ in 0..count {
581 let meta = read_pickle(&mut cursor, &PersistentIds::Unavailable)?;
582 let [key, _location, storage_type] = tar_fields(&meta, "storage metadata")?;
583 let key = key_string(key, "TAR storage key")?;
586 let dtype = match storage_type {
587 Object::Class { name, .. } => storage_type_to_dtype(name.as_str())?,
588 other => {
589 return Err(PytorchError::InvalidFormat(format!(
590 "TAR storage type must be a class, got {other:?}"
591 )));
592 }
593 };
594
595 let numel = read_tar_usize(&mut cursor, "storage element count")?;
596 let byte_len = numel.checked_mul(dtype.size()).ok_or_else(|| {
597 PytorchError::InvalidFormat(format!("TAR storage '{key}' byte length overflows usize"))
598 })?;
599 let offset = cursor.position() as usize;
600 let end = offset
601 .checked_add(byte_len)
602 .filter(|&end| end <= blob.len())
603 .ok_or_else(|| {
604 PytorchError::InvalidFormat(format!(
605 "TAR storage '{key}' ({byte_len} bytes) extends beyond the storages entry"
606 ))
607 })?;
608
609 layout.insert(key.clone(), (offset, byte_len));
610 info.insert(key, (dtype, byte_len));
611 cursor.set_position(end as u64);
612 }
613
614 {
617 let views = read_pickle(&mut cursor, &PersistentIds::Unavailable).map_err(|e| {
618 PytorchError::InvalidFormat(format!("TAR storage views list missing: {e}"))
619 })?;
620 let Object::List(views) = views else {
621 return Err(PytorchError::InvalidFormat(format!(
622 "TAR storage views must be a list, got {views:?}"
623 )));
624 };
625 for view in views {
626 let [target, root, offset, numel] = tar_fields(&view, "storage view")?;
627 let target = key_string(target, "TAR storage view key")?;
628 let root = key_string(root, "TAR storage view root")?;
629 let offset = non_negative(offset, "TAR storage view offset")?;
630 let numel = non_negative(numel, "TAR storage view element count")?;
631 let &(root_offset, root_len) = layout.get(&root).ok_or_else(|| {
632 PytorchError::InvalidFormat(format!(
633 "TAR storage view '{target}' refers to unknown storage '{root}'"
634 ))
635 })?;
636 let (dtype, _) = info[&root];
637 let element_size = dtype.size();
638 let (start, len) = offset
639 .checked_mul(element_size)
640 .zip(numel.checked_mul(element_size))
641 .filter(|&(start, len)| start.checked_add(len).is_some_and(|end| end <= root_len))
642 .ok_or_else(|| {
643 PytorchError::InvalidFormat(format!(
644 "TAR storage view '{target}' lies outside storage '{root}'"
645 ))
646 })?;
647 layout.insert(target.clone(), (root_offset + start, len));
648 info.insert(target, (dtype, len));
649 }
650 }
651
652 Ok((TarSource::new(blob, layout), info))
653}
654
655fn parse_tar_tensors(
658 blob: &[u8],
659 storage_info: &TarStorageInfo,
660 source: &Arc<StorageSource>,
661) -> Result<HashMap<String, Tensor>> {
662 let mut cursor = Cursor::new(blob);
663 let count = read_tar_count(&mut cursor, "tensor")?;
664 let mut tensors = HashMap::with_capacity(count.min(blob.len() / 8));
665
666 for _ in 0..count {
667 let meta = read_pickle(&mut cursor, &PersistentIds::Unavailable)?;
668 let [key, storage_key, _tensor_type] = tar_fields(&meta, "tensor metadata")?;
669 let key = key_string(key, "TAR tensor key")?;
670 let storage_key = key_string(storage_key, "TAR tensor storage key")?;
671
672 let rank = cursor.read_i32::<LittleEndian>()?;
673 let rank = usize::try_from(rank)
674 .ok()
675 .filter(|&rank| rank <= 64)
676 .ok_or_else(|| {
677 PytorchError::InvalidFormat(format!("TAR tensor '{key}' has invalid rank {rank}"))
678 })?;
679 cursor.read_i32::<LittleEndian>()?; let shape = (0..rank)
681 .map(|_| read_tar_usize(&mut cursor, "tensor shape"))
682 .collect::<Result<Vec<_>>>()?;
683 let stride = (0..rank)
684 .map(|_| read_tar_usize(&mut cursor, "tensor stride"))
685 .collect::<Result<Vec<_>>>()?;
686 let storage_offset = read_tar_usize(&mut cursor, "tensor storage offset")?;
687
688 let &(dtype, byte_len) = storage_info.get(&storage_key).ok_or_else(|| {
689 PytorchError::InvalidFormat(format!(
690 "TAR tensor '{key}' refers to unknown storage '{storage_key}'"
691 ))
692 })?;
693 let storage = StorageRef {
694 source: source.clone(),
695 key: storage_key,
696 dtype: Some(dtype),
697 reachable_bytes: byte_len,
700 view_offset: 0,
701 };
702 let tensor = build_tensor(storage, dtype, storage_offset, shape, stride)?;
703 tensors.insert(key, tensor);
704 }
705
706 Ok(tensors)
707}
708
709fn read_tar_count(cursor: &mut Cursor<&[u8]>, what: &str) -> Result<usize> {
710 let count = read_pickle(cursor, &PersistentIds::Unavailable)?;
711 Ok(non_negative(&count, &format!("TAR {what} count"))?)
712}
713
714fn read_tar_usize(cursor: &mut Cursor<&[u8]>, what: &str) -> Result<usize> {
716 let value = cursor.read_i64::<LittleEndian>()?;
717 usize::try_from(value).map_err(|_| {
718 PytorchError::InvalidFormat(format!("TAR {what} must be non-negative, got {value}"))
719 })
720}
721
722fn tar_fields<'a, const N: usize>(obj: &'a Object, what: &str) -> Result<&'a [Object; N]> {
724 match obj {
725 Object::Tuple(fields) => <&[Object; N]>::try_from(fields.as_slice()).map_err(|_| {
726 PytorchError::InvalidFormat(format!(
727 "TAR {what} must have {N} fields, got {}",
728 fields.len()
729 ))
730 }),
731 other => Err(PytorchError::InvalidFormat(format!(
732 "TAR {what} must be a tuple, got {other:?}"
733 ))),
734 }
735}
736
737fn check_little_endian(sys_info: &Object) -> Result<()> {
743 let Object::Dict(dict) = sys_info else {
744 return Err(PytorchError::InvalidFormat(format!(
745 "Legacy sys_info must be a dict, got {sys_info:?}"
746 )));
747 };
748 match dict.get("little_endian") {
749 Some(Object::Bool(true)) => Ok(()),
750 Some(Object::Bool(false)) => Err(big_endian_error()),
751 other => Err(PytorchError::InvalidFormat(format!(
752 "Legacy sys_info must carry a little_endian bool, got {other:?}"
753 ))),
754 }
755}
756
757fn big_endian_error() -> PytorchError {
758 PytorchError::InvalidFormat(
759 "Big-endian PyTorch files are not supported. The file was saved on a big-endian system and requires byte order conversion."
760 .to_string(),
761 )
762}
763
764fn select_top_level(root: Object, key: Option<&str>) -> Result<Object> {
770 let Some(key) = key else {
771 return Ok(root);
772 };
773 let Object::Dict(mut dict) = root else {
774 return Err(PytorchError::InvalidFormat(format!(
775 "Cannot select top-level key '{key}': the file's root is not a dictionary"
776 )));
777 };
778 dict.remove(key).ok_or_else(|| {
779 let mut available: Vec<_> = dict.keys().cloned().collect();
780 available.sort();
781 PytorchError::KeyNotFound(format!(
782 "'{key}' not found. Available top-level keys: {available:?}"
783 ))
784 })
785}
786
787fn extract_tensors_at(
788 root: Object,
789 top_level_key: Option<&str>,
790) -> Result<HashMap<String, Tensor>> {
791 match select_top_level(root, top_level_key)? {
792 Object::Dict(dict) => Ok(extract_tensors(dict)),
793 other => {
794 let found = other.python_type_name();
795 Err(PytorchError::InvalidFormat(match top_level_key {
796 Some(key) => {
797 format!("Top-level key '{key}' does not hold a dictionary, found {found}")
798 }
799 None => {
800 format!("Expected a dictionary at the root of the PyTorch file, found {found}")
801 }
802 }))
803 }
804 }
805}
806
807fn to_pickle_value(obj: Object) -> PickleValue {
812 match obj {
813 Object::None => PickleValue::None,
814 Object::Bool(b) => PickleValue::Bool(b),
815 Object::Int(i) => PickleValue::Int(i),
816 Object::Float(f) => PickleValue::Float(f),
817 Object::String(s) => PickleValue::String(s),
818 Object::Bytes(b) => PickleValue::Bytes(b),
819 Object::List(items) | Object::Tuple(items) => {
820 PickleValue::List(items.into_iter().map(to_pickle_value).collect())
821 }
822 Object::Dict(dict) => PickleValue::Dict(
823 dict.into_iter()
824 .map(|(k, v)| (k, to_pickle_value(v)))
825 .collect(),
826 ),
827 Object::Class { .. } | Object::Storage(_) | Object::Opaque(_) | Object::Tensor(_) => {
828 PickleValue::Unsupported(obj.python_type_name())
829 }
830 }
831}
832
833fn to_nested_value(value: PickleValue) -> NestedValue {
835 match value {
836 PickleValue::None => NestedValue::Default(None),
837 PickleValue::Bool(b) => NestedValue::Bool(b),
838 PickleValue::Int(i) => NestedValue::I64(i),
839 PickleValue::Float(f) => NestedValue::F64(f),
840 PickleValue::String(s) => NestedValue::String(s),
841 PickleValue::List(list) => {
842 NestedValue::Vec(list.into_iter().map(to_nested_value).collect())
843 }
844 PickleValue::Dict(dict) => NestedValue::Map(
845 dict.into_iter()
846 .map(|(k, v)| (k, to_nested_value(v)))
847 .collect(),
848 ),
849 PickleValue::Bytes(data) => {
850 NestedValue::Vec(data.into_iter().map(NestedValue::U8).collect())
851 }
852 PickleValue::Unsupported(type_name) => NestedValue::Unsupported(type_name),
853 }
854}