// Catch2 unit tests for TurboQuant MSE quantizer // Tests the TurboQuant_MSE class from turboquant.h // Paper: arXiv:2504.19874 - TurboQuant #include #include #include #include #include #include #include using namespace yams::vector; namespace { struct TurboQuantMSEFixture { TurboQuantMSEFixture() : rng(42) {} std::mt19937 rng; // Generate random unit vector std::vector generateUnitVector(size_t dim) { std::vector v(dim); std::normal_distribution dist(0.0f, 1.0f); float norm_sq = 0.0f; for (size_t i = 0; i < dim; ++i) { v[i] = dist(rng); norm_sq += v[i] * v[i]; } float norm = std::sqrt(norm_sq); for (size_t i = 0; i < dim; ++i) { v[i] /= norm; } return v; } // L2 distance between vectors float l2Distance(const std::vector& a, const std::vector& b) { float sum = 0.0f; for (size_t i = 0; i < a.size(); ++i) { float diff = a[i] - b[i]; sum += diff * diff; } return std::sqrt(sum); } // MSE between vectors double mse(const std::vector& a, const std::vector& b) { double sum = 0.0; for (size_t i = 0; i < a.size(); ++i) { double diff = static_cast(a[i]) - static_cast(b[i]); sum += diff * diff; } return sum / a.size(); } }; // Theoretical MSE bounds from paper (Table 1) const std::vector kMSEBounds = { 0.36, // b=1 0.117, // b=2 0.03, // b=3 0.009 // b=4 }; } // namespace // ============================================================================= // Test Group: Basic Encode/Decode // ============================================================================= TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE basic encode/decode round-trip", "[turboquant][mse][basic][catch2]") { TurboQuantConfig config; config.dimension = 128; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(128); std::vector indices = quantizer.encode(original); std::vector reconstructed = quantizer.decode(indices); // Should reconstruct to approximately the same vector // With uniform quantization, expect some distortion float dist = l2Distance(original, reconstructed); INFO("Distance: " << dist); REQUIRE(dist < 1.0f); // Reasonable bound for 4-bit quantization } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE correct index count", "[turboquant][mse][basic][catch2]") { TurboQuantConfig config; config.dimension = 256; config.bits_per_channel = 3; config.seed = 42; TurboQuantMSE quantizer(config); std::vector v = generateUnitVector(256); std::vector indices = quantizer.encode(v); // Should have one index per dimension REQUIRE(indices.size() == 256); // Each index should fit in 3 bits (0-7) for (size_t i = 0; i < indices.size(); ++i) { REQUIRE(indices[i] < 8); } } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE storage size calculation", "[turboquant][mse][basic][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); // 384 dimensions × 4 bits = 1536 bits = 192 bytes REQUIRE(quantizer.storageSize() == 192); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE packed encode size matches storageSize", "[turboquant][mse][basic][packed][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector v(384, 0.0f); v[0] = 1.0f; // Packed encode should return exactly storageSize() bytes auto packed = quantizer.packedEncode(v); REQUIRE(packed.size() == quantizer.storageSize()); REQUIRE(packed.size() == 192); // 384 * 4 / 8 = 192 bytes // Verify packed decode round-trips correctly auto decoded = quantizer.packedDecode(packed); REQUIRE(decoded.size() == 384); // Check reconstruction is valid (norm should be ~1.0 for unit vector) double norm_sq = 0.0; for (size_t i = 0; i < decoded.size(); ++i) { norm_sq += static_cast(decoded[i]) * static_cast(decoded[i]); } double norm = std::sqrt(norm_sq); REQUIRE(norm > 0.9); // Should be close to 1.0 } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE 2-bit storage size", "[turboquant][mse][basic][catch2]") { TurboQuantConfig config; config.dimension = 1536; config.bits_per_channel = 2; config.seed = 42; TurboQuantMSE quantizer(config); // 1536 dimensions × 2 bits = 3072 bits = 384 bytes (vs 1536 for 8-bit) REQUIRE(quantizer.storageSize() == 384); } // ============================================================================= // Test Group: MSE Bounds (Theoretical Guarantees) // ============================================================================= TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE 1-bit MSE bound", "[turboquant][mse][bounds][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 1; config.seed = 42; TurboQuantMSE quantizer(config); // Test on 100 random vectors and check average MSE double total_mse = 0.0; constexpr size_t kNumVectors = 100; for (size_t i = 0; i < kNumVectors; ++i) { std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); total_mse += mse(original, reconstructed); } double avg_mse = total_mse / kNumVectors; // From paper: 1-bit MSE ≈ 0.36 // Allow some margin for finite dimension effects INFO("Average MSE at 1-bit: " << avg_mse); REQUIRE(avg_mse < 0.40); // Loose bound REQUIRE(avg_mse < kMSEBounds[0] * 1.5); // Within 1.5x of theoretical } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE 2-bit MSE bound", "[turboquant][mse][bounds][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 2; config.seed = 42; TurboQuantMSE quantizer(config); double total_mse = 0.0; constexpr size_t kNumVectors = 100; for (size_t i = 0; i < kNumVectors; ++i) { std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); total_mse += mse(original, reconstructed); } double avg_mse = total_mse / kNumVectors; // From paper: 2-bit MSE ≈ 0.117 INFO("Average MSE at 2-bit: " << avg_mse); REQUIRE(avg_mse < 0.15); REQUIRE(avg_mse < kMSEBounds[1] * 1.5); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE 3-bit MSE bound", "[turboquant][mse][bounds][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 3; config.seed = 42; TurboQuantMSE quantizer(config); double total_mse = 0.0; constexpr size_t kNumVectors = 100; for (size_t i = 0; i < kNumVectors; ++i) { std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); total_mse += mse(original, reconstructed); } double avg_mse = total_mse / kNumVectors; // From paper: 3-bit MSE ≈ 0.03 INFO("Average MSE at 3-bit: " << avg_mse); REQUIRE(avg_mse < 0.05); REQUIRE(avg_mse < kMSEBounds[2] * 1.5); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE 4-bit MSE bound", "[turboquant][mse][bounds][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); double total_mse = 0.0; constexpr size_t kNumVectors = 100; for (size_t i = 0; i < kNumVectors; ++i) { std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); total_mse += mse(original, reconstructed); } double avg_mse = total_mse / kNumVectors; // From paper: 4-bit MSE ≈ 0.009 INFO("Average MSE at 4-bit: " << avg_mse); REQUIRE(avg_mse < 0.02); REQUIRE(avg_mse < kMSEBounds[3] * 2.0); // More relaxed due to finite effects } // ============================================================================= // Test Group: Different Dimensions // ============================================================================= TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE dimension 128", "[turboquant][mse][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 128; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(128); std::vector reconstructed = quantizer.roundTrip(original); double mse_val = mse(original, reconstructed); REQUIRE(mse_val < 0.05); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE dimension 768", "[turboquant][mse][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 768; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(768); std::vector reconstructed = quantizer.roundTrip(original); double mse_val = mse(original, reconstructed); REQUIRE(mse_val < 0.02); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE dimension 1536", "[turboquant][mse][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 1536; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(1536); std::vector reconstructed = quantizer.roundTrip(original); double mse_val = mse(original, reconstructed); REQUIRE(mse_val < 0.015); } // ============================================================================= // Test Group: Reproducibility // ============================================================================= TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE same seed produces same results", "[turboquant][mse][reproducibility][catch2]") { TurboQuantConfig config1; config1.dimension = 256; config1.bits_per_channel = 4; config1.seed = 12345; TurboQuantConfig config2; config2.dimension = 256; config2.bits_per_channel = 4; config2.seed = 12345; TurboQuantMSE q1(config1); TurboQuantMSE q2(config2); std::vector v = generateUnitVector(256); std::vector idx1 = q1.encode(v); std::vector idx2 = q2.encode(v); REQUIRE(idx1 == idx2); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE different seeds produce different results", "[turboquant][mse][reproducibility][catch2]") { TurboQuantConfig config1; config1.dimension = 256; config1.bits_per_channel = 4; config1.seed = 12345; TurboQuantConfig config2; config2.dimension = 256; config2.bits_per_channel = 4; config2.seed = 54321; TurboQuantMSE q1(config1); TurboQuantMSE q2(config2); std::vector v = generateUnitVector(256); std::vector idx1 = q1.encode(v); std::vector idx2 = q2.encode(v); // With high probability, these should differ bool all_same = (idx1 == idx2); INFO("Different seeds produced same result: " << all_same); // This test is probabilistic; there's a tiny chance they could match } // ============================================================================= // Test Group: Edge Cases // ============================================================================= TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE zero vector", "[turboquant][mse][edge][catch2]") { TurboQuantConfig config; config.dimension = 128; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector zero(128, 0.0f); // Should not crash std::vector indices = quantizer.encode(zero); REQUIRE(indices.size() == 128); std::vector reconstructed = quantizer.decode(indices); REQUIRE(reconstructed.size() == 128); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE near-zero vector", "[turboquant][mse][edge][catch2]") { TurboQuantConfig config; config.dimension = 64; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector near_zero(64, 1e-6f); // Should not crash std::vector indices = quantizer.encode(near_zero); REQUIRE(indices.size() == 64); } TEST_CASE_METHOD(TurboQuantMSEFixture, "TurboQuantMSE high dimensionality", "[turboquant][mse][edge][catch2]") { TurboQuantConfig config; config.dimension = 3072; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(3072); std::vector indices = quantizer.encode(original); std::vector reconstructed = quantizer.decode(indices); double mse_val = mse(original, reconstructed); INFO("MSE at dimension 3072: " << mse_val); REQUIRE(mse_val < 0.02); // Should still be low }