301 lines
9.7 KiB
C
301 lines
9.7 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file gguf_test_reader.h
|
|
* @brief Locates a tensor inside a GGUF v3 container and reads its raw bytes,
|
|
* so a Metal quant-kernel test can be driven by blocks from the real model
|
|
* file instead of by synthetic ones. Header-only and test-scope: it parses
|
|
* only enough of the container to resolve a tensor name to its type, shape
|
|
* and absolute byte offset, and never dequantizes anything.
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#ifndef FOUNDRY_METAL_TESTS_GGUF_TEST_READER_H
|
|
#define FOUNDRY_METAL_TESTS_GGUF_TEST_READER_H
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
/* ggml type ids as they appear in a GGUF tensor info record. */
|
|
#define GGUF_GGML_TYPE_Q5_K 13
|
|
#define GGUF_GGML_TYPE_Q6_K 14
|
|
|
|
#define GGUF_MAX_TENSOR_DIMS 4
|
|
#define GGUF_MAX_TENSOR_NAME 256
|
|
|
|
/* Where one tensor lives in the file, and what shape it has.
|
|
* GGUF stores dims fastest-varying first, so for a 2-D weight
|
|
* dims[0] is the column count and dims[1] the row count. */
|
|
typedef struct gguf_tensor_location {
|
|
uint32_t ggml_type;
|
|
uint32_t n_dims;
|
|
uint64_t dims[GGUF_MAX_TENSOR_DIMS];
|
|
uint64_t file_offset; /* absolute offset of the tensor's first byte */
|
|
} gguf_tensor_location;
|
|
|
|
/* An opened container, positioned so tensors can be looked up repeatedly. */
|
|
typedef struct gguf_test_file {
|
|
FILE *fp;
|
|
uint64_t tensor_count;
|
|
uint64_t tensor_info_start; /* absolute offset of the first tensor info */
|
|
uint64_t tensor_data_start; /* absolute offset of the tensor data block */
|
|
uint64_t alignment;
|
|
} gguf_test_file;
|
|
|
|
/* ================================================================
|
|
* Primitive reads
|
|
* ================================================================ */
|
|
|
|
static inline int gguf_read_exact(FILE *fp, void *dst, size_t bytes)
|
|
{
|
|
return fread(dst, 1, bytes, fp) == bytes ? 0 : -1;
|
|
}
|
|
|
|
static inline int gguf_read_u32(FILE *fp, uint32_t *out)
|
|
{
|
|
return gguf_read_exact(fp, out, sizeof(*out));
|
|
}
|
|
|
|
static inline int gguf_read_u64(FILE *fp, uint64_t *out)
|
|
{
|
|
return gguf_read_exact(fp, out, sizeof(*out));
|
|
}
|
|
|
|
/* Reads a GGUF string (u64 length + raw bytes) into `dst`, always
|
|
* NUL-terminating. Oversized strings are consumed but truncated in `dst`,
|
|
* which is fine here — no tensor name we look for is near the cap. */
|
|
static inline int gguf_read_string(FILE *fp, char *dst, size_t dst_size)
|
|
{
|
|
uint64_t length = 0;
|
|
if (gguf_read_u64(fp, &length) != 0)
|
|
return -1;
|
|
|
|
size_t kept = length < dst_size - 1 ? (size_t)length : dst_size - 1;
|
|
if (gguf_read_exact(fp, dst, kept) != 0)
|
|
return -1;
|
|
dst[kept] = '\0';
|
|
|
|
if (length > kept && fseeko(fp, (off_t)(length - kept), SEEK_CUR) != 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Metadata skipping
|
|
* ================================================================ */
|
|
|
|
/* Byte width of a fixed-size GGUF metadata value type, or 0 if the type is
|
|
* variable-length (string, array) or unknown. */
|
|
static inline size_t gguf_fixed_value_size(uint32_t value_type)
|
|
{
|
|
switch (value_type) {
|
|
case 0: case 1: case 7: return 1; /* uint8, int8, bool */
|
|
case 2: case 3: return 2; /* uint16, int16 */
|
|
case 4: case 5: case 6: return 4; /* uint32, int32, f32 */
|
|
case 10: case 11: case 12: return 8; /* uint64, int64, f64 */
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static inline int gguf_skip_value(FILE *fp, uint32_t value_type)
|
|
{
|
|
size_t fixed = gguf_fixed_value_size(value_type);
|
|
if (fixed > 0)
|
|
return fseeko(fp, (off_t)fixed, SEEK_CUR);
|
|
|
|
if (value_type == 8) { /* string */
|
|
uint64_t length = 0;
|
|
if (gguf_read_u64(fp, &length) != 0)
|
|
return -1;
|
|
return fseeko(fp, (off_t)length, SEEK_CUR);
|
|
}
|
|
|
|
if (value_type == 9) { /* array */
|
|
uint32_t element_type = 0;
|
|
uint64_t count = 0;
|
|
if (gguf_read_u32(fp, &element_type) != 0 || gguf_read_u64(fp, &count) != 0)
|
|
return -1;
|
|
size_t element_size = gguf_fixed_value_size(element_type);
|
|
if (element_size > 0)
|
|
return fseeko(fp, (off_t)(element_size * count), SEEK_CUR);
|
|
for (uint64_t i = 0; i < count; i++) {
|
|
if (gguf_skip_value(fp, element_type) != 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return -1; /* unknown type: cannot skip safely */
|
|
}
|
|
|
|
/* Advances past one tensor info record, optionally reporting it. */
|
|
static inline int gguf_read_tensor_info(FILE *fp, char *name, size_t name_size,
|
|
gguf_tensor_location *out)
|
|
{
|
|
if (gguf_read_string(fp, name, name_size) != 0)
|
|
return -1;
|
|
|
|
uint32_t n_dims = 0;
|
|
if (gguf_read_u32(fp, &n_dims) != 0 || n_dims > GGUF_MAX_TENSOR_DIMS)
|
|
return -1;
|
|
|
|
gguf_tensor_location location = {0};
|
|
location.n_dims = n_dims;
|
|
for (uint32_t i = 0; i < n_dims; i++) {
|
|
if (gguf_read_u64(fp, &location.dims[i]) != 0)
|
|
return -1;
|
|
}
|
|
|
|
uint64_t relative_offset = 0;
|
|
if (gguf_read_u32(fp, &location.ggml_type) != 0 ||
|
|
gguf_read_u64(fp, &relative_offset) != 0)
|
|
return -1;
|
|
|
|
location.file_offset = relative_offset; /* still relative; fixed up by the caller */
|
|
if (out) *out = location;
|
|
return 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Public API
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* Opens a GGUF v3 file and resolves where its tensor data begins.
|
|
*
|
|
* @param path Path to the .gguf file
|
|
* @param out Receives the opened handle (close with gguf_test_close)
|
|
* @return 0 on success, -1 if the file is missing or not a GGUF v3
|
|
*
|
|
* Thread-safe: yes (touches only the caller's handle)
|
|
* Reentrant: yes
|
|
* Signal-safe: no
|
|
*/
|
|
static inline int gguf_test_open(const char *path, gguf_test_file *out)
|
|
{
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
out->fp = fopen(path, "rb");
|
|
if (!out->fp)
|
|
return -1;
|
|
|
|
char magic[4] = {0};
|
|
uint32_t version = 0;
|
|
uint64_t kv_count = 0;
|
|
if (gguf_read_exact(out->fp, magic, 4) != 0 ||
|
|
memcmp(magic, "GGUF", 4) != 0 ||
|
|
gguf_read_u32(out->fp, &version) != 0 || version != 3 ||
|
|
gguf_read_u64(out->fp, &out->tensor_count) != 0 ||
|
|
gguf_read_u64(out->fp, &kv_count) != 0)
|
|
goto fail;
|
|
|
|
out->alignment = 32; /* GGUF default, overridden by metadata */
|
|
for (uint64_t i = 0; i < kv_count; i++) {
|
|
char key[GGUF_MAX_TENSOR_NAME];
|
|
uint32_t value_type = 0;
|
|
if (gguf_read_string(out->fp, key, sizeof(key)) != 0 ||
|
|
gguf_read_u32(out->fp, &value_type) != 0)
|
|
goto fail;
|
|
if (strcmp(key, "general.alignment") == 0 && value_type == 4) {
|
|
uint32_t alignment = 0;
|
|
if (gguf_read_u32(out->fp, &alignment) != 0)
|
|
goto fail;
|
|
out->alignment = alignment ? alignment : 32;
|
|
continue;
|
|
}
|
|
if (gguf_skip_value(out->fp, value_type) != 0)
|
|
goto fail;
|
|
}
|
|
|
|
off_t info_start = ftello(out->fp);
|
|
if (info_start < 0)
|
|
goto fail;
|
|
out->tensor_info_start = (uint64_t)info_start;
|
|
|
|
for (uint64_t i = 0; i < out->tensor_count; i++) {
|
|
char name[GGUF_MAX_TENSOR_NAME];
|
|
if (gguf_read_tensor_info(out->fp, name, sizeof(name), NULL) != 0)
|
|
goto fail;
|
|
}
|
|
|
|
off_t info_end = ftello(out->fp);
|
|
if (info_end < 0)
|
|
goto fail;
|
|
out->tensor_data_start =
|
|
((uint64_t)info_end + out->alignment - 1) / out->alignment * out->alignment;
|
|
return 0;
|
|
|
|
fail:
|
|
fclose(out->fp);
|
|
out->fp = NULL;
|
|
return -1;
|
|
}
|
|
|
|
static inline void gguf_test_close(gguf_test_file *file)
|
|
{
|
|
if (file && file->fp) {
|
|
fclose(file->fp);
|
|
file->fp = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds a tensor by name and reports its type, shape and absolute offset.
|
|
*
|
|
* @param file Handle from gguf_test_open
|
|
* @param tensor_name Exact tensor name, e.g. "blk.0.attn_qkv.weight"
|
|
* @param out Receives the location (file_offset is absolute)
|
|
* @return 0 on success, -1 if absent or the file is malformed
|
|
*
|
|
* Thread-safe: no (seeks the shared FILE*)
|
|
* Reentrant: no
|
|
* Signal-safe: no
|
|
*/
|
|
static inline int gguf_test_find_tensor(gguf_test_file *file, const char *tensor_name,
|
|
gguf_tensor_location *out)
|
|
{
|
|
if (!file || !file->fp)
|
|
return -1;
|
|
if (fseeko(file->fp, (off_t)file->tensor_info_start, SEEK_SET) != 0)
|
|
return -1;
|
|
|
|
for (uint64_t i = 0; i < file->tensor_count; i++) {
|
|
char name[GGUF_MAX_TENSOR_NAME];
|
|
gguf_tensor_location location;
|
|
if (gguf_read_tensor_info(file->fp, name, sizeof(name), &location) != 0)
|
|
return -1;
|
|
if (strcmp(name, tensor_name) == 0) {
|
|
location.file_offset += file->tensor_data_start;
|
|
*out = location;
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Reads raw bytes at an absolute file offset — used to pull a handful of real
|
|
* quantized rows out of a multi-gigabyte model without mapping the whole file.
|
|
*
|
|
* @return 0 on success, -1 on a short or failed read
|
|
*
|
|
* Thread-safe: no (seeks the shared FILE*)
|
|
* Reentrant: no
|
|
* Signal-safe: no
|
|
*/
|
|
static inline int gguf_test_read_at(gguf_test_file *file, uint64_t offset,
|
|
void *dst, size_t bytes)
|
|
{
|
|
if (!file || !file->fp)
|
|
return -1;
|
|
if (fseeko(file->fp, (off_t)offset, SEEK_SET) != 0)
|
|
return -1;
|
|
return gguf_read_exact(file->fp, dst, bytes);
|
|
}
|
|
|
|
#endif /* FOUNDRY_METAL_TESTS_GGUF_TEST_READER_H */
|