diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9bdc0ea --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +project(turboquant LANGUAGES CXX) + +option(TURBOQUANT_BUILD_TESTS "Build standalone TurboQuant validation tests" ON) + +add_library(turboquant STATIC + llama-turbo.cpp +) + +target_include_directories(turboquant PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_features(turboquant PUBLIC cxx_std_17) + +if(MSVC) + target_compile_options(turboquant PRIVATE /W4) +else() + target_compile_options(turboquant PRIVATE -Wall -Wextra -Wpedantic) +endif() + +if(TURBOQUANT_BUILD_TESTS) + include(CTest) + + add_executable(turboquant_roundtrip_test + tests/roundtrip_test.cpp + ) + target_link_libraries(turboquant_roundtrip_test PRIVATE turboquant) + target_compile_features(turboquant_roundtrip_test PRIVATE cxx_std_17) + + add_test( + NAME turboquant_roundtrip + COMMAND turboquant_roundtrip_test + ) +endif() diff --git a/tests/__pycache__/test_standalone_build.cpython-312.pyc b/tests/__pycache__/test_standalone_build.cpython-312.pyc new file mode 100644 index 0000000..d92c6cc Binary files /dev/null and b/tests/__pycache__/test_standalone_build.cpython-312.pyc differ diff --git a/tests/roundtrip_test.cpp b/tests/roundtrip_test.cpp new file mode 100644 index 0000000..c82f59d --- /dev/null +++ b/tests/roundtrip_test.cpp @@ -0,0 +1,104 @@ +#include "llama-turbo.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int kDim = 128; +constexpr float kCosineThreshold = 0.99f; +constexpr float kZeroTolerance = 1.0e-6f; + +[[nodiscard]] bool all_finite(const std::vector & values) { + for (float value : values) { + if (!std::isfinite(value)) { + return false; + } + } + return true; +} + +[[nodiscard]] float max_abs(const std::vector & values) { + float best = 0.0f; + for (float value : values) { + best = std::max(best, std::fabs(value)); + } + return best; +} + +[[nodiscard]] float cosine_similarity(const std::vector & lhs, const std::vector & rhs) { + float dot = 0.0f; + float lhs_norm = 0.0f; + float rhs_norm = 0.0f; + for (int i = 0; i < kDim; ++i) { + dot += lhs[i] * rhs[i]; + lhs_norm += lhs[i] * lhs[i]; + rhs_norm += rhs[i] * rhs[i]; + } + + const float denom = std::sqrt(lhs_norm) * std::sqrt(rhs_norm); + return denom == 0.0f ? 1.0f : dot / denom; +} + +[[nodiscard]] std::vector roundtrip(const std::vector & input, float & norm_out) { + std::vector packed(kDim / 2, 0); + norm_out = -1.0f; + polar_quant_encode_turbo4(input.data(), packed.data(), &norm_out, kDim); + + std::vector decoded(kDim, 0.0f); + polar_quant_decode_turbo4(packed.data(), decoded.data(), norm_out, kDim); + return decoded; +} + +void require(bool condition, const std::string & message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +void test_zero_vector_roundtrip() { + std::vector zeros(kDim, 0.0f); + float norm = -1.0f; + const auto decoded = roundtrip(zeros, norm); + + require(norm == 0.0f, "zero vector should encode with zero norm"); + require(all_finite(decoded), "zero vector decode produced non-finite values"); + require(max_abs(decoded) <= kZeroTolerance, "zero vector decode should remain near zero"); +} + +void test_gaussian_roundtrip_quality() { + std::mt19937 rng(12345); + std::normal_distribution dist(0.0f, 1.0f); + + std::vector input(kDim, 0.0f); + for (float & value : input) { + value = dist(rng); + } + + float norm = -1.0f; + const auto decoded = roundtrip(input, norm); + + require(norm > 0.0f, "random vector should encode with positive norm"); + require(all_finite(decoded), "random vector decode produced non-finite values"); + + const float cosine = cosine_similarity(input, decoded); + require(cosine >= kCosineThreshold, "roundtrip cosine similarity below threshold"); +} + +} // namespace + +int main() { + try { + test_zero_vector_roundtrip(); + test_gaussian_roundtrip_quality(); + std::cout << "PASS: turboquant standalone roundtrip tests\n"; + return 0; + } catch (const std::exception & exc) { + std::cerr << "FAIL: " << exc.what() << '\n'; + return 1; + } +} diff --git a/tests/test_standalone_build.py b/tests/test_standalone_build.py new file mode 100644 index 0000000..b7169d7 --- /dev/null +++ b/tests/test_standalone_build.py @@ -0,0 +1,64 @@ +import pathlib +import shutil +import subprocess +import tempfile +import unittest + + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class StandaloneBuildTest(unittest.TestCase): + def test_cmake_builds_and_runs_ctest(self) -> None: + build_dir = pathlib.Path(tempfile.mkdtemp(prefix="turboquant-cmake-")) + self.addCleanup(lambda: shutil.rmtree(build_dir, ignore_errors=True)) + + configure = subprocess.run( + [ + "cmake", + "-S", + str(REPO_ROOT), + "-B", + str(build_dir), + "-DTURBOQUANT_BUILD_TESTS=ON", + ], + capture_output=True, + text=True, + check=False, + ) + if configure.returncode != 0: + self.fail( + "cmake configure failed\n" + f"stdout:\n{configure.stdout}\n" + f"stderr:\n{configure.stderr}" + ) + + build = subprocess.run( + ["cmake", "--build", str(build_dir)], + capture_output=True, + text=True, + check=False, + ) + if build.returncode != 0: + self.fail( + "cmake build failed\n" + f"stdout:\n{build.stdout}\n" + f"stderr:\n{build.stderr}" + ) + + ctest = subprocess.run( + ["ctest", "--test-dir", str(build_dir), "--output-on-failure"], + capture_output=True, + text=True, + check=False, + ) + if ctest.returncode != 0: + self.fail( + "ctest failed\n" + f"stdout:\n{ctest.stdout}\n" + f"stderr:\n{ctest.stderr}" + ) + + +if __name__ == "__main__": + unittest.main()