Pantheon/tests/test_gguf.c

277 lines
8.5 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* GGUF parser and dequantization tests.
*/
#include "lib/test.h"
#include "../src/formats/gguf.h"
#include "../src/formats/gguf_dequant.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
/* ============================================================
* Dequantization tests
* ============================================================ */
/* Test Q4_0 dequantization with known values */
static void test_dequant_q4_0(void) {
pn_block_q4_0 block;
block.d = 0x3C00; /* FP16 1.0 */
memset(block.qs, 0x88, 16); /* All nibbles = 8 -> (8-8)*1.0 = 0.0 */
float out[32];
pn_dequantize_block_q4_0(&block, out);
for (int i = 0; i < 32; i++) {
ASSERT_FLT_NEAR(0.0f, out[i], 1e-6f);
}
/* Test with scale=2.0, first byte has lo=0, hi=15 */
block.d = 0x4000; /* FP16 2.0 */
memset(block.qs, 0, 16);
block.qs[0] = 0xF0; /* lo=0, hi=15 */
pn_dequantize_block_q4_0(&block, out);
ASSERT_FLT_NEAR(-16.0f, out[0], 1e-4f);
ASSERT_FLT_NEAR(14.0f, out[16], 1e-4f);
}
/* Test Q8_0 dequantization with known values */
static void test_dequant_q8_0(void) {
pn_block_q8_0 block;
block.d = 0x3C00; /* FP16 1.0 */
memset(block.qs, 0, 32);
block.qs[0] = 0;
block.qs[1] = 1;
block.qs[2] = -1;
block.qs[3] = 127;
block.qs[4] = -128;
float out[32];
pn_dequantize_block_q8_0(&block, out);
ASSERT_FLT_NEAR(0.0f, out[0], 1e-6f);
ASSERT_FLT_NEAR(1.0f, out[1], 1e-6f);
ASSERT_FLT_NEAR(-1.0f, out[2], 1e-6f);
ASSERT_FLT_NEAR(127.0f, out[3], 1e-6f);
ASSERT_FLT_NEAR(-128.0f, out[4], 1e-6f);
/* Test with scale=0.5 */
block.d = 0x3800; /* FP16 0.5 */
pn_dequantize_block_q8_0(&block, out);
ASSERT_FLT_NEAR(0.0f, out[0], 1e-6f);
ASSERT_FLT_NEAR(0.5f, out[1], 1e-6f);
ASSERT_FLT_NEAR(-0.5f, out[2], 1e-6f);
}
/* Test F16 dequantization */
static void test_dequant_f16(void) {
uint16_t src[] = { 0x0000, 0x3C00, 0xBC00, 0x4000, 0x3800 };
float dst[5];
int ret = pn_dequantize(src, dst, 5, GGUF_QUANT_F16);
ASSERT_INT_EQ(0, ret);
ASSERT_FLT_NEAR(0.0f, dst[0], 1e-6f);
ASSERT_FLT_NEAR(1.0f, dst[1], 1e-6f);
ASSERT_FLT_NEAR(-1.0f, dst[2], 1e-6f);
ASSERT_FLT_NEAR(2.0f, dst[3], 1e-6f);
ASSERT_FLT_NEAR(0.5f, dst[4], 1e-6f);
}
/* Test F32 passthrough */
static void test_dequant_f32(void) {
float src[] = { 1.0f, -2.5f, 3.14159f, 0.0f };
float dst[4];
int ret = pn_dequantize(src, dst, 4, GGUF_QUANT_F32);
ASSERT_INT_EQ(0, ret);
for (int i = 0; i < 4; i++) {
ASSERT_FLT_NEAR(src[i], dst[i], 1e-7f);
}
}
/* Test block size and type size functions */
static void test_block_sizes(void) {
ASSERT_INT_EQ(1, pn_gguf_block_size(GGUF_QUANT_F32));
ASSERT_INT_EQ(1, pn_gguf_block_size(GGUF_QUANT_F16));
ASSERT_INT_EQ(32, pn_gguf_block_size(GGUF_QUANT_Q4_0));
ASSERT_INT_EQ(32, pn_gguf_block_size(GGUF_QUANT_Q8_0));
/* K-quant block sizes */
ASSERT_INT_EQ(256, pn_gguf_block_size(GGUF_QUANT_Q4_K));
ASSERT_INT_EQ(256, pn_gguf_block_size(GGUF_QUANT_Q5_K));
ASSERT_INT_EQ(256, pn_gguf_block_size(GGUF_QUANT_Q6_K));
/* Type sizes: 128 elements */
ASSERT_INT_EQ(128 * 4, (int)pn_gguf_type_size(GGUF_QUANT_F32, 128));
ASSERT_INT_EQ(128 * 2, (int)pn_gguf_type_size(GGUF_QUANT_F16, 128));
/* Q4_0: 128/32 blocks * 18 bytes = 72 */
ASSERT_INT_EQ(72, (int)pn_gguf_type_size(GGUF_QUANT_Q4_0, 128));
/* Q8_0: 128/32 blocks * 34 bytes = 136 */
ASSERT_INT_EQ(136, (int)pn_gguf_type_size(GGUF_QUANT_Q8_0, 128));
/* K-quant type sizes: 256 elements */
ASSERT_INT_EQ(144, (int)pn_gguf_type_size(GGUF_QUANT_Q4_K, 256));
ASSERT_INT_EQ(176, (int)pn_gguf_type_size(GGUF_QUANT_Q5_K, 256));
ASSERT_INT_EQ(210, (int)pn_gguf_type_size(GGUF_QUANT_Q6_K, 256));
/* Bytes per block */
ASSERT_INT_EQ(144, (int)pn_gguf_bytes_per_block(GGUF_QUANT_Q4_K));
ASSERT_INT_EQ(176, (int)pn_gguf_bytes_per_block(GGUF_QUANT_Q5_K));
ASSERT_INT_EQ(210, (int)pn_gguf_bytes_per_block(GGUF_QUANT_Q6_K));
}
/* Test unsupported type returns -1 */
static void test_dequant_unsupported(void) {
float dummy_src = 0.0f;
float dummy_dst = 0.0f;
int ret = pn_dequantize(&dummy_src, &dummy_dst, 1, GGUF_QUANT_Q2_K);
ASSERT_INT_EQ(-1, ret);
}
/* Test bulk Q4_0 dequantization (multiple blocks) */
static void test_dequant_q4_0_bulk(void) {
pn_block_q4_0 blocks[2];
blocks[0].d = 0x3C00;
memset(blocks[0].qs, 0x88, 16);
blocks[1].d = 0x3800;
memset(blocks[1].qs, 0x99, 16);
float out[64];
int ret = pn_dequantize(blocks, out, 64, GGUF_QUANT_Q4_0);
ASSERT_INT_EQ(0, ret);
for (int i = 0; i < 32; i++) {
ASSERT_FLT_NEAR(0.0f, out[i], 1e-6f);
}
for (int i = 32; i < 64; i++) {
ASSERT_FLT_NEAR(0.5f, out[i], 1e-4f);
}
}
/* ============================================================
* K-quant dequantization tests
* ============================================================ */
/* Test Q4_K dequantization */
static void test_dequant_q4_k(void) {
pn_block_q4_k block;
memset(&block, 0, sizeof(block));
/* d=1.0, dmin=0.0 — simplifies formula to: d * sc[j] * nibble */
block.d = 0x3C00; /* FP16 1.0 */
block.dmin = 0x0000; /* FP16 0.0 */
/* Set scale[0] = 1 (low 6 bits of scales_raw[0]), rest = 0 */
block.scales[0] = 1;
memset(block.scales + 1, 0, 11);
/* ggml layout: 32-byte qs chunk, low nibbles → sub-block 0, high nibbles → sub-block 1.
* Set all 32 bytes of first chunk to 0x05 so low nibble = 5, high nibble = 0. */
memset(block.qs, 0x05, 32); /* lo=5, hi=0 for 32 bytes */
memset(block.qs + 32, 0, 96);
float out[256];
pn_dequantize_block_q4_k(&block, out);
/* Sub-block 0 (low nibbles): d * sc[0] * 5 - 0 = 5.0 */
for (int i = 0; i < 32; i++) {
ASSERT_FLT_NEAR(5.0f, out[i], 1e-4f);
}
/* Sub-block 1+ (sc[1..7]=0): 0.0 */
for (int i = 32; i < 256; i++) {
ASSERT_FLT_NEAR(0.0f, out[i], 1e-4f);
}
}
/* Test Q6_K dequantization */
static void test_dequant_q6_k(void) {
pn_block_q6_k block;
memset(&block, 0, sizeof(block));
/* d=1.0, scale[0]=1, rest=0 */
block.d = 0x3C00; /* FP16 1.0 */
block.scales[0] = 1;
/* Set first 16 elements to have q6 = 32 (which gives value = 1*(32-32) = 0) */
/* q6 = low4 | (high2 << 4). For q6=32: low4=0, high2=2 */
/* For first 8 elements (indices 0-7): ql bytes 0-3 contain pairs, qh byte 0 has pairs */
/* Just test that zero block produces all zeros minus offset */
float out[256];
pn_dequantize_block_q6_k(&block, out);
/* All zeros with scale=1: q6=0 for all, value = 1 * 1 * (0 - 32) = -32 */
ASSERT_FLT_NEAR(-32.0f, out[0], 1e-4f);
/* Sub-blocks with scale=0 should produce 0 regardless of q6 value */
for (int i = 16; i < 256; i++) {
ASSERT_FLT_NEAR(0.0f, out[i], 1e-4f);
}
}
/* Test Q5_K dequantization */
static void test_dequant_q5_k(void) {
pn_block_q5_k block;
memset(&block, 0, sizeof(block));
/* d=1.0, dmin=0.0, scale[0]=1 */
block.d = 0x3C00;
block.dmin = 0x0000;
block.scales[0] = 1;
/* ggml layout: 32-byte qs chunk, low nibbles → sub-block 0, high nibbles → sub-block 1.
* Set all 32 bytes to 0x03 so low nibble = 3, high nibble = 0. */
memset(block.qs, 0x03, 32); /* lo=3, hi=0 for 32 bytes */
memset(block.qs + 32, 0, 96);
memset(block.qh, 0, 32); /* all high bits = 0 */
float out[256];
pn_dequantize_block_q5_k(&block, out);
/* Sub-block 0 (low nibbles): d * sc[0] * 3 - 0 = 3.0 */
for (int i = 0; i < 32; i++) {
ASSERT_FLT_NEAR(3.0f, out[i], 1e-4f);
}
/* Rest should be 0 */
for (int i = 32; i < 256; i++) {
ASSERT_FLT_NEAR(0.0f, out[i], 1e-4f);
}
}
/* Test K-quant struct sizes match expected */
static void test_kquant_struct_sizes(void) {
ASSERT_INT_EQ(144, (int)sizeof(pn_block_q4_k));
ASSERT_INT_EQ(176, (int)sizeof(pn_block_q5_k));
ASSERT_INT_EQ(210, (int)sizeof(pn_block_q6_k));
}
int main(void) {
printf("GGUF & Dequantization Tests\n");
printf("===========================\n\n");
RUN_TEST(dequant_q4_0);
RUN_TEST(dequant_q8_0);
RUN_TEST(dequant_f16);
RUN_TEST(dequant_f32);
RUN_TEST(block_sizes);
RUN_TEST(dequant_unsupported);
RUN_TEST(dequant_q4_0_bulk);
RUN_TEST(dequant_q4_k);
RUN_TEST(dequant_q5_k);
RUN_TEST(dequant_q6_k);
RUN_TEST(kquant_struct_sizes);
TEST_REPORT();
return __ft_failed > 0 ? 1 : 0;
}