Foundry/safetensors/safetensors.c

984 lines
27 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
#include "safetensors.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
/* ============================================================= */
/* Internal structures */
/* ============================================================= */
typedef struct {
char* name;
fs_dtype dtype;
int64_t* shape;
int ndim;
size_t offset_start;
size_t offset_end;
} tensor_entry;
struct fs_file {
void* data; /* Memory-mapped data or buffer */
size_t size; /* Total file size */
bool is_mmap; /* True if mmap'd, false if buffer */
int fd; /* File descriptor (if mmap'd) */
char* header_json; /* JSON header string */
size_t header_len; /* Header length */
size_t data_offset; /* Offset to tensor data */
tensor_entry* tensors;
int num_tensors;
char** metadata_keys;
char** metadata_values;
int num_metadata;
};
typedef struct {
char* name;
fs_dtype dtype;
int64_t* shape;
int ndim;
void* data;
size_t data_size;
} writer_tensor;
struct fs_writer {
writer_tensor* tensors;
int num_tensors;
int capacity;
char** metadata_keys;
char** metadata_values;
int num_metadata;
int metadata_capacity;
};
/* ============================================================= */
/* Dtype utilities */
/* ============================================================= */
const char* fs_dtype_name(fs_dtype dtype) {
switch (dtype) {
case FS_DTYPE_F32: return "F32";
case FS_DTYPE_F64: return "F64";
case FS_DTYPE_F16: return "F16";
case FS_DTYPE_BF16: return "BF16";
case FS_DTYPE_I8: return "I8";
case FS_DTYPE_I16: return "I16";
case FS_DTYPE_I32: return "I32";
case FS_DTYPE_I64: return "I64";
case FS_DTYPE_U8: return "U8";
case FS_DTYPE_U16: return "U16";
case FS_DTYPE_U32: return "U32";
case FS_DTYPE_U64: return "U64";
case FS_DTYPE_BOOL: return "BOOL";
default: return "UNKNOWN";
}
}
size_t fs_dtype_size(fs_dtype dtype) {
switch (dtype) {
case FS_DTYPE_F32: return 4;
case FS_DTYPE_F64: return 8;
case FS_DTYPE_F16: return 2;
case FS_DTYPE_BF16: return 2;
case FS_DTYPE_I8: return 1;
case FS_DTYPE_I16: return 2;
case FS_DTYPE_I32: return 4;
case FS_DTYPE_I64: return 8;
case FS_DTYPE_U8: return 1;
case FS_DTYPE_U16: return 2;
case FS_DTYPE_U32: return 4;
case FS_DTYPE_U64: return 8;
case FS_DTYPE_BOOL: return 1;
default: return 0;
}
}
fs_dtype fs_dtype_from_string(const char* s) {
if (!s) return FS_DTYPE_UNKNOWN;
if (strcmp(s, "F32") == 0) return FS_DTYPE_F32;
if (strcmp(s, "F64") == 0) return FS_DTYPE_F64;
if (strcmp(s, "F16") == 0) return FS_DTYPE_F16;
if (strcmp(s, "BF16") == 0) return FS_DTYPE_BF16;
if (strcmp(s, "I8") == 0) return FS_DTYPE_I8;
if (strcmp(s, "I16") == 0) return FS_DTYPE_I16;
if (strcmp(s, "I32") == 0) return FS_DTYPE_I32;
if (strcmp(s, "I64") == 0) return FS_DTYPE_I64;
if (strcmp(s, "U8") == 0) return FS_DTYPE_U8;
if (strcmp(s, "U16") == 0) return FS_DTYPE_U16;
if (strcmp(s, "U32") == 0) return FS_DTYPE_U32;
if (strcmp(s, "U64") == 0) return FS_DTYPE_U64;
if (strcmp(s, "BOOL") == 0) return FS_DTYPE_BOOL;
return FS_DTYPE_UNKNOWN;
}
/* ============================================================= */
/* Minimal JSON parser */
/* ============================================================= */
static const char* skip_whitespace(const char* p) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
return p;
}
static char* parse_string(const char** pp) {
const char* p = *pp;
if (*p != '"') return NULL;
p++;
const char* start = p;
while (*p && *p != '"') {
if (*p == '\\') p++; /* Skip escaped char */
if (*p) p++;
}
size_t len = p - start;
char* s = malloc(len + 1);
if (!s) return NULL;
/* Copy with escape handling */
size_t j = 0;
for (size_t i = 0; i < len; i++) {
if (start[i] == '\\' && i + 1 < len) {
i++;
switch (start[i]) {
case 'n': s[j++] = '\n'; break;
case 't': s[j++] = '\t'; break;
case 'r': s[j++] = '\r'; break;
case '"': s[j++] = '"'; break;
case '\\': s[j++] = '\\'; break;
default: s[j++] = start[i]; break;
}
} else {
s[j++] = start[i];
}
}
s[j] = '\0';
if (*p == '"') p++;
*pp = p;
return s;
}
static int64_t parse_int(const char** pp) {
const char* p = *pp;
int64_t sign = 1;
if (*p == '-') { sign = -1; p++; }
int64_t val = 0;
while (*p >= '0' && *p <= '9') {
val = val * 10 + (*p - '0');
p++;
}
*pp = p;
return sign * val;
}
static int64_t* parse_int_array(const char** pp, int* count) {
const char* p = *pp;
*count = 0;
if (*p != '[') return NULL;
p++;
p = skip_whitespace(p);
/* Count elements first */
const char* scan = p;
int n = 0;
int depth = 0;
while (*scan && !(*scan == ']' && depth == 0)) {
if (*scan == '[') depth++;
else if (*scan == ']') depth--;
else if (*scan == ',' && depth == 0) n++;
else if ((*scan >= '0' && *scan <= '9') || *scan == '-') {
if (depth == 0 && (scan == p || *(scan-1) == ',' || *(scan-1) == '[' ||
*(scan-1) == ' ' || *(scan-1) == '\t' || *(scan-1) == '\n')) {
/* Already counted by comma */
}
}
scan++;
}
/* Handle empty array */
if (*p == ']') {
*pp = p + 1;
return malloc(1); /* Non-null but zero elements */
}
/* Re-count properly */
n = 1;
scan = p;
while (*scan && *scan != ']') {
if (*scan == ',') n++;
scan++;
}
int64_t* arr = malloc(n * sizeof(int64_t));
if (!arr) return NULL;
for (int i = 0; i < n && *p && *p != ']'; i++) {
p = skip_whitespace(p);
arr[i] = parse_int(&p);
p = skip_whitespace(p);
if (*p == ',') p++;
}
p = skip_whitespace(p);
if (*p == ']') p++;
*pp = p;
*count = n;
return arr;
}
/* Skip a JSON value (string, number, array, object, bool, null) */
static void skip_value(const char** pp) {
const char* p = *pp;
p = skip_whitespace(p);
if (*p == '"') {
p++;
while (*p && *p != '"') {
if (*p == '\\') p++;
if (*p) p++;
}
if (*p == '"') p++;
} else if (*p == '[') {
int depth = 1;
p++;
while (*p && depth > 0) {
if (*p == '[') depth++;
else if (*p == ']') depth--;
else if (*p == '"') {
p++;
while (*p && *p != '"') {
if (*p == '\\') p++;
if (*p) p++;
}
}
if (*p) p++;
}
} else if (*p == '{') {
int depth = 1;
p++;
while (*p && depth > 0) {
if (*p == '{') depth++;
else if (*p == '}') depth--;
else if (*p == '"') {
p++;
while (*p && *p != '"') {
if (*p == '\\') p++;
if (*p) p++;
}
}
if (*p) p++;
}
} else {
/* Number, bool, null */
while (*p && *p != ',' && *p != '}' && *p != ']' &&
*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
p++;
}
}
*pp = p;
}
/* ============================================================= */
/* Header parsing */
/* ============================================================= */
static bool parse_header(fs_file* f) {
const char* p = f->header_json;
/* Count tensors and metadata */
int tensor_count = 0;
int meta_count = 0;
p = skip_whitespace(p);
if (*p != '{') return false;
p++;
/* First pass: count entries */
const char* scan = p;
while (*scan && *scan != '}') {
scan = skip_whitespace(scan);
if (*scan != '"') break;
char* key = parse_string(&scan);
if (!key) break;
scan = skip_whitespace(scan);
if (*scan == ':') scan++;
scan = skip_whitespace(scan);
if (strcmp(key, "__metadata__") == 0) {
/* Count metadata entries */
if (*scan == '{') {
scan++;
while (*scan && *scan != '}') {
scan = skip_whitespace(scan);
if (*scan != '"') break;
skip_value(&scan);
scan = skip_whitespace(scan);
if (*scan == ':') scan++;
skip_value(&scan);
scan = skip_whitespace(scan);
if (*scan == ',') scan++;
meta_count++;
}
if (*scan == '}') scan++;
}
} else {
tensor_count++;
skip_value(&scan);
}
free(key);
scan = skip_whitespace(scan);
if (*scan == ',') scan++;
}
/* Allocate storage */
f->tensors = calloc(tensor_count, sizeof(tensor_entry));
f->num_tensors = 0;
if (meta_count > 0) {
f->metadata_keys = calloc(meta_count, sizeof(char*));
f->metadata_values = calloc(meta_count, sizeof(char*));
}
f->num_metadata = 0;
/* Second pass: parse entries */
p = f->header_json;
p = skip_whitespace(p);
if (*p == '{') p++;
while (*p && *p != '}') {
p = skip_whitespace(p);
if (*p != '"') break;
char* key = parse_string(&p);
if (!key) break;
p = skip_whitespace(p);
if (*p == ':') p++;
p = skip_whitespace(p);
if (strcmp(key, "__metadata__") == 0) {
/* Parse metadata object */
if (*p == '{') {
p++;
while (*p && *p != '}') {
p = skip_whitespace(p);
if (*p != '"') break;
char* mkey = parse_string(&p);
p = skip_whitespace(p);
if (*p == ':') p++;
p = skip_whitespace(p);
char* mval = parse_string(&p);
if (mkey && mval && f->num_metadata < meta_count) {
f->metadata_keys[f->num_metadata] = mkey;
f->metadata_values[f->num_metadata] = mval;
f->num_metadata++;
} else {
free(mkey);
free(mval);
}
p = skip_whitespace(p);
if (*p == ',') p++;
}
if (*p == '}') p++;
}
free(key);
} else {
/* Parse tensor entry */
tensor_entry* t = &f->tensors[f->num_tensors];
t->name = key;
if (*p == '{') {
p++;
while (*p && *p != '}') {
p = skip_whitespace(p);
if (*p != '"') break;
char* field = parse_string(&p);
p = skip_whitespace(p);
if (*p == ':') p++;
p = skip_whitespace(p);
if (strcmp(field, "dtype") == 0) {
char* dtype_str = parse_string(&p);
t->dtype = fs_dtype_from_string(dtype_str);
free(dtype_str);
} else if (strcmp(field, "shape") == 0) {
t->shape = parse_int_array(&p, &t->ndim);
} else if (strcmp(field, "data_offsets") == 0) {
int count;
int64_t* offsets = parse_int_array(&p, &count);
if (offsets && count >= 2) {
t->offset_start = (size_t)offsets[0];
t->offset_end = (size_t)offsets[1];
}
free(offsets);
} else {
skip_value(&p);
}
free(field);
p = skip_whitespace(p);
if (*p == ',') p++;
}
if (*p == '}') p++;
}
f->num_tensors++;
}
p = skip_whitespace(p);
if (*p == ',') p++;
}
return true;
}
/* ============================================================= */
/* File operations */
/* ============================================================= */
fs_file* fs_open(const char* path) {
if (!path) return NULL;
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 size = st.st_size;
if (size < 8) {
close(fd);
return NULL;
}
void* data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return NULL;
}
fs_file* f = calloc(1, sizeof(fs_file));
if (!f) {
munmap(data, size);
close(fd);
return NULL;
}
f->data = data;
f->size = size;
f->is_mmap = true;
f->fd = fd;
/* Read header length (little-endian u64) */
const uint8_t* bytes = (const uint8_t*)data;
uint64_t header_len = 0;
for (int i = 0; i < 8; i++) {
header_len |= ((uint64_t)bytes[i]) << (i * 8);
}
if (8 + header_len > size) {
fs_close(f);
return NULL;
}
f->header_len = (size_t)header_len;
f->header_json = malloc(header_len + 1);
if (!f->header_json) {
fs_close(f);
return NULL;
}
memcpy(f->header_json, bytes + 8, header_len);
f->header_json[header_len] = '\0';
f->data_offset = 8 + header_len;
if (!parse_header(f)) {
fs_close(f);
return NULL;
}
return f;
}
fs_file* fs_open_buffer(const void* data, size_t size) {
if (!data || size < 8) return NULL;
fs_file* f = calloc(1, sizeof(fs_file));
if (!f) return NULL;
f->data = (void*)data; /* Note: not copying */
f->size = size;
f->is_mmap = false;
f->fd = -1;
/* Read header length */
const uint8_t* bytes = (const uint8_t*)data;
uint64_t header_len = 0;
for (int i = 0; i < 8; i++) {
header_len |= ((uint64_t)bytes[i]) << (i * 8);
}
if (8 + header_len > size) {
free(f);
return NULL;
}
f->header_len = (size_t)header_len;
f->header_json = malloc(header_len + 1);
if (!f->header_json) {
free(f);
return NULL;
}
memcpy(f->header_json, bytes + 8, header_len);
f->header_json[header_len] = '\0';
f->data_offset = 8 + header_len;
if (!parse_header(f)) {
free(f->header_json);
free(f);
return NULL;
}
return f;
}
void fs_close(fs_file* f) {
if (!f) return;
if (f->is_mmap && f->data) {
munmap(f->data, f->size);
}
if (f->fd >= 0) {
close(f->fd);
}
free(f->header_json);
for (int i = 0; i < f->num_tensors; i++) {
free(f->tensors[i].name);
free(f->tensors[i].shape);
}
free(f->tensors);
for (int i = 0; i < f->num_metadata; i++) {
free(f->metadata_keys[i]);
free(f->metadata_values[i]);
}
free(f->metadata_keys);
free(f->metadata_values);
free(f);
}
/* ============================================================= */
/* Tensor queries */
/* ============================================================= */
int fs_num_tensors(fs_file* f) {
return f ? f->num_tensors : 0;
}
const char** fs_tensor_names(fs_file* f, int* count) {
if (!f || !count) return NULL;
const char** names = malloc(f->num_tensors * sizeof(char*));
if (!names) return NULL;
for (int i = 0; i < f->num_tensors; i++) {
names[i] = f->tensors[i].name;
}
*count = f->num_tensors;
return names;
}
bool fs_has_tensor(fs_file* f, const char* name) {
if (!f || !name) return false;
for (int i = 0; i < f->num_tensors; i++) {
if (strcmp(f->tensors[i].name, name) == 0) {
return true;
}
}
return false;
}
static tensor_entry* find_tensor(fs_file* f, const char* name) {
if (!f || !name) return NULL;
for (int i = 0; i < f->num_tensors; i++) {
if (strcmp(f->tensors[i].name, name) == 0) {
return &f->tensors[i];
}
}
return NULL;
}
fs_tensor_info* fs_get_info(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t) return NULL;
fs_tensor_info* info = calloc(1, sizeof(fs_tensor_info));
if (!info) return NULL;
info->name = strdup(t->name);
info->dtype = t->dtype;
info->ndim = t->ndim;
if (t->ndim > 0) {
info->shape = malloc(t->ndim * sizeof(int64_t));
if (info->shape) {
memcpy(info->shape, t->shape, t->ndim * sizeof(int64_t));
}
}
info->data_offset = t->offset_start;
info->data_size = t->offset_end - t->offset_start;
return info;
}
void fs_free_info(fs_tensor_info* info) {
if (!info) return;
free(info->name);
free(info->shape);
free(info);
}
const void* fs_get_data(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t) return NULL;
return (const uint8_t*)f->data + f->data_offset + t->offset_start;
}
const float* fs_get_f32(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t || t->dtype != FS_DTYPE_F32) return NULL;
return (const float*)((const uint8_t*)f->data + f->data_offset + t->offset_start);
}
const double* fs_get_f64(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t || t->dtype != FS_DTYPE_F64) return NULL;
return (const double*)((const uint8_t*)f->data + f->data_offset + t->offset_start);
}
const int32_t* fs_get_i32(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t || t->dtype != FS_DTYPE_I32) return NULL;
return (const int32_t*)((const uint8_t*)f->data + f->data_offset + t->offset_start);
}
const int64_t* fs_get_i64(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t || t->dtype != FS_DTYPE_I64) return NULL;
return (const int64_t*)((const uint8_t*)f->data + f->data_offset + t->offset_start);
}
const uint8_t* fs_get_u8(fs_file* f, const char* name) {
tensor_entry* t = find_tensor(f, name);
if (!t || t->dtype != FS_DTYPE_U8) return NULL;
return (const uint8_t*)f->data + f->data_offset + t->offset_start;
}
/* ============================================================= */
/* Metadata */
/* ============================================================= */
const char* fs_get_metadata(fs_file* f, const char* key) {
if (!f || !key) return NULL;
for (int i = 0; i < f->num_metadata; i++) {
if (strcmp(f->metadata_keys[i], key) == 0) {
return f->metadata_values[i];
}
}
return NULL;
}
const char** fs_metadata_keys(fs_file* f, int* count) {
if (!f || !count) return NULL;
const char** keys = malloc(f->num_metadata * sizeof(char*));
if (!keys) return NULL;
for (int i = 0; i < f->num_metadata; i++) {
keys[i] = f->metadata_keys[i];
}
*count = f->num_metadata;
return keys;
}
/* ============================================================= */
/* Writer */
/* ============================================================= */
fs_writer* fs_writer_new(void) {
fs_writer* w = calloc(1, sizeof(fs_writer));
if (!w) return NULL;
w->capacity = 16;
w->tensors = calloc(w->capacity, sizeof(writer_tensor));
w->metadata_capacity = 8;
w->metadata_keys = calloc(w->metadata_capacity, sizeof(char*));
w->metadata_values = calloc(w->metadata_capacity, sizeof(char*));
return w;
}
void fs_writer_free(fs_writer* w) {
if (!w) return;
for (int i = 0; i < w->num_tensors; i++) {
free(w->tensors[i].name);
free(w->tensors[i].shape);
free(w->tensors[i].data);
}
free(w->tensors);
for (int i = 0; i < w->num_metadata; i++) {
free(w->metadata_keys[i]);
free(w->metadata_values[i]);
}
free(w->metadata_keys);
free(w->metadata_values);
free(w);
}
void fs_writer_add_metadata(fs_writer* w, const char* key, const char* value) {
if (!w || !key || !value) return;
if (w->num_metadata >= w->metadata_capacity) {
w->metadata_capacity *= 2;
w->metadata_keys = realloc(w->metadata_keys, w->metadata_capacity * sizeof(char*));
w->metadata_values = realloc(w->metadata_values, w->metadata_capacity * sizeof(char*));
}
w->metadata_keys[w->num_metadata] = strdup(key);
w->metadata_values[w->num_metadata] = strdup(value);
w->num_metadata++;
}
void fs_writer_add_tensor(fs_writer* w, const char* name, fs_dtype dtype,
const int64_t* shape, int ndim, const void* data) {
if (!w || !name || !data) return;
if (w->num_tensors >= w->capacity) {
w->capacity *= 2;
w->tensors = realloc(w->tensors, w->capacity * sizeof(writer_tensor));
}
writer_tensor* t = &w->tensors[w->num_tensors];
t->name = strdup(name);
t->dtype = dtype;
t->ndim = ndim;
if (ndim > 0) {
t->shape = malloc(ndim * sizeof(int64_t));
memcpy(t->shape, shape, ndim * sizeof(int64_t));
} else {
t->shape = NULL;
}
/* Calculate data size */
size_t elem_size = fs_dtype_size(dtype);
size_t numel = 1;
for (int i = 0; i < ndim; i++) {
numel *= shape[i];
}
t->data_size = numel * elem_size;
t->data = malloc(t->data_size);
memcpy(t->data, data, t->data_size);
w->num_tensors++;
}
void fs_writer_add_f32(fs_writer* w, const char* name,
const int64_t* shape, int ndim, const float* data) {
fs_writer_add_tensor(w, name, FS_DTYPE_F32, shape, ndim, data);
}
static char* build_header_json(fs_writer* w, size_t* offsets) {
/* Estimate size needed */
size_t size = 256;
for (int i = 0; i < w->num_tensors; i++) {
size += strlen(w->tensors[i].name) + 128;
}
for (int i = 0; i < w->num_metadata; i++) {
size += strlen(w->metadata_keys[i]) + strlen(w->metadata_values[i]) + 16;
}
char* json = malloc(size);
if (!json) return NULL;
char* p = json;
char* end = json + size;
*p++ = '{';
bool first = true;
/* Metadata */
if (w->num_metadata > 0) {
p += snprintf(p, end - p, "\"__metadata__\":{");
for (int i = 0; i < w->num_metadata; i++) {
if (i > 0) *p++ = ',';
p += snprintf(p, end - p, "\"%s\":\"%s\"", w->metadata_keys[i], w->metadata_values[i]);
}
*p++ = '}';
first = false;
}
/* Tensors */
size_t current_offset = 0;
for (int i = 0; i < w->num_tensors; i++) {
writer_tensor* t = &w->tensors[i];
offsets[i] = current_offset;
if (!first) *p++ = ',';
first = false;
p += snprintf(p, end - p, "\"%s\":{\"dtype\":\"%s\",\"shape\":[",
t->name, fs_dtype_name(t->dtype));
for (int j = 0; j < t->ndim; j++) {
if (j > 0) *p++ = ',';
p += snprintf(p, end - p, "%lld", (long long)t->shape[j]);
}
p += snprintf(p, end - p, "],\"data_offsets\":[%zu,%zu]}",
current_offset, current_offset + t->data_size);
current_offset += t->data_size;
}
*p++ = '}';
*p = '\0';
return json;
}
void* fs_writer_to_buffer(fs_writer* w, size_t* size) {
if (!w || !size) return NULL;
size_t* offsets = malloc(w->num_tensors * sizeof(size_t));
if (!offsets) return NULL;
char* header = build_header_json(w, offsets);
if (!header) {
free(offsets);
return NULL;
}
size_t header_len = strlen(header);
/* Calculate total data size */
size_t data_size = 0;
for (int i = 0; i < w->num_tensors; i++) {
data_size += w->tensors[i].data_size;
}
size_t total = 8 + header_len + data_size;
uint8_t* buf = malloc(total);
if (!buf) {
free(header);
free(offsets);
return NULL;
}
/* Write header length (little-endian u64) */
for (int i = 0; i < 8; i++) {
buf[i] = (header_len >> (i * 8)) & 0xFF;
}
/* Write header */
memcpy(buf + 8, header, header_len);
/* Write tensor data */
uint8_t* data_ptr = buf + 8 + header_len;
for (int i = 0; i < w->num_tensors; i++) {
memcpy(data_ptr + offsets[i], w->tensors[i].data, w->tensors[i].data_size);
}
free(header);
free(offsets);
*size = total;
return buf;
}
bool fs_writer_save(fs_writer* w, const char* path) {
if (!w || !path) return false;
size_t size;
void* buf = fs_writer_to_buffer(w, &size);
if (!buf) return false;
FILE* f = fopen(path, "wb");
if (!f) {
free(buf);
return false;
}
size_t written = fwrite(buf, 1, size, f);
fclose(f);
free(buf);
return written == size;
}
/* ============================================================= */
/* Utility */
/* ============================================================= */
void fs_print(fs_file* f) {
if (!f) {
printf("(null safetensors file)\n");
return;
}
printf("Safetensors file: %zu bytes, %d tensors\n", f->size, f->num_tensors);
if (f->num_metadata > 0) {
printf("Metadata:\n");
for (int i = 0; i < f->num_metadata; i++) {
printf(" %s: %s\n", f->metadata_keys[i], f->metadata_values[i]);
}
}
printf("Tensors:\n");
for (int i = 0; i < f->num_tensors; i++) {
tensor_entry* t = &f->tensors[i];
printf(" %s: %s [", t->name, fs_dtype_name(t->dtype));
for (int j = 0; j < t->ndim; j++) {
if (j > 0) printf(", ");
printf("%lld", (long long)t->shape[j]);
}
printf("] (%zu bytes)\n", t->offset_end - t->offset_start);
}
}