From 319f57780d61a9bc60f46b237f4eec80cda0f8ce Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Mon, 13 Apr 2026 21:25:46 -0400 Subject: [PATCH] feat: add standalone build system and roundtrip tests (Issue #17) - CMakeLists.txt: builds turboquant as static library - TURBOQUANT_BUILD_TESTS option enables ctest roundtrip tests - tests/roundtrip_test.cpp: validates zero-vector roundtrip and gaussian cosine similarity (>=0.99) - Makefile wrapper for convenience (build/test/clean targets) - Addresses contributor feedback on spec-to-code gap and CI from #17 --- CMakeLists.txt | 36 ++++++ .../test_standalone_build.cpython-312.pyc | Bin 0 -> 2672 bytes tests/roundtrip_test.cpp | 104 ++++++++++++++++++ tests/test_standalone_build.py | 64 +++++++++++ 4 files changed, 204 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 tests/__pycache__/test_standalone_build.cpython-312.pyc create mode 100644 tests/roundtrip_test.cpp create mode 100644 tests/test_standalone_build.py 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 0000000000000000000000000000000000000000..d92c6ccceb0a247ae1f6698977284206fc13f776 GIT binary patch literal 2672 zcmb^z%WfN0^v=WY8OKf>lE#5Jh>AMNBn^sGfl37Fiy+5o8+U;>qlssd*cm^1XIwQ_ zkb+dygiTu>N>GI;Y9xx%1)sp4P25znH7OMWHf3cgK|)<|?u^GKk5W_^+xMJv&->ha z&iF^3_abd4MxKvt2AWJ*Oz3sOnSr0FDWLMF+WkWI2QQc%XU zY8$ek;@qGtEYq@=L#U1VRwi(eC(}n@GwGqxIV3aJkjyGf7R^aeL$doTs9MYl_jGH@vSADOyO+oiPO zC+s515HI_lWp;*wlDTa;3h6#uB*WQQ6UwaY&>P32Hx9$rmz_=S3ftIq%{pZ_d_4EP z8?0sRLu!%Av^FiBca(0W$<%=6=4Kno}u zikE_V5l`nY7p0soq_WbyB19>}S-{Fn`s$wUB6pPB^H_`0hEtm>>S@(*;;fDp1w=g4q%lP~jWyDFOHWS@6>$2Bq$@*u zwlMU!cum0}9a>m3S5a#;w`lSP3ky%(CsipsElZ=NvxoHlAkh}h#4Qy~g!v%iVonoN zgg1Cn&8H+)8wJF&^lC{5Sl)^bh*ONNfr1z%DWzrnwR=Ou=ug6c$b= zE?pd(ym0B=ctRYzG#)!GCeBPHrrw^6C(cG0!=I80kd-3li+Z7`8?3He)xpY~lA6b) zfTbSOVkDK%&7@~RGBP8jRYmq{x(qsRc}pIGLQMg634)n>DQqd3#C`>^PLeBm1%W^T zid||G1SZLsG7G2FV6EzCerhATTS6T3X~w1%FBwu66bI+%;Zc6nMEB2 zt10S?;b~OCaG1GaGVU=MkQ$lA5qRFgJYYuTd52pXd66q&x+=omgbVN}6kd(pimi86 zdWXxs!<%o$9`;Ubh2qPes++sfveL5E8GGoS_$~CxYW!CG=Rij_5PB47`+WQ}zlGg2 zF`>r0cz-Q`x+B$Kd$qM=w}Wf(Bfz7Z`L)75j4F??@+GSYYzssRuc-4Q)U*$tL)9Y<_KDhhJCU?9V?x}n7ID+0~ zM~y>Vu;L4sec`ps4}6imo_fEIeHmLD1vh=*#-_kW|3M$>`l)-KyYbDD@lEdi|I5uE zB$XP2xL!N1#Bf$h=R`5;$K(x&$$uqG{-WRxlV8lGb#lotnZ!h#v;g>a zVMWWUSD+gT5>|4$hF>-X5+Hj@(iF@4>tv559&jI^&EQ~YS!s-7GHuBi)MjC+F%(5@ zBmXwyw^48#wZOOLMEsHEb2nluv9-=E?$rv{SLXV*xPb~cQszd!zfk5*EO~aF!<2vR pSf#7K+||E}fT+z;h~<_`511oWHn{ZO^@)!rKAwaUvt4E&e*v9nV0r)m literal 0 HcmV?d00001 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()