Skip to main content

fuel_formats/
pickle.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Python pickle parser — minimum subset needed to read PyTorch
3//! `.pth` checkpoint files.
4//!
5//! This is **not** a general-purpose pickle parser. It implements just
6//! enough of [protocol 2](https://docs.python.org/3/library/pickle.html)
7//! to extract `state_dict`-style tensor metadata from PyTorch
8//! checkpoints. The recognized object types are hard-coded around
9//! `torch._utils._rebuild_tensor_v2` and friends.
10//!
11//! The Tensor-construction layer (`PthTensors::get`, `read_all`,
12//! `read_all_with_key`) lives in `fuel-core/src/pickle.rs` since it
13//! materializes `Tensor`s from raw storage bytes; this module
14//! produces the metadata index.
15
16use std::collections::HashMap;
17use std::io::BufRead;
18
19use byteorder::{LittleEndian, ReadBytesExt};
20use fuel_ir::{Context, DType, Error as E, Layout, Result, Shape, bail};
21
22const VERBOSE: bool = false;
23
24/// Subset of Python pickle opcodes recognized by this reader.
25///
26/// Reference: <https://docs.juliahub.com/Pickle/LAUNc/0.1.0/opcode/>.
27/// CPython opcode reference:
28/// <https://github.com/python/cpython/blob/ed25f097160b5cbb0c9a1f9a746d2f1bbc96515a/Lib/pickletools.py#L2123>
29#[repr(u8)]
30#[derive(Debug, Eq, PartialEq, Clone)]
31pub enum OpCode {
32    Proto = 0x80,
33    Global = b'c',
34    BinPut = b'q',
35    LongBinPut = b'r',
36    EmptyTuple = b')',
37    Reduce = b'R',
38    Mark = b'(',
39    BinUnicode = b'X',
40    BinInt = b'J',
41    Tuple = b't',
42    BinPersId = b'Q',
43    BinInt1 = b'K',
44    BinInt2 = b'M',
45    Tuple1 = 0x85,
46    Tuple2 = 0x86,
47    Tuple3 = 0x87,
48    NewTrue = 0x88,
49    NewFalse = 0x89,
50    None = b'N',
51    BinGet = b'h',
52    LongBinGet = b'j',
53    SetItem = b's',
54    SetItems = b'u',
55    EmptyDict = b'}',
56    Dict = b'd',
57    Build = b'b',
58    Stop = b'.',
59    NewObj = 0x81,
60    EmptyList = b']',
61    BinFloat = b'G',
62    Append = b'a',
63    Appends = b'e',
64    Long1 = 0x8a,
65}
66
67impl TryFrom<u8> for OpCode {
68    type Error = u8;
69    fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
70        match value {
71            0x80 => Ok(Self::Proto),
72            b'c' => Ok(Self::Global),
73            b'q' => Ok(Self::BinPut),
74            b'r' => Ok(Self::LongBinPut),
75            b')' => Ok(Self::EmptyTuple),
76            b'R' => Ok(Self::Reduce),
77            b'(' => Ok(Self::Mark),
78            b'X' => Ok(Self::BinUnicode),
79            b'J' => Ok(Self::BinInt),
80            b't' => Ok(Self::Tuple),
81            b'Q' => Ok(Self::BinPersId),
82            b'K' => Ok(Self::BinInt1),
83            b'M' => Ok(Self::BinInt2),
84            b'N' => Ok(Self::None),
85            0x85 => Ok(Self::Tuple1),
86            0x86 => Ok(Self::Tuple2),
87            0x87 => Ok(Self::Tuple3),
88            0x88 => Ok(Self::NewTrue),
89            0x89 => Ok(Self::NewFalse),
90            b'h' => Ok(Self::BinGet),
91            b'j' => Ok(Self::LongBinGet),
92            b's' => Ok(Self::SetItem),
93            b'u' => Ok(Self::SetItems),
94            b'}' => Ok(Self::EmptyDict),
95            b'd' => Ok(Self::EmptyDict),
96            b'b' => Ok(Self::Build),
97            b'.' => Ok(Self::Stop),
98            0x81 => Ok(Self::NewObj),
99            b']' => Ok(Self::EmptyList),
100            b'G' => Ok(Self::BinFloat),
101            b'a' => Ok(Self::Append),
102            b'e' => Ok(Self::Appends),
103            0x8a => Ok(Self::Long1),
104            value => Err(value),
105        }
106    }
107}
108
109fn read_to_newline<R: BufRead>(r: &mut R) -> Result<Vec<u8>> {
110    let mut data: Vec<u8> = Vec::with_capacity(32);
111    r.read_until(b'\n', &mut data)?;
112    data.pop();
113    if data.last() == Some(&b'\r') {
114        data.pop();
115    }
116    Ok(data)
117}
118
119/// A deserialized Python object encountered during pickle parsing.
120#[derive(Debug, Clone, PartialEq)]
121pub enum Object {
122    Class {
123        module_name: String,
124        class_name: String,
125    },
126    Int(i32),
127    Long(i64),
128    Float(f64),
129    Unicode(String),
130    Bool(bool),
131    None,
132    Tuple(Vec<Object>),
133    List(Vec<Object>),
134    Mark,
135    Dict(Vec<(Object, Object)>),
136    Reduce {
137        callable: Box<Object>,
138        args: Box<Object>,
139    },
140    Build {
141        callable: Box<Object>,
142        args: Box<Object>,
143    },
144    PersistentLoad(Box<Object>),
145}
146
147type OResult<T> = std::result::Result<T, Object>;
148
149impl Object {
150    pub fn unicode(self) -> OResult<String> {
151        match self {
152            Self::Unicode(t) => Ok(t),
153            _ => Err(self),
154        }
155    }
156
157    pub fn reduce(self) -> OResult<(Self, Self)> {
158        match self {
159            Self::Reduce { callable, args } => Ok((*callable, *args)),
160            _ => Err(self),
161        }
162    }
163
164    pub fn none(self) -> OResult<()> {
165        match self {
166            Self::None => Ok(()),
167            _ => Err(self),
168        }
169    }
170
171    pub fn persistent_load(self) -> OResult<Self> {
172        match self {
173            Self::PersistentLoad(t) => Ok(*t),
174            _ => Err(self),
175        }
176    }
177
178    pub fn bool(self) -> OResult<bool> {
179        match self {
180            Self::Bool(t) => Ok(t),
181            _ => Err(self),
182        }
183    }
184
185    pub fn int(self) -> OResult<i32> {
186        match self {
187            Self::Int(t) => Ok(t),
188            _ => Err(self),
189        }
190    }
191
192    pub fn int_or_long(self) -> OResult<i64> {
193        match self {
194            Self::Int(t) => Ok(t as i64),
195            Self::Long(t) => Ok(t),
196            _ => Err(self),
197        }
198    }
199
200    pub fn tuple(self) -> OResult<Vec<Self>> {
201        match self {
202            Self::Tuple(t) => Ok(t),
203            _ => Err(self),
204        }
205    }
206
207    pub fn dict(self) -> OResult<Vec<(Self, Self)>> {
208        match self {
209            Self::Dict(t) => Ok(t),
210            _ => Err(self),
211        }
212    }
213
214    pub fn class(self) -> OResult<(String, String)> {
215        match self {
216            Self::Class {
217                module_name,
218                class_name,
219            } => Ok((module_name, class_name)),
220            _ => Err(self),
221        }
222    }
223
224    /// Attempt to interpret this object as a PyTorch tensor reconstruction call and extract
225    /// its [`TensorInfo`]. Returns `Ok(None)` if the object does not match the expected
226    /// pattern (e.g. it is not a `_rebuild_tensor_v2` call).
227    pub fn into_tensor_info(
228        self,
229        name: Self,
230        dir_name: &std::path::Path,
231    ) -> Result<Option<TensorInfo>> {
232        let name = match name.unicode() {
233            Ok(name) => name,
234            Err(_) => return Ok(None),
235        };
236        let (callable, args) = match self.reduce() {
237            Ok(callable_args) => callable_args,
238            _ => return Ok(None),
239        };
240        let (callable, args) = match callable {
241            Object::Class {
242                module_name,
243                class_name,
244            } if module_name == "torch._tensor" && class_name == "_rebuild_from_type_v2" => {
245                let mut args = args.tuple()?;
246                let callable = args.remove(0);
247                let args = args.remove(1);
248                (callable, args)
249            }
250            Object::Class {
251                module_name,
252                class_name,
253            } if module_name == "torch._utils" && class_name == "_rebuild_parameter" => {
254                let mut args = args.tuple()?;
255                args.remove(0).reduce()?
256            }
257            _ => (callable, args),
258        };
259        match callable {
260            Object::Class {
261                module_name,
262                class_name,
263            } if module_name == "torch._utils" && class_name == "_rebuild_tensor_v2" => {}
264            _ => return Ok(None),
265        };
266        let (layout, dtype, file_path, storage_size) = rebuild_args(args)?;
267        Ok(Some(TensorInfo {
268            name,
269            dtype,
270            layout,
271            path: format!("{}/{}", dir_name.to_string_lossy(), file_path),
272            storage_size,
273        }))
274    }
275}
276
277impl TryFrom<Object> for String {
278    type Error = Object;
279    fn try_from(value: Object) -> std::result::Result<Self, Self::Error> {
280        match value {
281            Object::Unicode(s) => Ok(s),
282            other => Err(other),
283        }
284    }
285}
286
287impl TryFrom<Object> for usize {
288    type Error = Object;
289    fn try_from(value: Object) -> std::result::Result<Self, Self::Error> {
290        match value {
291            Object::Int(s) if s >= 0 => Ok(s as usize),
292            other => Err(other),
293        }
294    }
295}
296
297impl<T: TryFrom<Object, Error = Object>> TryFrom<Object> for Vec<T> {
298    type Error = Object;
299    fn try_from(value: Object) -> std::result::Result<Self, Self::Error> {
300        match value {
301            Object::Tuple(values) => values
302                .into_iter()
303                .map(|v| T::try_from(v))
304                .collect::<std::result::Result<Vec<T>, Self::Error>>(),
305            other => Err(other),
306        }
307    }
308}
309
310/// The pickle virtual machine's execution stack.
311#[derive(Debug)]
312pub struct Stack {
313    stack: Vec<Object>,
314    memo: HashMap<u32, Object>,
315}
316
317impl Stack {
318    pub fn empty() -> Self {
319        Self {
320            stack: Vec::with_capacity(512),
321            memo: HashMap::new(),
322        }
323    }
324
325    pub fn stack(&self) -> &[Object] {
326        self.stack.as_slice()
327    }
328
329    /// Read and execute pickle opcodes from `r` until a `STOP` opcode is encountered.
330    pub fn read_loop<R: BufRead>(&mut self, r: &mut R) -> Result<()> {
331        loop {
332            if self.read(r)? {
333                break;
334            }
335        }
336        Ok(())
337    }
338
339    /// Consume the stack and return the single top-level object.
340    pub fn finalize(mut self) -> Result<Object> {
341        self.pop()
342    }
343
344    fn push(&mut self, obj: Object) {
345        self.stack.push(obj)
346    }
347
348    fn pop(&mut self) -> Result<Object> {
349        match self.stack.pop() {
350            None => bail!("unexpected empty stack"),
351            Some(obj) => Ok(obj),
352        }
353    }
354
355    // https://docs.juliahub.com/Pickle/LAUNc/0.1.0/opcode/#Pickle.OpCodes.BUILD
356    fn build(&mut self) -> Result<()> {
357        let args = self.pop()?;
358        let obj = self.pop()?;
359        let obj = match (obj, args) {
360            (Object::Dict(mut obj), Object::Dict(mut args)) => {
361                obj.append(&mut args);
362                Object::Dict(obj)
363            }
364            (obj, args) => Object::Build {
365                callable: Box::new(obj),
366                args: Box::new(args),
367            },
368        };
369        self.push(obj);
370        Ok(())
371    }
372
373    fn reduce(&mut self) -> Result<()> {
374        let args = self.pop()?;
375        let callable = self.pop()?;
376        #[allow(clippy::single_match)]
377        let reduced = match &callable {
378            Object::Class {
379                module_name,
380                class_name,
381            } => {
382                if module_name == "collections"
383                    && (class_name == "OrderedDict" || class_name == "defaultdict")
384                {
385                    // TODO: have a separate ordered dict and a separate default dict.
386                    Some(Object::Dict(vec![]))
387                } else {
388                    None
389                }
390            }
391            _ => None,
392        };
393        let reduced = reduced.unwrap_or_else(|| Object::Reduce {
394            callable: Box::new(callable),
395            args: Box::new(args),
396        });
397        self.push(reduced);
398        Ok(())
399    }
400
401    fn last(&mut self) -> Result<&mut Object> {
402        match self.stack.last_mut() {
403            None => bail!("unexpected empty stack"),
404            Some(obj) => Ok(obj),
405        }
406    }
407
408    fn memo_get(&self, id: u32) -> Result<Object> {
409        match self.memo.get(&id) {
410            None => bail!("missing object in memo {id}"),
411            Some(obj) => Ok(obj.clone()),
412        }
413    }
414
415    fn memo_put(&mut self, id: u32) -> Result<()> {
416        let obj = self.last()?.clone();
417        self.memo.insert(id, obj);
418        Ok(())
419    }
420
421    fn persistent_load(&self, id: Object) -> Result<Object> {
422        Ok(Object::PersistentLoad(Box::new(id)))
423    }
424
425    fn new_obj(&self, class: Object, args: Object) -> Result<Object> {
426        Ok(Object::Reduce {
427            callable: Box::new(class),
428            args: Box::new(args),
429        })
430    }
431
432    fn pop_to_marker(&mut self) -> Result<Vec<Object>> {
433        let mut mark_idx = None;
434        for (idx, obj) in self.stack.iter().enumerate().rev() {
435            if obj == &Object::Mark {
436                mark_idx = Some(idx);
437                break;
438            }
439        }
440        match mark_idx {
441            Some(mark_idx) => {
442                let objs = self.stack.split_off(mark_idx + 1);
443                self.stack.pop();
444                Ok(objs)
445            }
446            None => bail!("marker object not found"),
447        }
448    }
449
450    /// Read and execute a single pickle opcode. Returns `Ok(true)` when a `STOP` opcode is
451    /// encountered, indicating the stream is complete.
452    pub fn read<R: BufRead>(&mut self, r: &mut R) -> Result<bool> {
453        let op_code = match OpCode::try_from(r.read_u8()?) {
454            Ok(op_code) => op_code,
455            Err(op_code) => bail!("unknown op-code {op_code}"),
456        };
457        match op_code {
458            OpCode::Proto => {
459                let version = r.read_u8()?;
460                if VERBOSE {
461                    println!("proto {version}");
462                }
463            }
464            OpCode::Global => {
465                let module_name = read_to_newline(r)?;
466                let class_name = read_to_newline(r)?;
467                let module_name = String::from_utf8_lossy(&module_name).to_string();
468                let class_name = String::from_utf8_lossy(&class_name).to_string();
469                self.push(Object::Class {
470                    module_name,
471                    class_name,
472                })
473            }
474            OpCode::BinInt1 => {
475                let arg = r.read_u8()?;
476                self.push(Object::Int(arg as i32))
477            }
478            OpCode::BinInt2 => {
479                let arg = r.read_u16::<LittleEndian>()?;
480                self.push(Object::Int(arg as i32))
481            }
482            OpCode::BinInt => {
483                let arg = r.read_i32::<LittleEndian>()?;
484                self.push(Object::Int(arg))
485            }
486            OpCode::BinFloat => {
487                // Floats are encoded big-endian whereas int types use little-endian.
488                // https://github.com/python/cpython/blob/0c80da4c14d904a367968955544dd6ae58c8101c/Lib/pickletools.py#L855
489                // https://github.com/pytorch/pytorch/blob/372d078f361e726bb4ac0884ac334b04c58179ef/torch/_weights_only_unpickler.py#L243
490                let arg = r.read_f64::<byteorder::BigEndian>()?;
491                self.push(Object::Float(arg))
492            }
493            OpCode::BinUnicode => {
494                let len = r.read_u32::<LittleEndian>()?;
495                let mut data = vec![0u8; len as usize];
496                r.read_exact(&mut data)?;
497                let data = String::from_utf8(data).map_err(E::wrap)?;
498                self.push(Object::Unicode(data))
499            }
500            OpCode::BinPersId => {
501                let id = self.pop()?;
502                let obj = self.persistent_load(id)?;
503                self.push(obj)
504            }
505            OpCode::Tuple => {
506                let objs = self.pop_to_marker()?;
507                self.push(Object::Tuple(objs))
508            }
509            OpCode::Tuple1 => {
510                let obj = self.pop()?;
511                self.push(Object::Tuple(vec![obj]))
512            }
513            OpCode::Tuple2 => {
514                let obj2 = self.pop()?;
515                let obj1 = self.pop()?;
516                self.push(Object::Tuple(vec![obj1, obj2]))
517            }
518            OpCode::Tuple3 => {
519                let obj3 = self.pop()?;
520                let obj2 = self.pop()?;
521                let obj1 = self.pop()?;
522                self.push(Object::Tuple(vec![obj1, obj2, obj3]))
523            }
524            OpCode::NewTrue => self.push(Object::Bool(true)),
525            OpCode::NewFalse => self.push(Object::Bool(false)),
526            OpCode::Append => {
527                let value = self.pop()?;
528                let pylist = self.last()?;
529                if let Object::List(d) = pylist {
530                    d.push(value)
531                } else {
532                    bail!("expected a list, got {pylist:?}")
533                }
534            }
535            OpCode::Appends => {
536                let objs = self.pop_to_marker()?;
537                let pylist = self.last()?;
538                if let Object::List(d) = pylist {
539                    d.extend(objs)
540                } else {
541                    bail!("expected a list, got {pylist:?}")
542                }
543            }
544            OpCode::SetItem => {
545                let value = self.pop()?;
546                let key = self.pop()?;
547                let pydict = self.last()?;
548                if let Object::Dict(d) = pydict {
549                    d.push((key, value))
550                } else {
551                    bail!("expected a dict, got {pydict:?}")
552                }
553            }
554            OpCode::SetItems => {
555                let mut objs = self.pop_to_marker()?;
556                let pydict = self.last()?;
557                if let Object::Dict(d) = pydict {
558                    if objs.len() % 2 != 0 {
559                        bail!("setitems: not an even number of objects")
560                    }
561                    while let Some(value) = objs.pop() {
562                        let key = objs.pop().context("empty objs")?;
563                        d.push((key, value))
564                    }
565                } else {
566                    bail!("expected a dict, got {pydict:?}")
567                }
568            }
569            OpCode::None => self.push(Object::None),
570            OpCode::Stop => return Ok(true),
571            OpCode::Build => self.build()?,
572            OpCode::EmptyDict => self.push(Object::Dict(vec![])),
573            OpCode::Dict => {
574                let mut objs = self.pop_to_marker()?;
575                let mut pydict = vec![];
576                if objs.len() % 2 != 0 {
577                    bail!("setitems: not an even number of objects")
578                }
579                while let Some(value) = objs.pop() {
580                    let key = objs.pop().context("empty objs")?;
581                    pydict.push((key, value))
582                }
583                self.push(Object::Dict(pydict))
584            }
585            OpCode::Mark => self.push(Object::Mark),
586            OpCode::Reduce => self.reduce()?,
587            OpCode::EmptyTuple => self.push(Object::Tuple(vec![])),
588            OpCode::EmptyList => self.push(Object::List(vec![])),
589            OpCode::BinGet => {
590                let arg = r.read_u8()?;
591                let obj = self.memo_get(arg as u32)?;
592                self.push(obj)
593            }
594            OpCode::LongBinGet => {
595                let arg = r.read_u32::<LittleEndian>()?;
596                let obj = self.memo_get(arg)?;
597                self.push(obj)
598            }
599            OpCode::BinPut => {
600                let arg = r.read_u8()?;
601                self.memo_put(arg as u32)?
602            }
603            OpCode::LongBinPut => {
604                let arg = r.read_u32::<LittleEndian>()?;
605                self.memo_put(arg)?
606            }
607            OpCode::NewObj => {
608                let args = self.pop()?;
609                let class = self.pop()?;
610                let obj = self.new_obj(class, args)?;
611                self.push(obj)
612            }
613            OpCode::Long1 => {
614                let n_bytes = r.read_u8()?;
615                let mut v = 0;
616                // Decode the next n bytes in little endian
617                for i in 0..n_bytes {
618                    v |= (r.read_u8()? as i64) << (i * 8);
619                }
620                self.push(Object::Long(v))
621            }
622        }
623        Ok(false)
624    }
625}
626
627impl From<Object> for E {
628    fn from(value: Object) -> Self {
629        E::Msg(format!("conversion error on {value:?}"))
630    }
631}
632
633// https://github.com/pytorch/pytorch/blob/4eac43d046ded0f0a5a5fa8db03eb40f45bf656e/torch/_utils.py#L198
634// Arguments: storage, storage_offset, size, stride, requires_grad, backward_hooks
635fn rebuild_args(args: Object) -> Result<(Layout, DType, String, usize)> {
636    let mut args = args.tuple()?;
637    let stride_usize = Vec::<usize>::try_from(args.remove(3))?;
638    let stride: fuel_ir::StrideVec = stride_usize.iter().map(|&s| s as isize).collect();
639    let size = Vec::<usize>::try_from(args.remove(2))?;
640    let offset = args.remove(1).int_or_long()? as usize;
641    let storage = args.remove(0).persistent_load()?;
642    let mut storage = storage.tuple()?;
643    let storage_size = storage.remove(4).int_or_long()? as usize;
644    let path = storage.remove(2).unicode()?;
645    let (_module_name, class_name) = storage.remove(1).class()?;
646    let dtype = match class_name.as_str() {
647        "FloatStorage" => DType::F32,
648        "DoubleStorage" => DType::F64,
649        "HalfStorage" => DType::F16,
650        "BFloat16Storage" => DType::BF16,
651        "ByteStorage" => DType::U8,
652        "LongStorage" => DType::I64,
653        other => bail!("unsupported storage type {other}"),
654    };
655    let layout = Layout::new(Shape::from(size), stride, offset * dtype.size_in_bytes());
656    Ok((layout, dtype, path, storage_size))
657}
658
659/// Metadata describing a single tensor inside a PyTorch `.pth` archive.
660///
661/// Produced by [`read_pth_tensor_info`]. The `path` field contains the
662/// zip-internal path to the raw data file — fuel-core's `PthTensors`
663/// uses it to materialize the actual `Tensor`.
664#[derive(Debug, Clone)]
665pub struct TensorInfo {
666    /// The parameter name (e.g. `"model.layers.0.weight"`).
667    pub name: String,
668    /// The element data type.
669    pub dtype: DType,
670    /// Shape, stride, and storage offset.
671    pub layout: Layout,
672    /// Zip-internal path to the raw storage file.
673    pub path: String,
674    /// Number of elements in the storage (may be larger than the tensor if storage is shared).
675    pub storage_size: usize,
676}
677
678/// Parse PyTorch `.pth` archive metadata, returning one [`TensorInfo`]
679/// per tensor in the embedded `state_dict`.
680///
681/// Walks every `data.pkl` entry in the zip archive, runs the pickle VM
682/// to recover the top-level Python object, optionally drills into a
683/// `key` (e.g. `"state_dict"`), then extracts a [`TensorInfo`] from
684/// each `_rebuild_tensor_v2` call.
685pub fn read_pth_tensor_info<P: AsRef<std::path::Path>>(
686    file: P,
687    verbose: bool,
688    key: Option<&str>,
689) -> Result<Vec<TensorInfo>> {
690    let file = std::fs::File::open(file)?;
691    let zip_reader = std::io::BufReader::new(file);
692    let mut zip = zip::ZipArchive::new(zip_reader)?;
693    let zip_file_names = zip
694        .file_names()
695        .map(|f| f.map(|name| name.to_string()))
696        .collect::<std::result::Result<Vec<String>, _>>()?;
697
698    let mut tensor_infos = vec![];
699    for file_name in zip_file_names.iter() {
700        if !file_name.ends_with("data.pkl") {
701            continue;
702        }
703        let dir_name = std::path::PathBuf::from(file_name.strip_suffix(".pkl").context("no .pkl")?);
704        let reader = zip.by_name(file_name)?;
705        let mut reader = std::io::BufReader::new(reader);
706        let mut stack = Stack::empty();
707        stack.read_loop(&mut reader)?;
708        let obj = stack.finalize()?;
709        if VERBOSE || verbose {
710            println!("{obj:#?}");
711        }
712
713        let obj = match obj {
714            Object::Build { callable, args } => match *callable {
715                Object::Reduce { callable, args: _ } => match *callable {
716                    Object::Class {
717                        module_name,
718                        class_name,
719                    } if module_name == "__torch__" && class_name == "Module" => *args,
720                    _ => continue,
721                },
722                _ => continue,
723            },
724            obj => obj,
725        };
726
727        // If key is provided, extract the state_dict from the object.
728        let obj = if let Some(key) = key {
729            if let Object::Dict(key_values) = obj {
730                key_values
731                    .into_iter()
732                    .find(|(k, _)| *k == Object::Unicode(key.to_owned()))
733                    .map(|(_, v)| v)
734                    .ok_or_else(|| E::Msg(format!("key {key} not found")))?
735            } else {
736                obj
737            }
738        } else {
739            obj
740        };
741
742        // Assuming `obj` is now state_dict.
743        if let Object::Dict(key_values) = obj {
744            for (name, value) in key_values.into_iter() {
745                match value.into_tensor_info(name, &dir_name) {
746                    Ok(Some(tensor_info)) => tensor_infos.push(tensor_info),
747                    Ok(None) => {}
748                    Err(err) => eprintln!("skipping: {err:?}"),
749                }
750            }
751        }
752    }
753    Ok(tensor_infos)
754}