Foundry/pth/tests/test_pth.c

227 lines
6.3 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* Unit tests for PyTorch .pth reader module
*/
#include "lib/test.h"
#include "pth/pth.h"
#include "safetensors/safetensors.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* ============================================================= */
/* No-model tests (always run) */
/* ============================================================= */
static void test_pt_open_nonexistent(void) {
pt_file *f = pt_open("/tmp/nonexistent_file_12345.pth");
ASSERT_NULL(f);
}
static void test_pt_open_not_zip(void) {
/* Create a garbage file */
const char *path = "/tmp/test_pth_garbage.bin";
FILE *fp = fopen(path, "wb");
ASSERT_NOT_NULL(fp);
fprintf(fp, "this is not a zip file");
fclose(fp);
pt_file *f = pt_open(path);
ASSERT_NULL(f);
unlink(path);
}
static void test_pt_open_null(void) {
pt_file *f = pt_open(NULL);
ASSERT_NULL(f);
}
static void test_pt_tensor_count_null(void) {
ASSERT_INT_EQ(pt_tensor_count(NULL), 0);
}
static void test_pt_tensor_at_null(void) {
ASSERT_NULL(pt_tensor_at(NULL, 0));
}
static void test_pt_tensor_names_null(void) {
int count = -1;
const char **names = pt_tensor_names(NULL, &count);
ASSERT_NULL(names);
ASSERT_INT_EQ(count, 0);
}
/* ============================================================= */
/* Model-gated tests (skip if .pth not present) */
/* ============================================================= */
#define RWKV7_FILENAME "RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth"
static const char *rwkv7_pth_path(void) {
/* Check env var first */
const char *env = getenv("RWKV7_PTH");
if (env && access(env, R_OK) == 0) return env;
/* Try relative paths (from different CWDs) */
static const char *paths[] = {
"../models/base/" RWKV7_FILENAME,
"../../models/base/" RWKV7_FILENAME,
"../RWKVx/models/base/" RWKV7_FILENAME,
};
for (int i = 0; i < 3; i++) {
if (access(paths[i], R_OK) == 0) return paths[i];
}
return NULL;
}
static int has_model(void) {
return rwkv7_pth_path() != NULL;
}
static void test_pt_open_rwkv7(void) {
if (!has_model()) {
printf(" (skipped — model not found)\n");
return;
}
pt_file *f = pt_open(rwkv7_pth_path());
ASSERT_NOT_NULL(f);
ASSERT_TRUE(pt_tensor_count(f) > 0);
printf(" found %d tensors\n", pt_tensor_count(f));
pt_close(f);
}
static void test_pt_tensor_names_rwkv7(void) {
if (!has_model()) {
printf(" (skipped — model not found)\n");
return;
}
pt_file *f = pt_open(rwkv7_pth_path());
ASSERT_NOT_NULL(f);
int count = 0;
const char **names = pt_tensor_names(f, &count);
ASSERT_NOT_NULL(names);
ASSERT_TRUE(count > 0);
/* Check for expected tensor names */
int found_emb = 0, found_blocks = 0, found_head = 0;
for (int i = 0; i < count; i++) {
if (strstr(names[i], "emb.weight")) found_emb = 1;
if (strstr(names[i], "blocks.0.")) found_blocks = 1;
if (strstr(names[i], "head.")) found_head = 1;
}
ASSERT_TRUE(found_emb);
ASSERT_TRUE(found_blocks);
ASSERT_TRUE(found_head);
/* Print first few names */
printf(" first 5 tensors:\n");
for (int i = 0; i < 5 && i < count; i++)
printf(" [%d] %s\n", i, names[i]);
free(names);
pt_close(f);
}
static void test_pt_tensor_dtypes(void) {
if (!has_model()) {
printf(" (skipped — model not found)\n");
return;
}
pt_file *f = pt_open(rwkv7_pth_path());
ASSERT_NOT_NULL(f);
int n = pt_tensor_count(f);
for (int i = 0; i < n; i++) {
const pt_tensor *t = pt_tensor_at(f, i);
ASSERT_NOT_NULL(t);
/* All RWKV-7 tensors should be BF16 */
ASSERT_TRUE(t->dtype == FS_DTYPE_BF16 || t->dtype == FS_DTYPE_F32 ||
t->dtype == FS_DTYPE_F16);
ASSERT_NOT_NULL(t->data);
ASSERT_TRUE(t->data_size > 0);
}
pt_close(f);
}
static void test_pt_to_safetensors(void) {
if (!has_model()) {
printf(" (skipped — model not found)\n");
return;
}
pt_file *f = pt_open(rwkv7_pth_path());
ASSERT_NOT_NULL(f);
int n = pt_tensor_count(f);
ASSERT_TRUE(n > 0);
/* Convert to safetensors */
const char *out_path = "/tmp/test_pth_convert.safetensors";
fs_writer *w = fs_writer_new();
ASSERT_NOT_NULL(w);
int written = 0;
for (int i = 0; i < n; i++) {
const pt_tensor *t = pt_tensor_at(f, i);
if (!t || !t->data || t->data_size == 0) continue;
fs_writer_add_tensor(w, t->name, t->dtype, t->shape, t->ndim, t->data);
written++;
}
ASSERT_TRUE(fs_writer_save(w, out_path));
fs_writer_free(w);
/* Read back and verify */
fs_file *sf = fs_open(out_path);
ASSERT_NOT_NULL(sf);
ASSERT_INT_EQ(fs_num_tensors(sf), written);
/* Spot check: verify first tensor shape matches */
const pt_tensor *first = pt_tensor_at(f, 0);
if (first && first->data) {
fs_tensor_info *info = fs_get_info(sf, first->name);
ASSERT_NOT_NULL(info);
ASSERT_INT_EQ(info->ndim, first->ndim);
for (int d = 0; d < first->ndim; d++)
ASSERT_TRUE(info->shape[d] == first->shape[d]);
ASSERT_TRUE(info->dtype == first->dtype);
fs_free_info(info);
}
fs_close(sf);
pt_close(f);
unlink(out_path);
printf(" converted %d tensors successfully\n", written);
}
/* ============================================================= */
/* Main */
/* ============================================================= */
int main(void) {
printf("\n pth module tests\n\n");
/* Always-run tests */
RUN_TEST(pt_open_nonexistent);
RUN_TEST(pt_open_not_zip);
RUN_TEST(pt_open_null);
RUN_TEST(pt_tensor_count_null);
RUN_TEST(pt_tensor_at_null);
RUN_TEST(pt_tensor_names_null);
/* Model-gated tests */
printf("\n model-gated tests (RWKV-7 0.1B)\n\n");
RUN_TEST(pt_open_rwkv7);
RUN_TEST(pt_tensor_names_rwkv7);
RUN_TEST(pt_tensor_dtypes);
RUN_TEST(pt_to_safetensors);
TEST_REPORT();
return __ft_failed > 0 ? 1 : 0;
}