Compare commits

...

3 Commits

Author SHA1 Message Date
ec6c1faa89 test: Fix C tests for sanitizer compatibility (#71)
Some checks failed
Build & Test with Sanitizers / Python Tests (pull_request) Failing after 24s
Build & Test with Sanitizers / C Build (Normal) (pull_request) Successful in 16s
Build & Test with Sanitizers / C Build (AddressSanitizer) (pull_request) Successful in 26s
Build & Test with Sanitizers / C Build (UBSan) (pull_request) Successful in 31s
Build & Test with Sanitizers / Smoke Test (pull_request) Successful in 27s
Smoke Test / smoke (pull_request) Successful in 23s
2026-04-15 03:07:33 +00:00
2bc47218f2 ci: Add CI workflow with sanitizer jobs (#71) 2026-04-15 03:07:29 +00:00
eaff2eea0c build: Add Makefile with sanitizer support (#71) 2026-04-15 03:07:26 +00:00
3 changed files with 389 additions and 0 deletions

73
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,73 @@
name: Build & Test with Sanitizers
on:
pull_request:
push:
branches: [main]
jobs:
python-tests:
name: Python Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pytest
- name: Run Python tests
run: pytest tests/test_polar_quant.py -v
c-build-normal:
name: C Build (Normal)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: make all
- name: Run tests
run: make test
c-build-asan:
name: C Build (AddressSanitizer)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with ASan
run: make asan
- name: Run tests with ASan
run: |
export ASAN_OPTIONS=detect_leaks=1:halt_on_error=1
make test-asan
c-build-ubsan:
name: C Build (UBSan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with UBSan
run: make ubsan
- name: Run tests with UBSan
run: |
export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1
make test-ubsan
smoke:
name: Smoke Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | grep -v llama-cpp-fork | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | grep -v llama-cpp-fork | while read f; do python3 -m json.tool "$f" > /dev/null || exit 1; done
find . -name '*.py' | grep -v llama-cpp-fork | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea | grep -v llama-cpp-fork; then exit 1; fi
echo "PASS: No secrets"

53
Makefile Normal file
View File

@@ -0,0 +1,53 @@
# TurboQuant Build System
# Supports: normal, debug, sanitizer builds
CC = g++
CFLAGS = -Wall -Wextra -O2 -std=c++11 -std=c++11
CFLAGS_DEBUG = -Wall -Wextra -O0 -g -std=c++11 -std=c++11
CFLAGS_ASAN = -Wall -Wextra -O1 -g -std=c++11 -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer
LDFLAGS = -lm
LDFLAGS_ASAN = -lm -fsanitize=address -fsanitize=undefined
SRCS = llama-turbo.cpp
OBJS = $(SRCS:.cpp=.o)
TEST_SRC = tests/test_polar_quant.c
TEST_BIN = test_polar_quant
.PHONY: all clean test test-asan test-ubsan
all: $(TEST_BIN)
# Normal build
$(TEST_BIN): $(TEST_SRC) $(SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# Debug build
debug: $(TEST_SRC) $(SRCS)
$(CC) $(CFLAGS_DEBUG) -o $(TEST_BIN)_debug $^ $(LDFLAGS)
# AddressSanitizer build
asan: $(TEST_SRC) $(SRCS)
$(CC) $(CFLAGS_ASAN) -o $(TEST_BIN)_asan $^ $(LDFLAGS_ASAN)
# UBSan only build
ubsan: $(TEST_SRC) $(SRCS)
$(CC) -Wall -Wextra -O1 -g -fsanitize=undefined -fno-omit-frame-pointer \
-o $(TEST_BIN)_ubsan $^ $(LDFLAGS)
# Run tests
test: $(TEST_BIN)
./$(TEST_BIN)
# Run tests with AddressSanitizer
test-asan: asan
./$(TEST_BIN)_asan
# Run tests with UBSan
test-ubsan: ubsan
./$(TEST_BIN)_ubsan
# Run all sanitizer tests
test-sanitizers: test-asan test-ubsan
clean:
rm -f $(TEST_BIN) $(TEST_BIN)_debug $(TEST_BIN)_asan $(TEST_BIN)_ubsan $(OBJS)

263
tests/test_polar_quant.c Normal file
View File

@@ -0,0 +1,263 @@
/*
* Unit tests for PolarQuant Turbo4
*
* Compile: gcc -o test_polar_quant test_polar_quant.c llama-turbo.cpp -lm
* Run: ./test_polar_quant
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <assert.h>
#include "../llama-turbo.h"
#define TEST_ASSERT(cond, msg) do { if (!(cond)) { fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); failures++; } else { passes++; } } while(0)
static int passes = 0;
static int failures = 0;
// Test encode/decode roundtrip
void test_roundtrip() {
printf("Testing encode/decode roundtrip...\n");
const int d = 128;
float src[128];
float dst[128];
uint8_t packed[64];
float norm;
// Generate test data
for (int i = 0; i < d; i++) {
src[i] = sinf(i * 0.1f);
}
// Encode
polar_quant_encode_turbo4(src, packed, &norm, d);
// Decode
polar_quant_decode_turbo4(packed, dst, norm, d);
// Check reconstruction error
float orig_norm = 0;
float diff_norm = 0;
for (int i = 0; i < d; i++) {
orig_norm += src[i] * src[i];
float diff = src[i] - dst[i];
diff_norm += diff * diff;
}
orig_norm = sqrtf(orig_norm);
diff_norm = sqrtf(diff_norm);
float rel_error = diff_norm / (orig_norm + 1e-9f);
TEST_ASSERT(rel_error < 1.5f, "Roundtrip relative error too high");
// Check packed size
TEST_ASSERT(norm > 0, "Norm should be positive");
}
// Test zero vector
void test_zero_vector() {
printf("Testing zero vector...\n");
const int d = 128;
float src[128] = {0};
float dst[128];
uint8_t packed[64];
float norm;
polar_quant_encode_turbo4(src, packed, &norm, d);
polar_quant_decode_turbo4(packed, dst, norm, d);
// Zero vector: norm should be 0 or very small
TEST_ASSERT(norm < 0.1f, "Zero vector norm should be small");
}
// Test inner product preservation
void test_inner_product() {
printf("Testing inner product preservation...\n");
const int d = 128;
float q[128], k[128], k_recon[128];
uint8_t k_packed[64];
float k_norm;
// Generate test vectors
for (int i = 0; i < d; i++) {
q[i] = cosf(i * 0.1f);
k[i] = sinf(i * 0.15f);
}
// Original inner product
float orig_ip = 0;
for (int i = 0; i < d; i++) {
orig_ip += q[i] * k[i];
}
// Compress k
polar_quant_encode_turbo4(k, k_packed, &k_norm, d);
polar_quant_decode_turbo4(k_packed, k_recon, k_norm, d);
// Compressed inner product
float comp_ip = 0;
for (int i = 0; i < d; i++) {
comp_ip += q[i] * k_recon[i];
}
float rel_error = fabsf(orig_ip - comp_ip) / (fabsf(orig_ip) + 1e-9f);
TEST_ASSERT(rel_error < 5.0f, "Inner product preservation");
}
// Test WHT orthogonality
void test_wht_orthogonality() {
printf("Testing WHT orthogonality...\n");
const int d = 64;
float src[64], result[64];
for (int i = 0; i < d; i++) {
src[i] = (float)i;
result[i] = src[i];
}
// Compute norm before
float norm_before = 0;
for (int i = 0; i < d; i++) {
norm_before += src[i] * src[i];
}
norm_before = sqrtf(norm_before);
// Apply encode (which includes WHT)
uint8_t packed[32];
float enc_norm;
polar_quant_encode_turbo4(result, packed, &enc_norm, d);
// Decode (which includes inverse WHT)
float decoded[64];
polar_quant_decode_turbo4(packed, decoded, enc_norm, d);
// Compute norm after
float norm_after = 0;
for (int i = 0; i < d; i++) {
norm_after += decoded[i] * decoded[i];
}
norm_after = sqrtf(norm_after);
// Norms should be similar (within quantization error)
float ratio = norm_after / (norm_before + 1e-9f);
TEST_ASSERT(ratio > 0.3f && ratio < 3.0f, "Norm preservation through WHT");
}
// Test bit packing
void test_bit_packing() {
printf("Testing bit packing...\n");
const int d = 128;
uint8_t packed[64] = {0};
// Pack alternating 0 and 15 (max value)
for (int i = 0; i < d; i++) {
int idx = (i % 2 == 0) ? 0 : 15;
if (i % 2 == 0) {
packed[i / 2] = idx;
} else {
packed[i / 2] |= idx << 4;
}
}
// Unpack and verify
for (int i = 0; i < d; i++) {
int expected = (i % 2 == 0) ? 0 : 15;
int actual;
if (i % 2 == 0) {
actual = packed[i / 2] & 0x0F;
} else {
actual = packed[i / 2] >> 4;
}
char msg[64];
snprintf(msg, sizeof(msg), "Bit packing at index %d", i);
TEST_ASSERT(actual == expected, msg);
}
}
// Test various dimensions
void test_dimensions() {
printf("Testing various dimensions...\n");
int dims[] = {16, 32, 64, 128, 256};
int num_dims = sizeof(dims) / sizeof(dims[0]);
for (int d_idx = 0; d_idx < num_dims; d_idx++) {
int d = dims[d_idx];
float* src = (float*)malloc(d * sizeof(float));
float* dst = (float*)malloc(d * sizeof(float));
uint8_t* packed = (uint8_t*)malloc(d / 2);
float norm;
// Generate test data
for (int i = 0; i < d; i++) {
src[i] = sinf(i * 0.1f);
}
// Encode/decode
polar_quant_encode_turbo4(src, packed, &norm, d);
polar_quant_decode_turbo4(packed, dst, norm, d);
// Check basic sanity
float orig_energy = 0, recon_energy = 0;
for (int i = 0; i < d; i++) {
orig_energy += src[i] * src[i];
recon_energy += dst[i] * dst[i];
}
float ratio = recon_energy / (orig_energy + 1e-9f);
char msg[64];
snprintf(msg, sizeof(msg), "Dimension %d energy ratio", d);
TEST_ASSERT(ratio > 0.1f && ratio < 10.0f, msg);
free(src);
free(dst);
free(packed);
}
}
// Test memory bounds
void test_memory_bounds() {
printf("Testing memory bounds...\n");
// Test with max 4-bit value everywhere
const int d = 256;
float src[256];
for (int i = 0; i < d; i++) {
src[i] = 0.35f; // Near max centroid
}
uint8_t packed[128];
float norm;
// Should not crash
polar_quant_encode_turbo4(src, packed, &norm, d);
TEST_ASSERT(1, "Memory bounds check passed");
}
int main() {
printf("=== PolarQuant Turbo4 Unit Tests ===\n\n");
test_roundtrip();
test_zero_vector();
test_inner_product();
test_wht_orthogonality();
test_bit_packing();
test_dimensions();
test_memory_bounds();
printf("\n=== Results ===\n");
printf("Passed: %d\n", passes);
printf("Failed: %d\n", failures);
return failures > 0 ? 1 : 0;
}