1use anyhow::{anyhow, bail, Result};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TensorDType {
19 F32,
21 F64,
23 I64,
25}
26
27impl TensorDType {
28 pub fn elem_size(self) -> usize {
30 match self {
31 TensorDType::F32 => 4,
32 TensorDType::F64 => 8,
33 TensorDType::I64 => 8,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub enum TensorData {
41 F32(Vec<f32>),
43 F64(Vec<f64>),
45 I64(Vec<i64>),
47}
48
49#[derive(Debug, Clone)]
51pub struct PytorchTensor {
52 pub name: String,
54 pub dtype: TensorDType,
56 pub shape: Vec<usize>,
58 pub requires_grad: bool,
60 pub data: TensorData,
62}
63
64impl PytorchTensor {
65 pub fn element_count(&self) -> usize {
67 match &self.data {
68 TensorData::F32(v) => v.len(),
69 TensorData::F64(v) => v.len(),
70 TensorData::I64(v) => v.len(),
71 }
72 }
73}
74
75pub fn read_state_dict(file_bytes: &[u8]) -> Result<Vec<PytorchTensor>> {
79 if !(file_bytes.len() >= 4 && &file_bytes[0..4] == b"PK\x03\x04") {
80 bail!("not a zip-based PyTorch checkpoint (missing PK\\x03\\x04 signature); legacy pure-pickle .pt files are not supported");
81 }
82
83 let entries = read_stored_zip_entries(file_bytes)?;
84
85 let pickle = entries
87 .iter()
88 .find(|(name, _)| name.ends_with("data.pkl"))
89 .map(|(_, body)| body.clone())
90 .ok_or_else(|| anyhow!("checkpoint zip has no data.pkl entry"))?;
91
92 let mut storages: HashMap<String, Vec<u8>> = HashMap::new();
94 for (name, body) in &entries {
95 if let Some(pos) = name.rfind("/data/") {
96 let key = &name[pos + "/data/".len()..];
97 if !key.is_empty() && key.bytes().all(|b| b.is_ascii_digit()) {
98 storages.insert(key.to_string(), body.clone());
99 }
100 }
101 }
102
103 let specs = run_pickle(&pickle)?;
104
105 let mut tensors = Vec::with_capacity(specs.len());
106 for spec in specs {
107 let storage = storages.get(&spec.storage_key).ok_or_else(|| {
108 anyhow!(
109 "state_dict references storage '{}' that is not present in the archive",
110 spec.storage_key
111 )
112 })?;
113 tensors.push(reconstruct_tensor(spec, storage)?);
114 }
115
116 Ok(tensors)
117}
118
119fn read_stored_zip_entries(data: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
125 const LOCAL_SIG: &[u8] = b"PK\x03\x04";
126 let mut entries = Vec::new();
127 let mut cursor = 0usize;
128
129 while cursor + 30 <= data.len() && &data[cursor..cursor + 4] == LOCAL_SIG {
130 let method = u16::from_le_bytes([data[cursor + 8], data[cursor + 9]]);
131 let comp_size = u32::from_le_bytes([
132 data[cursor + 18],
133 data[cursor + 19],
134 data[cursor + 20],
135 data[cursor + 21],
136 ]) as usize;
137 let uncomp_size = u32::from_le_bytes([
138 data[cursor + 22],
139 data[cursor + 23],
140 data[cursor + 24],
141 data[cursor + 25],
142 ]) as usize;
143 let name_len = u16::from_le_bytes([data[cursor + 26], data[cursor + 27]]) as usize;
144 let extra_len = u16::from_le_bytes([data[cursor + 28], data[cursor + 29]]) as usize;
145
146 let name_start = cursor + 30;
147 let name_end = name_start + name_len;
148 if name_end > data.len() {
149 bail!("corrupt zip: entry name runs past end of file");
150 }
151 let name = String::from_utf8_lossy(&data[name_start..name_end]).to_string();
152
153 let body_start = name_end + extra_len;
154 let body_end = body_start + comp_size;
155 if body_end > data.len() {
156 bail!("corrupt zip: entry '{}' body runs past end of file", name);
157 }
158
159 if !name.ends_with('/') {
161 if method != 0 {
162 bail!(
163 "zip entry '{}' uses compression method {} but only STORED (0) is supported; \
164 torch.save archives are stored uncompressed",
165 name,
166 method
167 );
168 }
169 if comp_size != uncomp_size {
170 bail!("zip entry '{}' has inconsistent STORED sizes", name);
171 }
172 entries.push((name, data[body_start..body_end].to_vec()));
173 }
174
175 cursor = body_end;
176 }
177
178 if entries.is_empty() {
179 bail!("no readable STORED zip entries found");
180 }
181 Ok(entries)
182}
183
184#[derive(Debug, Clone)]
190struct TensorSpec {
191 name: String,
192 storage_key: String,
193 dtype: TensorDType,
194 storage_offset: usize,
195 size: Vec<usize>,
196 stride: Vec<usize>,
197 requires_grad: bool,
198}
199
200#[derive(Debug, Clone)]
202enum Obj {
203 None,
204 Bool(bool),
205 Int(i64),
206 Str(String),
207 Tuple(Vec<Obj>),
208 List(Vec<Obj>),
209 Dict(Vec<(Obj, Obj)>),
210 Global(String),
211 Mark,
212 Storage {
214 dtype: TensorDType,
215 key: String,
216 },
217 Tensor(TensorSpecPartial),
219 Opaque,
221}
222
223#[derive(Debug, Clone)]
225struct TensorSpecPartial {
226 storage_key: String,
227 dtype: TensorDType,
228 storage_offset: usize,
229 size: Vec<usize>,
230 stride: Vec<usize>,
231 requires_grad: bool,
232}
233
234fn run_pickle(buf: &[u8]) -> Result<Vec<TensorSpec>> {
236 let mut stack: Vec<Obj> = Vec::new();
237 let mut memo: HashMap<u32, Obj> = HashMap::new();
238 let mut pos = 0usize;
239 let mut memo_counter: u32 = 0;
240
241 macro_rules! need {
242 ($n:expr) => {{
243 if pos + $n > buf.len() {
244 bail!("pickle stream truncated");
245 }
246 }};
247 }
248
249 let top: Obj = loop {
250 if pos >= buf.len() {
251 bail!("pickle stream ended without STOP");
252 }
253 let op = buf[pos];
254 pos += 1;
255
256 match op {
257 0x80 => {
258 need!(1);
260 pos += 1;
261 }
262 0x95 => {
263 need!(8);
265 pos += 8;
266 }
267 b'(' => stack.push(Obj::Mark), b'.' => {
269 break stack
271 .pop()
272 .ok_or_else(|| anyhow!("STOP with empty stack"))?;
273 }
274 b'0' => {
275 stack.pop();
277 }
278 b'}' => stack.push(Obj::Dict(Vec::new())), b']' => stack.push(Obj::List(Vec::new())), b')' => stack.push(Obj::Tuple(Vec::new())), b'N' => stack.push(Obj::None), 0x88 => stack.push(Obj::Bool(true)), 0x89 => stack.push(Obj::Bool(false)), b'K' => {
285 need!(1);
287 stack.push(Obj::Int(buf[pos] as i64));
288 pos += 1;
289 }
290 b'M' => {
291 need!(2);
293 stack.push(Obj::Int(u16::from_le_bytes([buf[pos], buf[pos + 1]]) as i64));
294 pos += 2;
295 }
296 b'J' => {
297 need!(4);
299 stack.push(Obj::Int(i32::from_le_bytes([
300 buf[pos],
301 buf[pos + 1],
302 buf[pos + 2],
303 buf[pos + 3],
304 ]) as i64));
305 pos += 4;
306 }
307 0x8a => {
308 need!(1);
310 let n = buf[pos] as usize;
311 pos += 1;
312 need!(n);
313 stack.push(Obj::Int(read_signed_le(&buf[pos..pos + n])));
314 pos += n;
315 }
316 b'G' => {
317 need!(8);
320 pos += 8;
321 stack.push(Obj::Opaque);
322 }
323 0x8c => {
324 need!(1);
326 let n = buf[pos] as usize;
327 pos += 1;
328 need!(n);
329 stack.push(Obj::Str(str_from(&buf[pos..pos + n])?));
330 pos += n;
331 }
332 b'X' => {
333 need!(4);
335 let n = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
336 as usize;
337 pos += 4;
338 need!(n);
339 stack.push(Obj::Str(str_from(&buf[pos..pos + n])?));
340 pos += n;
341 }
342 b'q' => {
343 need!(1);
345 let idx = buf[pos] as u32;
346 pos += 1;
347 let obj = clone_top(&stack)?;
348 memo.insert(idx, obj);
349 }
350 b'r' => {
351 need!(4);
353 let idx = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
354 pos += 4;
355 let obj = clone_top(&stack)?;
356 memo.insert(idx, obj);
357 }
358 0x94 => {
359 let obj = clone_top(&stack)?;
361 memo.insert(memo_counter, obj);
362 memo_counter += 1;
363 }
364 b'h' => {
365 need!(1);
367 let idx = buf[pos] as u32;
368 pos += 1;
369 stack.push(memo_get(&memo, idx)?);
370 }
371 b'j' => {
372 need!(4);
374 let idx = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
375 pos += 4;
376 stack.push(memo_get(&memo, idx)?);
377 }
378 b'c' => {
379 let module = read_line(buf, &mut pos)?;
381 let name = read_line(buf, &mut pos)?;
382 stack.push(Obj::Global(format!("{module}.{name}")));
383 }
384 0x93 => {
385 let name = pop_str(&mut stack)?;
387 let module = pop_str(&mut stack)?;
388 stack.push(Obj::Global(format!("{module}.{name}")));
389 }
390 0x85 => tuple_n(&mut stack, 1)?, 0x86 => tuple_n(&mut stack, 2)?, 0x87 => tuple_n(&mut stack, 3)?, b't' => {
394 let items = pop_to_mark(&mut stack)?;
396 stack.push(Obj::Tuple(items));
397 }
398 b'a' => {
399 let v = stack.pop().ok_or_else(|| anyhow!("APPEND underflow"))?;
401 if let Some(Obj::List(list)) = stack.last_mut() {
402 list.push(v);
403 } else {
404 bail!("APPEND on non-list");
405 }
406 }
407 b'e' => {
408 let items = pop_to_mark(&mut stack)?;
410 if let Some(Obj::List(list)) = stack.last_mut() {
411 list.extend(items);
412 } else {
413 bail!("APPENDS on non-list");
414 }
415 }
416 b's' => {
417 let value = stack.pop().ok_or_else(|| anyhow!("SETITEM underflow"))?;
419 let key = stack.pop().ok_or_else(|| anyhow!("SETITEM underflow"))?;
420 if let Some(Obj::Dict(d)) = stack.last_mut() {
421 d.push((key, value));
422 } else {
423 bail!("SETITEM on non-dict");
424 }
425 }
426 b'u' => {
427 let items = pop_to_mark(&mut stack)?;
429 if items.len() % 2 != 0 {
430 bail!("SETITEMS with odd number of items");
431 }
432 if let Some(Obj::Dict(d)) = stack.last_mut() {
433 let mut it = items.into_iter();
434 while let (Some(k), Some(v)) = (it.next(), it.next()) {
435 d.push((k, v));
436 }
437 } else {
438 bail!("SETITEMS on non-dict");
439 }
440 }
441 b'Q' => {
442 let pid = stack.pop().ok_or_else(|| anyhow!("BINPERSID underflow"))?;
444 stack.push(resolve_persid(pid)?);
445 }
446 b'R' => {
447 let args = stack.pop().ok_or_else(|| anyhow!("REDUCE underflow"))?;
449 let callable = stack.pop().ok_or_else(|| anyhow!("REDUCE underflow"))?;
450 stack.push(apply_reduce(callable, args)?);
451 }
452 b'b' => {
453 stack.pop().ok_or_else(|| anyhow!("BUILD underflow"))?;
455 }
456 other => bail!("unsupported pickle opcode: 0x{other:02x}"),
457 }
458 };
459
460 let mut specs = Vec::new();
462 collect_specs(&top, "", &mut specs)?;
463 if specs.is_empty() {
464 bail!("no tensors (via _rebuild_tensor_v2) were found in the checkpoint");
465 }
466 Ok(specs)
467}
468
469fn collect_specs(obj: &Obj, prefix: &str, out: &mut Vec<TensorSpec>) -> Result<()> {
472 match obj {
473 Obj::Dict(entries) => {
474 for (k, v) in entries {
475 let key = match k {
476 Obj::Str(s) => s.clone(),
477 Obj::Int(i) => i.to_string(),
478 _ => continue,
479 };
480 let path = if prefix.is_empty() {
481 key
482 } else {
483 format!("{prefix}.{key}")
484 };
485 collect_specs(v, &path, out)?;
486 }
487 Ok(())
488 }
489 Obj::Tensor(p) => {
490 out.push(TensorSpec {
491 name: prefix.to_string(),
492 storage_key: p.storage_key.clone(),
493 dtype: p.dtype,
494 storage_offset: p.storage_offset,
495 size: p.size.clone(),
496 stride: p.stride.clone(),
497 requires_grad: p.requires_grad,
498 });
499 Ok(())
500 }
501 _ => Ok(()),
502 }
503}
504
505fn resolve_persid(pid: Obj) -> Result<Obj> {
507 let items = match pid {
508 Obj::Tuple(items) => items,
509 _ => bail!("persistent id is not a tuple (unsupported checkpoint layout)"),
510 };
511 if items.len() < 3 {
512 bail!("persistent id tuple too short");
513 }
514 match &items[0] {
515 Obj::Str(s) if s == "storage" => {}
516 _ => bail!("unsupported persistent id kind (expected 'storage')"),
517 }
518 let dtype = match &items[1] {
519 Obj::Global(g) => storage_dtype(g)?,
520 _ => bail!("persistent id storage type is not a global"),
521 };
522 let key = match &items[2] {
523 Obj::Str(s) => s.clone(),
524 Obj::Int(i) => i.to_string(),
525 _ => bail!("persistent id storage key has unexpected type"),
526 };
527 Ok(Obj::Storage { dtype, key })
528}
529
530fn storage_dtype(global: &str) -> Result<TensorDType> {
532 match global {
533 "torch.FloatStorage" => Ok(TensorDType::F32),
534 "torch.DoubleStorage" => Ok(TensorDType::F64),
535 "torch.LongStorage" => Ok(TensorDType::I64),
536 other => bail!(
537 "unsupported storage type '{}': only FloatStorage, DoubleStorage and LongStorage are supported",
538 other
539 ),
540 }
541}
542
543fn apply_reduce(callable: Obj, args: Obj) -> Result<Obj> {
545 let name = match &callable {
546 Obj::Global(g) => g.as_str(),
547 _ => return Ok(Obj::Opaque),
548 };
549 match name {
550 "torch._utils._rebuild_tensor_v2" | "torch._utils._rebuild_tensor" => {
551 let a = match args {
552 Obj::Tuple(a) => a,
553 _ => bail!("_rebuild_tensor args are not a tuple"),
554 };
555 if a.len() < 4 {
556 bail!("_rebuild_tensor expects at least 4 args, got {}", a.len());
557 }
558 let (dtype, storage_key) = match &a[0] {
559 Obj::Storage { dtype, key } => (*dtype, key.clone()),
560 _ => bail!("_rebuild_tensor first arg is not a storage"),
561 };
562 let storage_offset = as_usize(&a[1])?;
563 let size = as_usize_tuple(&a[2])?;
564 let stride = as_usize_tuple(&a[3])?;
565 let requires_grad = matches!(a.get(4), Some(Obj::Bool(true)));
566 Ok(Obj::Tensor(TensorSpecPartial {
567 storage_key,
568 dtype,
569 storage_offset,
570 size,
571 stride,
572 requires_grad,
573 }))
574 }
575 "collections.OrderedDict" | "builtins.dict" | "__builtin__.dict" => {
577 Ok(Obj::Dict(Vec::new()))
578 }
579 _ => Ok(Obj::Opaque),
580 }
581}
582
583fn reconstruct_tensor(spec: TensorSpec, storage: &[u8]) -> Result<PytorchTensor> {
586 let elem = spec.dtype.elem_size();
587 if storage.len() % elem != 0 {
588 bail!(
589 "storage '{}' length {} is not a multiple of element size {}",
590 spec.storage_key,
591 storage.len(),
592 elem
593 );
594 }
595 let n_storage_elems = storage.len() / elem;
596 let numel: usize = spec.size.iter().product();
597
598 if spec.size.len() != spec.stride.len() {
599 bail!("tensor '{}' size/stride rank mismatch", spec.name);
600 }
601
602 let mut indices = vec![0usize; spec.size.len()];
605 let mut flat = Vec::with_capacity(numel);
606 for _ in 0..numel {
607 let mut storage_index = spec.storage_offset;
608 for (dim, &idx) in indices.iter().enumerate() {
609 storage_index += idx * spec.stride[dim];
610 }
611 if storage_index >= n_storage_elems {
612 bail!(
613 "tensor '{}' indexes element {} beyond storage of {} elements",
614 spec.name,
615 storage_index,
616 n_storage_elems
617 );
618 }
619 flat.push(storage_index);
620
621 for dim in (0..spec.size.len()).rev() {
623 indices[dim] += 1;
624 if indices[dim] < spec.size[dim] {
625 break;
626 }
627 indices[dim] = 0;
628 }
629 }
630
631 let data = match spec.dtype {
632 TensorDType::F32 => TensorData::F32(
633 flat.iter()
634 .map(|&i| {
635 let b = &storage[i * 4..i * 4 + 4];
636 f32::from_le_bytes([b[0], b[1], b[2], b[3]])
637 })
638 .collect(),
639 ),
640 TensorDType::F64 => TensorData::F64(
641 flat.iter()
642 .map(|&i| {
643 let b = &storage[i * 8..i * 8 + 8];
644 f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
645 })
646 .collect(),
647 ),
648 TensorDType::I64 => TensorData::I64(
649 flat.iter()
650 .map(|&i| {
651 let b = &storage[i * 8..i * 8 + 8];
652 i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
653 })
654 .collect(),
655 ),
656 };
657
658 Ok(PytorchTensor {
659 name: spec.name,
660 dtype: spec.dtype,
661 shape: spec.size,
662 requires_grad: spec.requires_grad,
663 data,
664 })
665}
666
667fn read_signed_le(bytes: &[u8]) -> i64 {
672 if bytes.is_empty() {
673 return 0;
674 }
675 let mut val: i64 = 0;
676 for (i, &b) in bytes.iter().enumerate() {
677 val |= (b as i64) << (8 * i);
678 }
679 let bits = bytes.len() * 8;
681 if bits < 64 && (bytes[bytes.len() - 1] & 0x80) != 0 {
682 val |= -1i64 << bits;
683 }
684 val
685}
686
687fn str_from(bytes: &[u8]) -> Result<String> {
688 String::from_utf8(bytes.to_vec()).map_err(|_| anyhow!("invalid UTF-8 in pickle string"))
689}
690
691fn read_line(buf: &[u8], pos: &mut usize) -> Result<String> {
692 let start = *pos;
693 while *pos < buf.len() && buf[*pos] != b'\n' {
694 *pos += 1;
695 }
696 if *pos >= buf.len() {
697 bail!("unterminated GLOBAL line in pickle stream");
698 }
699 let s = str_from(&buf[start..*pos])?;
700 *pos += 1; Ok(s)
702}
703
704fn clone_top(stack: &[Obj]) -> Result<Obj> {
705 stack
706 .last()
707 .cloned()
708 .ok_or_else(|| anyhow!("memoize on empty stack"))
709}
710
711fn memo_get(memo: &HashMap<u32, Obj>, idx: u32) -> Result<Obj> {
712 memo.get(&idx)
713 .cloned()
714 .ok_or_else(|| anyhow!("BINGET of unknown memo index {idx}"))
715}
716
717fn pop_str(stack: &mut Vec<Obj>) -> Result<String> {
718 match stack.pop() {
719 Some(Obj::Str(s)) => Ok(s),
720 _ => bail!("expected a string on the pickle stack"),
721 }
722}
723
724fn tuple_n(stack: &mut Vec<Obj>, n: usize) -> Result<()> {
725 if stack.len() < n {
726 bail!("TUPLE{n} underflow");
727 }
728 let items = stack.split_off(stack.len() - n);
729 stack.push(Obj::Tuple(items));
730 Ok(())
731}
732
733fn pop_to_mark(stack: &mut Vec<Obj>) -> Result<Vec<Obj>> {
734 let mut items = Vec::new();
735 while let Some(obj) = stack.pop() {
736 if matches!(obj, Obj::Mark) {
737 items.reverse();
738 return Ok(items);
739 }
740 items.push(obj);
741 }
742 bail!("no MARK found on the pickle stack")
743}
744
745fn as_usize(obj: &Obj) -> Result<usize> {
746 match obj {
747 Obj::Int(i) if *i >= 0 => Ok(*i as usize),
748 _ => bail!("expected a non-negative integer"),
749 }
750}
751
752fn as_usize_tuple(obj: &Obj) -> Result<Vec<usize>> {
753 match obj {
754 Obj::Tuple(items) => items.iter().map(as_usize).collect(),
755 _ => bail!("expected a tuple of integers (size/stride)"),
756 }
757}