987 lines
32 KiB
C
987 lines
32 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* PyTorch .pth file reader
|
|
*
|
|
* A .pth file is a ZIP archive containing:
|
|
* archive/data.pkl - Pickle: OrderedDict mapping tensor names -> storage refs
|
|
* archive/data/0 - Raw bytes for storage #0
|
|
* archive/data/1 - Raw bytes for storage #1
|
|
* ...
|
|
*
|
|
* Three sub-components:
|
|
* 1. ZIP reader - parse ZIP central directory, locate entries
|
|
* 2. Pickle parser - interpret pickle opcodes to extract tensor metadata
|
|
* 3. Storage resolver - map storage IDs to ZIP entries, populate pt_tensor structs
|
|
*/
|
|
|
|
#include "pth/pth.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
/* ============================================================= */
|
|
/* ZIP reader */
|
|
/* ============================================================= */
|
|
|
|
/* ZIP signatures */
|
|
#define ZIP_EOCD_SIG 0x06054b50
|
|
#define ZIP_CD_SIG 0x02014b50
|
|
#define ZIP_LF_SIG 0x04034b50
|
|
|
|
typedef struct {
|
|
char name[512];
|
|
uint32_t compressed_size;
|
|
uint32_t uncompressed_size;
|
|
uint32_t local_header_offset;
|
|
const uint8_t *data; /* pointer to actual file data (after local header) */
|
|
size_t data_size;
|
|
} zip_entry;
|
|
|
|
typedef struct {
|
|
uint8_t *base; /* mmap'd file */
|
|
size_t file_size;
|
|
zip_entry *entries;
|
|
int n_entries;
|
|
} zip_archive;
|
|
|
|
static uint16_t read_u16(const uint8_t *p) {
|
|
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
|
}
|
|
|
|
static uint32_t read_u32(const uint8_t *p) {
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
|
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
|
}
|
|
|
|
static zip_archive *zip_open(const char *path) {
|
|
int fd = open(path, O_RDONLY);
|
|
if (fd < 0) return NULL;
|
|
|
|
struct stat st;
|
|
if (fstat(fd, &st) < 0) { close(fd); return NULL; }
|
|
size_t file_size = (size_t)st.st_size;
|
|
|
|
if (file_size < 22) { close(fd); return NULL; } /* min EOCD size */
|
|
|
|
uint8_t *base = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
close(fd);
|
|
if (base == MAP_FAILED) return NULL;
|
|
|
|
/* Find End of Central Directory (scan backwards) */
|
|
const uint8_t *eocd = NULL;
|
|
size_t search_start = file_size > 65557 ? file_size - 65557 : 0;
|
|
for (size_t i = file_size - 22; i >= search_start; i--) {
|
|
if (read_u32(base + i) == ZIP_EOCD_SIG) {
|
|
eocd = base + i;
|
|
break;
|
|
}
|
|
if (i == 0) break;
|
|
}
|
|
if (!eocd) { munmap(base, file_size); return NULL; }
|
|
|
|
uint16_t n_entries = read_u16(eocd + 10);
|
|
uint32_t cd_offset = read_u32(eocd + 16);
|
|
|
|
if (cd_offset >= file_size) { munmap(base, file_size); return NULL; }
|
|
|
|
/* Parse central directory */
|
|
zip_entry *entries = calloc(n_entries, sizeof(zip_entry));
|
|
if (!entries) { munmap(base, file_size); return NULL; }
|
|
|
|
const uint8_t *p = base + cd_offset;
|
|
for (int i = 0; i < n_entries; i++) {
|
|
if (p + 46 > base + file_size) goto fail;
|
|
if (read_u32(p) != ZIP_CD_SIG) goto fail;
|
|
|
|
uint16_t name_len = read_u16(p + 28);
|
|
uint16_t extra_len = read_u16(p + 30);
|
|
uint16_t comment_len = read_u16(p + 32);
|
|
entries[i].compressed_size = read_u32(p + 20);
|
|
entries[i].uncompressed_size = read_u32(p + 24);
|
|
entries[i].local_header_offset = read_u32(p + 42);
|
|
|
|
if (p + 46 + name_len > base + file_size) goto fail;
|
|
size_t copy_len = name_len < 511 ? name_len : 511;
|
|
memcpy(entries[i].name, p + 46, copy_len);
|
|
entries[i].name[copy_len] = '\0';
|
|
|
|
/* Resolve data pointer from local file header */
|
|
uint32_t lf_off = entries[i].local_header_offset;
|
|
if (lf_off + 30 <= file_size) {
|
|
const uint8_t *lf = base + lf_off;
|
|
if (read_u32(lf) == ZIP_LF_SIG) {
|
|
uint16_t lf_name_len = read_u16(lf + 26);
|
|
uint16_t lf_extra_len = read_u16(lf + 28);
|
|
size_t data_off = lf_off + 30 + lf_name_len + lf_extra_len;
|
|
if (data_off <= file_size) {
|
|
entries[i].data = base + data_off;
|
|
entries[i].data_size = entries[i].uncompressed_size;
|
|
}
|
|
}
|
|
}
|
|
|
|
p += 46 + name_len + extra_len + comment_len;
|
|
}
|
|
|
|
zip_archive *z = malloc(sizeof(zip_archive));
|
|
if (!z) goto fail;
|
|
z->base = base;
|
|
z->file_size = file_size;
|
|
z->entries = entries;
|
|
z->n_entries = n_entries;
|
|
return z;
|
|
|
|
fail:
|
|
free(entries);
|
|
munmap(base, file_size);
|
|
return NULL;
|
|
}
|
|
|
|
static const zip_entry *zip_find(const zip_archive *z, const char *name) {
|
|
for (int i = 0; i < z->n_entries; i++) {
|
|
if (strcmp(z->entries[i].name, name) == 0) return &z->entries[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Find entry by suffix (e.g. "data.pkl" matches "archive/data.pkl") */
|
|
static const zip_entry *zip_find_suffix(const zip_archive *z, const char *suffix) {
|
|
size_t slen = strlen(suffix);
|
|
for (int i = 0; i < z->n_entries; i++) {
|
|
size_t nlen = strlen(z->entries[i].name);
|
|
if (nlen >= slen && strcmp(z->entries[i].name + nlen - slen, suffix) == 0)
|
|
return &z->entries[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void zip_close(zip_archive *z) {
|
|
if (!z) return;
|
|
munmap(z->base, z->file_size);
|
|
free(z->entries);
|
|
free(z);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Pickle parser */
|
|
/* ============================================================= */
|
|
|
|
/* Pickle opcodes we need */
|
|
#define PK_PROTO 0x80
|
|
#define PK_FRAME 0x95
|
|
#define PK_STOP 0x2e /* . */
|
|
#define PK_MARK 0x28 /* ( */
|
|
#define PK_POP 0x30 /* 0 */
|
|
#define PK_DUP 0x32 /* 2 */
|
|
#define PK_NONE 0x4e /* N */
|
|
#define PK_NEWTRUE 0x88
|
|
#define PK_NEWFALSE 0x89
|
|
#define PK_EMPTY_TUPLE 0x29 /* ) */
|
|
#define PK_TUPLE1 0x85
|
|
#define PK_TUPLE2 0x86
|
|
#define PK_TUPLE3 0x87
|
|
#define PK_TUPLE 0x74 /* t */
|
|
#define PK_EMPTY_LIST 0x5d /* ] */
|
|
#define PK_EMPTY_DICT 0x7d /* } */
|
|
#define PK_APPEND 0x61 /* a */
|
|
#define PK_APPENDS 0x65 /* e */
|
|
#define PK_SETITEM 0x73 /* s */
|
|
#define PK_SETITEMS 0x75 /* u */
|
|
#define PK_SHORT_BINUNICODE 0x8c
|
|
#define PK_BINUNICODE 0x58 /* X */
|
|
#define PK_BININT1 0x4b /* K */
|
|
#define PK_BININT2 0x4d /* M */
|
|
#define PK_BININT 0x4a /* J */
|
|
#define PK_LONG1 0x8a
|
|
#define PK_BINFLOAT 0x47 /* G */
|
|
#define PK_BINPERSID 0x51 /* Q */
|
|
#define PK_REDUCE 0x52 /* R */
|
|
#define PK_GLOBAL 0x63 /* c */
|
|
#define PK_STACK_GLOBAL 0x93
|
|
#define PK_BUILD 0x62 /* b */
|
|
#define PK_SHORT_BINSTRING 0x55 /* U */
|
|
#define PK_BINGET 0x68 /* h */
|
|
#define PK_LONG_BINGET 0x6a /* j */
|
|
#define PK_BINPUT 0x71 /* q */
|
|
#define PK_LONG_BINPUT 0x72 /* r */
|
|
#define PK_MEMOIZE 0x94
|
|
#define PK_SHORT_BINBYTES 0x42 /* B */
|
|
#define PK_BINBYTES 0x43 /* C */
|
|
|
|
/* Pickle stack item types */
|
|
enum pk_type {
|
|
PK_T_NONE,
|
|
PK_T_BOOL,
|
|
PK_T_INT,
|
|
PK_T_FLOAT,
|
|
PK_T_STRING,
|
|
PK_T_TUPLE,
|
|
PK_T_LIST,
|
|
PK_T_DICT,
|
|
PK_T_GLOBAL, /* module.class reference */
|
|
PK_T_PERSID, /* persistent ID (storage reference) */
|
|
PK_T_MARK,
|
|
PK_T_BYTES,
|
|
};
|
|
|
|
typedef struct pk_item pk_item;
|
|
|
|
struct pk_item {
|
|
enum pk_type type;
|
|
union {
|
|
int64_t ival;
|
|
double fval;
|
|
char sval[512];
|
|
struct {
|
|
pk_item *items;
|
|
int count;
|
|
} seq;
|
|
};
|
|
};
|
|
|
|
/* Extracted tensor info from pickle */
|
|
typedef struct {
|
|
char name[256];
|
|
char storage_class[64]; /* e.g. "BFloat16Storage" */
|
|
int64_t storage_id; /* numeric ID */
|
|
char storage_key[64]; /* string key like "0", "1", etc. */
|
|
int64_t shape[8];
|
|
int ndim;
|
|
int64_t offset; /* byte offset into storage */
|
|
} pkl_tensor_info;
|
|
|
|
typedef struct {
|
|
pkl_tensor_info *tensors;
|
|
int n_tensors;
|
|
int cap_tensors;
|
|
} pkl_result;
|
|
|
|
#define PK_STACK_MAX 2048
|
|
#define PK_MEMO_MAX 1024
|
|
|
|
typedef struct {
|
|
pk_item stack[PK_STACK_MAX];
|
|
int sp;
|
|
pk_item memo[PK_MEMO_MAX];
|
|
int memo_used[PK_MEMO_MAX];
|
|
pkl_result result;
|
|
const uint8_t *data;
|
|
size_t size;
|
|
size_t pos;
|
|
} pk_state;
|
|
|
|
static void pk_push(pk_state *s, pk_item item) {
|
|
if (s->sp < PK_STACK_MAX) s->stack[s->sp++] = item;
|
|
}
|
|
|
|
static pk_item pk_pop(pk_state *s) {
|
|
if (s->sp > 0) return s->stack[--s->sp];
|
|
pk_item empty = {0};
|
|
return empty;
|
|
}
|
|
|
|
static int pk_find_mark(pk_state *s) {
|
|
for (int i = s->sp - 1; i >= 0; i--) {
|
|
if (s->stack[i].type == PK_T_MARK) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void pkl_add_tensor(pkl_result *r, pkl_tensor_info *t) {
|
|
if (r->n_tensors >= r->cap_tensors) {
|
|
r->cap_tensors = r->cap_tensors ? r->cap_tensors * 2 : 64;
|
|
r->tensors = realloc(r->tensors, r->cap_tensors * sizeof(pkl_tensor_info));
|
|
}
|
|
r->tensors[r->n_tensors++] = *t;
|
|
}
|
|
|
|
/* Read a string from the pickle stream */
|
|
static void pk_read_string(pk_state *s, char *buf, size_t bufsize, size_t len) {
|
|
size_t copy = len < bufsize - 1 ? len : bufsize - 1;
|
|
if (s->pos + len <= s->size) {
|
|
memcpy(buf, s->data + s->pos, copy);
|
|
}
|
|
buf[copy] = '\0';
|
|
s->pos += len;
|
|
}
|
|
|
|
/* Process BINPERSID: top of stack is a tuple describing a storage.
|
|
* PyTorch persistent ID tuple layout:
|
|
* ("storage", torch.XxxStorage, "key", "device", size)
|
|
* Items: [0]=STRING "storage", [1]=GLOBAL torch.XxxStorage,
|
|
* [2]=STRING key ("0","1",...), [3]=STRING device, [4]=INT size
|
|
*
|
|
* We encode into PERSID.sval as "key:StorageClassName" */
|
|
static void pk_handle_persid(pk_state *s) {
|
|
pk_item pid = pk_pop(s);
|
|
pk_item result = {0};
|
|
result.type = PK_T_PERSID;
|
|
if (pid.type == PK_T_TUPLE && pid.seq.count >= 3) {
|
|
/* Find the GLOBAL (storage class) and the key string */
|
|
const char *storage_class = NULL;
|
|
const char *storage_key = NULL;
|
|
for (int i = 0; i < pid.seq.count; i++) {
|
|
if (pid.seq.items[i].type == PK_T_GLOBAL && !storage_class)
|
|
storage_class = pid.seq.items[i].sval;
|
|
else if (pid.seq.items[i].type == PK_T_STRING) {
|
|
/* Skip "storage" and "cpu"/"cuda" — key is the short numeric string */
|
|
const char *sv = pid.seq.items[i].sval;
|
|
if (strcmp(sv, "storage") != 0 && strcmp(sv, "cpu") != 0 &&
|
|
strncmp(sv, "cuda", 4) != 0 && !storage_key)
|
|
storage_key = sv;
|
|
}
|
|
}
|
|
if (storage_key && storage_class) {
|
|
snprintf(result.sval, sizeof(result.sval), "%s:%s",
|
|
storage_key, storage_class);
|
|
}
|
|
free(pid.seq.items);
|
|
}
|
|
pk_push(s, result);
|
|
}
|
|
|
|
|
|
/* The actual tensor extraction happens during BUILD/REDUCE when we see
|
|
* _rebuild_tensor_v2 calls. The dict gives us name -> storage mapping. */
|
|
|
|
|
|
static int pk_parse(pk_state *s) {
|
|
while (s->pos < s->size) {
|
|
uint8_t op = s->data[s->pos++];
|
|
|
|
switch (op) {
|
|
case PK_PROTO: {
|
|
if (s->pos >= s->size) return -1;
|
|
s->pos++; /* skip version byte */
|
|
break;
|
|
}
|
|
case PK_FRAME: {
|
|
s->pos += 8; /* skip frame length */
|
|
break;
|
|
}
|
|
case PK_STOP:
|
|
return 0;
|
|
|
|
case PK_MARK: {
|
|
pk_item m = {0};
|
|
m.type = PK_T_MARK;
|
|
pk_push(s, m);
|
|
break;
|
|
}
|
|
case PK_POP:
|
|
pk_pop(s);
|
|
break;
|
|
|
|
case PK_DUP:
|
|
if (s->sp > 0) pk_push(s, s->stack[s->sp - 1]);
|
|
break;
|
|
|
|
case PK_NONE: {
|
|
pk_item n = {0};
|
|
n.type = PK_T_NONE;
|
|
pk_push(s, n);
|
|
break;
|
|
}
|
|
case PK_NEWTRUE: {
|
|
pk_item b = {0};
|
|
b.type = PK_T_BOOL;
|
|
b.ival = 1;
|
|
pk_push(s, b);
|
|
break;
|
|
}
|
|
case PK_NEWFALSE: {
|
|
pk_item b = {0};
|
|
b.type = PK_T_BOOL;
|
|
b.ival = 0;
|
|
pk_push(s, b);
|
|
break;
|
|
}
|
|
|
|
case PK_EMPTY_TUPLE: {
|
|
pk_item t = {0};
|
|
t.type = PK_T_TUPLE;
|
|
t.seq.items = NULL;
|
|
t.seq.count = 0;
|
|
pk_push(s, t);
|
|
break;
|
|
}
|
|
case PK_TUPLE1: {
|
|
pk_item a = pk_pop(s);
|
|
pk_item t = {0};
|
|
t.type = PK_T_TUPLE;
|
|
t.seq.items = malloc(sizeof(pk_item));
|
|
t.seq.count = 1;
|
|
t.seq.items[0] = a;
|
|
pk_push(s, t);
|
|
break;
|
|
}
|
|
case PK_TUPLE2: {
|
|
pk_item b = pk_pop(s);
|
|
pk_item a = pk_pop(s);
|
|
pk_item t = {0};
|
|
t.type = PK_T_TUPLE;
|
|
t.seq.items = malloc(2 * sizeof(pk_item));
|
|
t.seq.count = 2;
|
|
t.seq.items[0] = a;
|
|
t.seq.items[1] = b;
|
|
pk_push(s, t);
|
|
break;
|
|
}
|
|
case PK_TUPLE3: {
|
|
pk_item c = pk_pop(s);
|
|
pk_item b = pk_pop(s);
|
|
pk_item a = pk_pop(s);
|
|
pk_item t = {0};
|
|
t.type = PK_T_TUPLE;
|
|
t.seq.items = malloc(3 * sizeof(pk_item));
|
|
t.seq.count = 3;
|
|
t.seq.items[0] = a;
|
|
t.seq.items[1] = b;
|
|
t.seq.items[2] = c;
|
|
pk_push(s, t);
|
|
break;
|
|
}
|
|
case PK_TUPLE: {
|
|
int mark = pk_find_mark(s);
|
|
if (mark < 0) break;
|
|
int count = s->sp - mark - 1;
|
|
pk_item t = {0};
|
|
t.type = PK_T_TUPLE;
|
|
t.seq.count = count;
|
|
t.seq.items = count > 0 ? malloc(count * sizeof(pk_item)) : NULL;
|
|
for (int i = 0; i < count; i++)
|
|
t.seq.items[i] = s->stack[mark + 1 + i];
|
|
s->sp = mark;
|
|
pk_push(s, t);
|
|
break;
|
|
}
|
|
|
|
case PK_EMPTY_LIST: {
|
|
pk_item l = {0};
|
|
l.type = PK_T_LIST;
|
|
pk_push(s, l);
|
|
break;
|
|
}
|
|
case PK_EMPTY_DICT: {
|
|
pk_item d = {0};
|
|
d.type = PK_T_DICT;
|
|
pk_push(s, d);
|
|
break;
|
|
}
|
|
|
|
case PK_APPEND:
|
|
pk_pop(s); /* discard appended item, we don't need list contents */
|
|
break;
|
|
|
|
case PK_APPENDS: {
|
|
int mark = pk_find_mark(s);
|
|
if (mark >= 0) s->sp = mark; /* pop mark + all items above */
|
|
break;
|
|
}
|
|
|
|
case PK_SETITEM: {
|
|
pk_item val = pk_pop(s);
|
|
pk_item key = pk_pop(s);
|
|
/* If this is a state_dict entry, assign name to the last tensor */
|
|
if (key.type == PK_T_STRING && s->result.n_tensors > 0) {
|
|
pkl_tensor_info *last = &s->result.tensors[s->result.n_tensors - 1];
|
|
if (last->name[0] == '\0') {
|
|
strncpy(last->name, key.sval, sizeof(last->name) - 1);
|
|
}
|
|
}
|
|
(void)val;
|
|
break;
|
|
}
|
|
case PK_SETITEMS: {
|
|
int mark = pk_find_mark(s);
|
|
if (mark < 0) break;
|
|
/* Walk key-value pairs and assign tensor names.
|
|
* Stack between mark+1 and sp has pairs: key, value, key, value, ...
|
|
* But the stack may have overflowed (PK_STACK_MAX=256) if there are
|
|
* many tensors. In that case, items were silently dropped. */
|
|
int name_idx = 0;
|
|
for (int i = mark + 1; i + 1 < s->sp; i += 2) {
|
|
if (s->stack[i].type == PK_T_STRING) {
|
|
/* Find next unnamed tensor and assign */
|
|
for (int t = name_idx; t < s->result.n_tensors; t++) {
|
|
if (s->result.tensors[t].name[0] == '\0') {
|
|
strncpy(s->result.tensors[t].name, s->stack[i].sval,
|
|
sizeof(s->result.tensors[t].name) - 1);
|
|
name_idx = t + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
s->sp = mark; /* pop mark + all items */
|
|
break;
|
|
}
|
|
|
|
case PK_SHORT_BINUNICODE: {
|
|
if (s->pos >= s->size) return -1;
|
|
uint8_t len = s->data[s->pos++];
|
|
pk_item str = {0};
|
|
str.type = PK_T_STRING;
|
|
pk_read_string(s, str.sval, sizeof(str.sval), len);
|
|
pk_push(s, str);
|
|
break;
|
|
}
|
|
case PK_BINUNICODE: {
|
|
if (s->pos + 4 > s->size) return -1;
|
|
uint32_t len = read_u32(s->data + s->pos);
|
|
s->pos += 4;
|
|
pk_item str = {0};
|
|
str.type = PK_T_STRING;
|
|
pk_read_string(s, str.sval, sizeof(str.sval), len);
|
|
pk_push(s, str);
|
|
break;
|
|
}
|
|
case PK_SHORT_BINSTRING: {
|
|
if (s->pos >= s->size) return -1;
|
|
uint8_t len = s->data[s->pos++];
|
|
pk_item str = {0};
|
|
str.type = PK_T_STRING;
|
|
pk_read_string(s, str.sval, sizeof(str.sval), len);
|
|
pk_push(s, str);
|
|
break;
|
|
}
|
|
|
|
case PK_BININT1: {
|
|
if (s->pos >= s->size) return -1;
|
|
pk_item n = {0};
|
|
n.type = PK_T_INT;
|
|
n.ival = s->data[s->pos++];
|
|
pk_push(s, n);
|
|
break;
|
|
}
|
|
case PK_BININT2: {
|
|
if (s->pos + 2 > s->size) return -1;
|
|
pk_item n = {0};
|
|
n.type = PK_T_INT;
|
|
n.ival = read_u16(s->data + s->pos);
|
|
s->pos += 2;
|
|
pk_push(s, n);
|
|
break;
|
|
}
|
|
case PK_BININT: {
|
|
if (s->pos + 4 > s->size) return -1;
|
|
pk_item n = {0};
|
|
n.type = PK_T_INT;
|
|
int32_t v;
|
|
memcpy(&v, s->data + s->pos, 4);
|
|
n.ival = v;
|
|
s->pos += 4;
|
|
pk_push(s, n);
|
|
break;
|
|
}
|
|
case PK_LONG1: {
|
|
if (s->pos >= s->size) return -1;
|
|
uint8_t nbytes = s->data[s->pos++];
|
|
pk_item n = {0};
|
|
n.type = PK_T_INT;
|
|
n.ival = 0;
|
|
if (s->pos + nbytes <= s->size) {
|
|
/* Little-endian, possibly signed */
|
|
for (int i = 0; i < nbytes && i < 8; i++)
|
|
n.ival |= (int64_t)s->data[s->pos + i] << (i * 8);
|
|
/* Sign extend if negative */
|
|
if (nbytes > 0 && nbytes < 8 && (s->data[s->pos + nbytes - 1] & 0x80))
|
|
for (int i = nbytes; i < 8; i++)
|
|
n.ival |= (int64_t)0xFF << (i * 8);
|
|
}
|
|
s->pos += nbytes;
|
|
pk_push(s, n);
|
|
break;
|
|
}
|
|
case PK_BINFLOAT: {
|
|
if (s->pos + 8 > s->size) return -1;
|
|
pk_item f = {0};
|
|
f.type = PK_T_FLOAT;
|
|
/* Big-endian double */
|
|
uint8_t tmp[8];
|
|
for (int i = 0; i < 8; i++) tmp[i] = s->data[s->pos + 7 - i];
|
|
memcpy(&f.fval, tmp, 8);
|
|
s->pos += 8;
|
|
pk_push(s, f);
|
|
break;
|
|
}
|
|
|
|
case PK_SHORT_BINBYTES: {
|
|
if (s->pos >= s->size) return -1;
|
|
uint8_t len = s->data[s->pos++];
|
|
pk_item b = {0};
|
|
b.type = PK_T_BYTES;
|
|
pk_read_string(s, b.sval, sizeof(b.sval), len);
|
|
pk_push(s, b);
|
|
break;
|
|
}
|
|
case PK_BINBYTES: {
|
|
if (s->pos + 4 > s->size) return -1;
|
|
uint32_t len = read_u32(s->data + s->pos);
|
|
s->pos += 4;
|
|
pk_item b = {0};
|
|
b.type = PK_T_BYTES;
|
|
pk_read_string(s, b.sval, sizeof(b.sval), len);
|
|
pk_push(s, b);
|
|
break;
|
|
}
|
|
|
|
case PK_GLOBAL: {
|
|
/* Read "module\nclass\n" */
|
|
pk_item g = {0};
|
|
g.type = PK_T_GLOBAL;
|
|
char module[256] = {0};
|
|
char cls[256] = {0};
|
|
int mi = 0, ci = 0;
|
|
/* Read module name until newline */
|
|
while (s->pos < s->size && s->data[s->pos] != '\n' && mi < 255)
|
|
module[mi++] = (char)s->data[s->pos++];
|
|
if (s->pos < s->size) s->pos++; /* skip \n */
|
|
/* Read class name until newline */
|
|
while (s->pos < s->size && s->data[s->pos] != '\n' && ci < 255)
|
|
cls[ci++] = (char)s->data[s->pos++];
|
|
if (s->pos < s->size) s->pos++; /* skip \n */
|
|
snprintf(g.sval, sizeof(g.sval), "%s.%s", module, cls);
|
|
|
|
/* Security: only allow known torch/collections classes */
|
|
if (strstr(g.sval, "torch") == NULL &&
|
|
strstr(g.sval, "collections") == NULL &&
|
|
strstr(g.sval, "_codecs") == NULL) {
|
|
fprintf(stderr, "pth: blocked GLOBAL: %s\n", g.sval);
|
|
return -1;
|
|
}
|
|
pk_push(s, g);
|
|
break;
|
|
}
|
|
|
|
case PK_STACK_GLOBAL: {
|
|
/* Pop class name, pop module name, push global */
|
|
pk_item cls = pk_pop(s);
|
|
pk_item mod = pk_pop(s);
|
|
pk_item g = {0};
|
|
g.type = PK_T_GLOBAL;
|
|
if (mod.type == PK_T_STRING && cls.type == PK_T_STRING)
|
|
snprintf(g.sval, sizeof(g.sval), "%s.%s", mod.sval, cls.sval);
|
|
pk_push(s, g);
|
|
break;
|
|
}
|
|
|
|
case PK_REDUCE: {
|
|
/* Pop args tuple, pop callable, call callable(args) */
|
|
pk_item args = pk_pop(s);
|
|
pk_item callable = pk_pop(s);
|
|
|
|
if (callable.type == PK_T_GLOBAL &&
|
|
strstr(callable.sval, "_rebuild_tensor_v2")) {
|
|
/* args is a tuple: (storage, offset, shape, stride, ...) */
|
|
/* storage is a PERSID, shape is a tuple of ints */
|
|
if (args.type == PK_T_TUPLE && args.seq.count >= 3) {
|
|
pkl_tensor_info ti = {0};
|
|
/* Name will be assigned later by SETITEM */
|
|
|
|
/* Storage info from persistent ID (item 0) */
|
|
pk_item *storage = &args.seq.items[0];
|
|
if (storage->type == PK_T_PERSID) {
|
|
/* sval is "key:torch.XxxStorage" */
|
|
char *colon = strchr(storage->sval, ':');
|
|
if (colon) {
|
|
*colon = '\0';
|
|
strncpy(ti.storage_key, storage->sval, sizeof(ti.storage_key) - 1);
|
|
strncpy(ti.storage_class, colon + 1, sizeof(ti.storage_class) - 1);
|
|
/* Extract just the storage class name */
|
|
char *dot = strrchr(ti.storage_class, '.');
|
|
if (dot) {
|
|
memmove(ti.storage_class, dot + 1, strlen(dot + 1) + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Offset (item 1) */
|
|
if (args.seq.items[1].type == PK_T_INT)
|
|
ti.offset = args.seq.items[1].ival;
|
|
|
|
/* Shape (item 2) - tuple of ints */
|
|
pk_item *shape = &args.seq.items[2];
|
|
if (shape->type == PK_T_TUPLE) {
|
|
ti.ndim = shape->seq.count < 8 ? shape->seq.count : 8;
|
|
for (int i = 0; i < ti.ndim; i++) {
|
|
if (shape->seq.items[i].type == PK_T_INT)
|
|
ti.shape[i] = shape->seq.items[i].ival;
|
|
}
|
|
}
|
|
|
|
pkl_add_tensor(&s->result, &ti);
|
|
}
|
|
/* Free tuple items */
|
|
if (args.type == PK_T_TUPLE && args.seq.items) {
|
|
for (int i = 0; i < args.seq.count; i++) {
|
|
if (args.seq.items[i].type == PK_T_TUPLE && args.seq.items[i].seq.items)
|
|
free(args.seq.items[i].seq.items);
|
|
}
|
|
free(args.seq.items);
|
|
}
|
|
} else if (callable.type == PK_T_GLOBAL &&
|
|
strstr(callable.sval, "OrderedDict")) {
|
|
/* OrderedDict() - push an empty dict */
|
|
pk_item d = {0};
|
|
d.type = PK_T_DICT;
|
|
pk_push(s, d);
|
|
if (args.type == PK_T_TUPLE && args.seq.items)
|
|
free(args.seq.items);
|
|
break;
|
|
} else {
|
|
/* Unknown REDUCE — push None */
|
|
if (args.type == PK_T_TUPLE && args.seq.items)
|
|
free(args.seq.items);
|
|
}
|
|
|
|
/* Push a placeholder result */
|
|
pk_item r = {0};
|
|
r.type = PK_T_NONE;
|
|
pk_push(s, r);
|
|
break;
|
|
}
|
|
|
|
case PK_BUILD: {
|
|
/* Pop state dict, pop obj, apply state to obj */
|
|
pk_item state = pk_pop(s);
|
|
/* Leave the object on top of stack */
|
|
(void)state;
|
|
break;
|
|
}
|
|
|
|
case PK_BINPERSID:
|
|
pk_handle_persid(s);
|
|
break;
|
|
|
|
case PK_BINPUT: {
|
|
if (s->pos >= s->size) return -1;
|
|
int idx = s->data[s->pos++];
|
|
if (idx < PK_MEMO_MAX && s->sp > 0) {
|
|
s->memo[idx] = s->stack[s->sp - 1];
|
|
s->memo_used[idx] = 1;
|
|
}
|
|
break;
|
|
}
|
|
case PK_LONG_BINPUT: {
|
|
if (s->pos + 4 > s->size) return -1;
|
|
uint32_t idx = read_u32(s->data + s->pos);
|
|
s->pos += 4;
|
|
if (idx < PK_MEMO_MAX && s->sp > 0) {
|
|
s->memo[idx] = s->stack[s->sp - 1];
|
|
s->memo_used[idx] = 1;
|
|
}
|
|
break;
|
|
}
|
|
case PK_BINGET: {
|
|
if (s->pos >= s->size) return -1;
|
|
int idx = s->data[s->pos++];
|
|
if (idx < PK_MEMO_MAX && s->memo_used[idx])
|
|
pk_push(s, s->memo[idx]);
|
|
else {
|
|
pk_item n = {0};
|
|
n.type = PK_T_NONE;
|
|
pk_push(s, n);
|
|
}
|
|
break;
|
|
}
|
|
case PK_LONG_BINGET: {
|
|
if (s->pos + 4 > s->size) return -1;
|
|
uint32_t idx = read_u32(s->data + s->pos);
|
|
s->pos += 4;
|
|
if (idx < PK_MEMO_MAX && s->memo_used[idx])
|
|
pk_push(s, s->memo[idx]);
|
|
else {
|
|
pk_item n = {0};
|
|
n.type = PK_T_NONE;
|
|
pk_push(s, n);
|
|
}
|
|
break;
|
|
}
|
|
case PK_MEMOIZE: {
|
|
/* Memoize top of stack at next memo index */
|
|
/* Find next free slot */
|
|
for (int i = 0; i < PK_MEMO_MAX; i++) {
|
|
if (!s->memo_used[i]) {
|
|
if (s->sp > 0) {
|
|
s->memo[i] = s->stack[s->sp - 1];
|
|
s->memo_used[i] = 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fprintf(stderr, "pth: unknown pickle opcode 0x%02x at offset %zu\n",
|
|
op, s->pos - 1);
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Storage resolver */
|
|
/* ============================================================= */
|
|
|
|
static fs_dtype storage_class_to_dtype(const char *cls) {
|
|
if (strstr(cls, "BFloat16")) return FS_DTYPE_BF16;
|
|
if (strstr(cls, "Float")) return FS_DTYPE_F32;
|
|
if (strstr(cls, "Half")) return FS_DTYPE_F16;
|
|
if (strstr(cls, "Double")) return FS_DTYPE_F64;
|
|
if (strstr(cls, "Byte")) return FS_DTYPE_U8;
|
|
if (strstr(cls, "Char")) return FS_DTYPE_I8;
|
|
if (strstr(cls, "Short")) return FS_DTYPE_I16;
|
|
if (strstr(cls, "Int")) return FS_DTYPE_I32;
|
|
if (strstr(cls, "Long")) return FS_DTYPE_I64;
|
|
return FS_DTYPE_UNKNOWN;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Public API */
|
|
/* ============================================================= */
|
|
|
|
struct pt_file {
|
|
zip_archive *zip;
|
|
pt_tensor *tensors;
|
|
int n_tensors;
|
|
};
|
|
|
|
pt_file *pt_open(const char *path) {
|
|
if (!path) return NULL;
|
|
|
|
zip_archive *zip = zip_open(path);
|
|
if (!zip) return NULL;
|
|
|
|
/* Find the pickle file */
|
|
const zip_entry *pkl_entry = zip_find_suffix(zip, "data.pkl");
|
|
if (!pkl_entry || !pkl_entry->data) {
|
|
zip_close(zip);
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse pickle */
|
|
pk_state *pks = calloc(1, sizeof(pk_state));
|
|
if (!pks) { zip_close(zip); return NULL; }
|
|
pks->data = pkl_entry->data;
|
|
pks->size = pkl_entry->data_size;
|
|
|
|
/* Single-pass parse: REDUCE adds tensors (unnamed), SETITEM/SETITEMS
|
|
* assigns names retroactively from the dict key strings. */
|
|
if (pk_parse(pks) < 0) {
|
|
free(pks->result.tensors);
|
|
free(pks);
|
|
zip_close(zip);
|
|
return NULL;
|
|
}
|
|
|
|
pkl_result *pkl = &pks->result;
|
|
|
|
/* Resolve storages: map each tensor's storage_key to a ZIP entry */
|
|
pt_file *f = malloc(sizeof(pt_file));
|
|
if (!f) {
|
|
free(pkl->tensors);
|
|
free(pks);
|
|
zip_close(zip);
|
|
return NULL;
|
|
}
|
|
|
|
f->zip = zip;
|
|
f->n_tensors = pkl->n_tensors;
|
|
f->tensors = calloc(pkl->n_tensors, sizeof(pt_tensor));
|
|
if (!f->tensors) {
|
|
free(pkl->tensors);
|
|
free(pks);
|
|
free(f);
|
|
zip_close(zip);
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = 0; i < pkl->n_tensors; i++) {
|
|
pkl_tensor_info *ti = &pkl->tensors[i];
|
|
pt_tensor *pt = &f->tensors[i];
|
|
|
|
strncpy(pt->name, ti->name, sizeof(pt->name) - 1);
|
|
pt->dtype = storage_class_to_dtype(ti->storage_class);
|
|
pt->ndim = ti->ndim;
|
|
memcpy(pt->shape, ti->shape, sizeof(pt->shape));
|
|
|
|
/* Compute total element count and data size */
|
|
size_t n_elements = 1;
|
|
for (int d = 0; d < pt->ndim; d++) n_elements *= (size_t)pt->shape[d];
|
|
size_t elem_size = fs_dtype_size(pt->dtype);
|
|
pt->data_size = n_elements * elem_size;
|
|
|
|
/* Find ZIP entry for this storage */
|
|
/* Try multiple naming patterns:
|
|
* "archive/data/KEY", "data/KEY", "archive/data.pkl/../data/KEY" */
|
|
char entry_name[512];
|
|
const zip_entry *ze = NULL;
|
|
|
|
/* Pattern 1: archive/data/KEY */
|
|
snprintf(entry_name, sizeof(entry_name), "archive/data/%s", ti->storage_key);
|
|
ze = zip_find(zip, entry_name);
|
|
|
|
if (!ze) {
|
|
/* Pattern 2: Try with prefix from pkl entry name */
|
|
/* e.g. if pkl is "RWKV-x070-World-0.1B.../data.pkl",
|
|
* storage is "RWKV-x070-World-0.1B.../data/0" */
|
|
const char *pkl_name = pkl_entry->name;
|
|
const char *pkl_suffix = strstr(pkl_name, "data.pkl");
|
|
if (pkl_suffix) {
|
|
size_t prefix_len = (size_t)(pkl_suffix - pkl_name);
|
|
snprintf(entry_name, sizeof(entry_name), "%.*sdata/%s",
|
|
(int)prefix_len, pkl_name, ti->storage_key);
|
|
ze = zip_find(zip, entry_name);
|
|
}
|
|
}
|
|
|
|
if (ze && ze->data) {
|
|
size_t byte_offset = (size_t)ti->offset * elem_size;
|
|
if (byte_offset + pt->data_size <= ze->data_size)
|
|
pt->data = ze->data + byte_offset;
|
|
else if (byte_offset < ze->data_size)
|
|
pt->data = ze->data + byte_offset;
|
|
}
|
|
}
|
|
|
|
free(pkl->tensors);
|
|
free(pks);
|
|
return f;
|
|
}
|
|
|
|
int pt_tensor_count(const pt_file *f) {
|
|
return f ? f->n_tensors : 0;
|
|
}
|
|
|
|
const pt_tensor *pt_tensor_at(const pt_file *f, int index) {
|
|
if (!f || index < 0 || index >= f->n_tensors) return NULL;
|
|
return &f->tensors[index];
|
|
}
|
|
|
|
const char **pt_tensor_names(const pt_file *f, int *count) {
|
|
if (!f) { if (count) *count = 0; return NULL; }
|
|
const char **names = malloc(f->n_tensors * sizeof(char *));
|
|
if (!names) { if (count) *count = 0; return NULL; }
|
|
for (int i = 0; i < f->n_tensors; i++)
|
|
names[i] = f->tensors[i].name;
|
|
if (count) *count = f->n_tensors;
|
|
return names;
|
|
}
|
|
|
|
void pt_close(pt_file *f) {
|
|
if (!f) return;
|
|
zip_close(f->zip);
|
|
free(f->tensors);
|
|
free(f);
|
|
}
|