From d4ce0cc8828711a27603816f1313283e3b1e8e3a Mon Sep 17 00:00:00 2001 From: trvon Date: Tue, 22 Sep 2026 17:40:59 -0600 Subject: [PATCH 1/3] perf(distances): runtime AVX2+FMA dispatch for float kernels on x86 Portable x86-64 builds leave SQLITE_VEC_ENABLE_AVX off, so float inner product, L2 and cosine ran scalar strict-order loops. Add AVX2+FMA kernels with per-function target attributes (four accumulators) and select them after a one-time CPUID check; baseline TUs stay baseline ISA. Compile-time AVX builds and non-GCC/Clang compilers are unchanged, and SQLITE_VEC_DISABLE_X86_DISPATCH opts out. Threadripper 3960X, -O2, pinned core, 20k rows: 384d inner product 268 -> 67 ns (4.0x), L2 3.9x, cosine 3.4x; 768d 2.8-4.3x. A new test checks the dispatched kernels against the scalar reference for dims 1-70, 384, and 768. --- include/sqlite-vec-cpp/distances/cosine.hpp | 15 ++ .../distances/inner_product.hpp | 7 + include/sqlite-vec-cpp/distances/l2.hpp | 7 + include/sqlite-vec-cpp/simd/x86_dispatch.hpp | 135 ++++++++++++++++++ tests/test_distances.cpp | 63 ++++++-- 5 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 include/sqlite-vec-cpp/simd/x86_dispatch.hpp diff --git a/include/sqlite-vec-cpp/distances/cosine.hpp b/include/sqlite-vec-cpp/distances/cosine.hpp index bb3810f..5b1046d 100644 --- a/include/sqlite-vec-cpp/distances/cosine.hpp +++ b/include/sqlite-vec-cpp/distances/cosine.hpp @@ -16,6 +16,8 @@ #include "../simd/neon.hpp" #endif +#include "../simd/x86_dispatch.hpp" + namespace sqlite_vec_cpp::distances { /// Cosine distance = 1 - cosine_similarity @@ -113,6 +115,19 @@ float cosine_distance(std::span a, std::span b) { if (a.size() >= 16) { return simd::cosine_distance_float_neon(a, b); } +#endif +#ifdef SQLITE_VEC_X86_RUNTIME_DISPATCH + if (a.size() >= 8 && x86::cpu_has_avx2_fma()) { + float dot = 0.0f; + float a_mag = 0.0f; + float b_mag = 0.0f; + x86::cosine_terms_avx2(a.data(), b.data(), a.size(), dot, a_mag, b_mag); + const float denom = std::sqrt(a_mag) * std::sqrt(b_mag); + if (denom < 1e-8f) { + return 1.0f; + } + return 1.0f - (dot / denom); + } #endif return cosine_distance_float(a, b); } else if constexpr (std::is_same_v) { diff --git a/include/sqlite-vec-cpp/distances/inner_product.hpp b/include/sqlite-vec-cpp/distances/inner_product.hpp index a306d91..48defd7 100644 --- a/include/sqlite-vec-cpp/distances/inner_product.hpp +++ b/include/sqlite-vec-cpp/distances/inner_product.hpp @@ -16,6 +16,8 @@ #include "../simd/neon.hpp" #endif +#include "../simd/x86_dispatch.hpp" + namespace sqlite_vec_cpp::distances { /// Inner product (dot product) distance = 1 - dot(a, b) for normalized vectors @@ -161,6 +163,11 @@ float inner_product_distance(std::span a, std::span b) { if (a.size() >= 16) { return simd::inner_product_float_neon(a, b); } +#endif +#ifdef SQLITE_VEC_X86_RUNTIME_DISPATCH + if (a.size() >= 8 && x86::cpu_has_avx2_fma()) { + return 1.0f - x86::dot_avx2(a.data(), b.data(), a.size()); + } #endif return inner_product_distance_float(a, b); } else if constexpr (std::is_same_v) { diff --git a/include/sqlite-vec-cpp/distances/l2.hpp b/include/sqlite-vec-cpp/distances/l2.hpp index 60e793c..fef405c 100644 --- a/include/sqlite-vec-cpp/distances/l2.hpp +++ b/include/sqlite-vec-cpp/distances/l2.hpp @@ -16,6 +16,8 @@ #include "../simd/neon.hpp" #endif +#include "../simd/x86_dispatch.hpp" + namespace sqlite_vec_cpp::distances { /// L2 (Euclidean) distance metric - generic fallback implementation @@ -138,6 +140,11 @@ template float l2_distance(std::span a, std if (a.size() > 16) { return simd::l2_distance_float_neon(a, b); } +#endif +#ifdef SQLITE_VEC_X86_RUNTIME_DISPATCH + if (a.size() >= 8 && x86::cpu_has_avx2_fma()) { + return std::sqrt(x86::l2_squared_avx2(a.data(), b.data(), a.size())); + } #endif return l2_distance_float(a, b); } else if constexpr (std::is_same_v) { diff --git a/include/sqlite-vec-cpp/simd/x86_dispatch.hpp b/include/sqlite-vec-cpp/simd/x86_dispatch.hpp new file mode 100644 index 0000000..79d3d46 --- /dev/null +++ b/include/sqlite-vec-cpp/simd/x86_dispatch.hpp @@ -0,0 +1,135 @@ +#pragma once + +// Runtime-dispatched AVX2+FMA float kernels for portable x86-64 builds. +// +// SQLITE_VEC_ENABLE_AVX compiles AVX kernels into every including TU, which +// requires building the whole consumer for an AVX-capable CPU. Distributed +// binaries instead target baseline x86-64 and previously fell back to scalar +// loops. These kernels carry their own target attribute, so they compile in a +// baseline TU and are only called after a one-time CPUID check confirms +// AVX2 and FMA. Define SQLITE_VEC_DISABLE_X86_DISPATCH to opt out. + +#if !defined(SQLITE_VEC_ENABLE_AVX) && !defined(SQLITE_VEC_DISABLE_X86_DISPATCH) && \ + (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__)) +#define SQLITE_VEC_X86_RUNTIME_DISPATCH 1 + +#include +#include + +#define SQLITE_VEC_TARGET_AVX2_FMA __attribute__((target("avx2,fma"))) + +namespace sqlite_vec_cpp::distances::x86 { + +inline bool cpu_has_avx2_fma() noexcept { + static const bool supported = [] { + __builtin_cpu_init(); + return __builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma"); + }(); + return supported; +} + +SQLITE_VEC_TARGET_AVX2_FMA inline float hsum256(__m256 v) noexcept { + __m128 lo = _mm256_castps256_ps128(v); + const __m128 hi = _mm256_extractf128_ps(v, 1); + lo = _mm_add_ps(lo, hi); + __m128 shuf = _mm_movehdup_ps(lo); + __m128 sums = _mm_add_ps(lo, shuf); + shuf = _mm_movehl_ps(shuf, sums); + sums = _mm_add_ss(sums, shuf); + return _mm_cvtss_f32(sums); +} + +// Four independent accumulators keep the FMA pipes busy; the tail is scalar. +SQLITE_VEC_TARGET_AVX2_FMA inline float dot_avx2(const float* a, const float* b, + std::size_t n) noexcept { + __m256 s0 = _mm256_setzero_ps(); + __m256 s1 = _mm256_setzero_ps(); + __m256 s2 = _mm256_setzero_ps(); + __m256 s3 = _mm256_setzero_ps(); + std::size_t i = 0; + for (; i + 32 <= n; i += 32) { + s0 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i), s0); + s1 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i + 8), _mm256_loadu_ps(b + i + 8), s1); + s2 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i + 16), _mm256_loadu_ps(b + i + 16), s2); + s3 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i + 24), _mm256_loadu_ps(b + i + 24), s3); + } + for (; i + 8 <= n; i += 8) + s0 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i), s0); + float sum = hsum256(_mm256_add_ps(_mm256_add_ps(s0, s1), _mm256_add_ps(s2, s3))); + for (; i < n; ++i) + sum += a[i] * b[i]; + return sum; +} + +SQLITE_VEC_TARGET_AVX2_FMA inline float l2_squared_avx2(const float* a, const float* b, + std::size_t n) noexcept { + __m256 s0 = _mm256_setzero_ps(); + __m256 s1 = _mm256_setzero_ps(); + __m256 s2 = _mm256_setzero_ps(); + __m256 s3 = _mm256_setzero_ps(); + std::size_t i = 0; + for (; i + 32 <= n; i += 32) { + const __m256 d0 = _mm256_sub_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i)); + const __m256 d1 = _mm256_sub_ps(_mm256_loadu_ps(a + i + 8), _mm256_loadu_ps(b + i + 8)); + const __m256 d2 = _mm256_sub_ps(_mm256_loadu_ps(a + i + 16), _mm256_loadu_ps(b + i + 16)); + const __m256 d3 = _mm256_sub_ps(_mm256_loadu_ps(a + i + 24), _mm256_loadu_ps(b + i + 24)); + s0 = _mm256_fmadd_ps(d0, d0, s0); + s1 = _mm256_fmadd_ps(d1, d1, s1); + s2 = _mm256_fmadd_ps(d2, d2, s2); + s3 = _mm256_fmadd_ps(d3, d3, s3); + } + for (; i + 8 <= n; i += 8) { + const __m256 d = _mm256_sub_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i)); + s0 = _mm256_fmadd_ps(d, d, s0); + } + float sum = hsum256(_mm256_add_ps(_mm256_add_ps(s0, s1), _mm256_add_ps(s2, s3))); + for (; i < n; ++i) { + const float d = a[i] - b[i]; + sum += d * d; + } + return sum; +} + +// Accumulates dot(a,b), |a|^2 and |b|^2 in one pass for cosine distance. +SQLITE_VEC_TARGET_AVX2_FMA inline void cosine_terms_avx2(const float* a, const float* b, + std::size_t n, float& dot, float& aa, + float& bb) noexcept { + __m256 sd0 = _mm256_setzero_ps(); + __m256 sd1 = _mm256_setzero_ps(); + __m256 sa0 = _mm256_setzero_ps(); + __m256 sa1 = _mm256_setzero_ps(); + __m256 sb0 = _mm256_setzero_ps(); + __m256 sb1 = _mm256_setzero_ps(); + std::size_t i = 0; + for (; i + 16 <= n; i += 16) { + const __m256 a0 = _mm256_loadu_ps(a + i); + const __m256 b0 = _mm256_loadu_ps(b + i); + const __m256 a1 = _mm256_loadu_ps(a + i + 8); + const __m256 b1 = _mm256_loadu_ps(b + i + 8); + sd0 = _mm256_fmadd_ps(a0, b0, sd0); + sd1 = _mm256_fmadd_ps(a1, b1, sd1); + sa0 = _mm256_fmadd_ps(a0, a0, sa0); + sa1 = _mm256_fmadd_ps(a1, a1, sa1); + sb0 = _mm256_fmadd_ps(b0, b0, sb0); + sb1 = _mm256_fmadd_ps(b1, b1, sb1); + } + for (; i + 8 <= n; i += 8) { + const __m256 a0 = _mm256_loadu_ps(a + i); + const __m256 b0 = _mm256_loadu_ps(b + i); + sd0 = _mm256_fmadd_ps(a0, b0, sd0); + sa0 = _mm256_fmadd_ps(a0, a0, sa0); + sb0 = _mm256_fmadd_ps(b0, b0, sb0); + } + dot = hsum256(_mm256_add_ps(sd0, sd1)); + aa = hsum256(_mm256_add_ps(sa0, sa1)); + bb = hsum256(_mm256_add_ps(sb0, sb1)); + for (; i < n; ++i) { + dot += a[i] * b[i]; + aa += a[i] * a[i]; + bb += b[i] * b[i]; + } +} + +} // namespace sqlite_vec_cpp::distances::x86 + +#endif diff --git a/tests/test_distances.cpp b/tests/test_distances.cpp index 532a639..b4f0d54 100644 --- a/tests/test_distances.cpp +++ b/tests/test_distances.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -273,17 +274,17 @@ void test_int8_neon_consistency() { } // Cosine: dispatch (NEON) vs scalar - float cosine_dispatch = cosine_distance(std::span(a), - std::span(b)); - float cosine_scalar = cosine_distance_int(std::span(a), - std::span(b)); + float cosine_dispatch = + cosine_distance(std::span(a), std::span(b)); + float cosine_scalar = + cosine_distance_int(std::span(a), std::span(b)); assert(approx_equal(cosine_dispatch, cosine_scalar, 1e-4f)); assert(!std::isnan(cosine_dispatch)); assert(cosine_dispatch >= 0.0f && cosine_dispatch <= 2.0f); // Inner product: dispatch (NEON) vs scalar - float ip_dispatch = inner_product_distance(std::span(a), - std::span(b)); + float ip_dispatch = + inner_product_distance(std::span(a), std::span(b)); float ip_scalar = inner_product_distance_int(std::span(a), std::span(b)); assert(approx_equal(ip_dispatch, ip_scalar, 1e-4f)); @@ -297,16 +298,16 @@ void test_int8_neon_consistency() { vb[i] = static_cast((i * 9 + 17) % 255 - 127); } - float cos_d = cosine_distance(std::span(va), - std::span(vb)); - float cos_s = cosine_distance_int(std::span(va), - std::span(vb)); + float cos_d = + cosine_distance(std::span(va), std::span(vb)); + float cos_s = + cosine_distance_int(std::span(va), std::span(vb)); assert(approx_equal(cos_d, cos_s, 1e-4f)); float ip_d = inner_product_distance(std::span(va), - std::span(vb)); + std::span(vb)); float ip_s = inner_product_distance_int(std::span(va), - std::span(vb)); + std::span(vb)); assert(approx_equal(ip_d, ip_s, 1e-4f)); } @@ -316,6 +317,43 @@ void test_int8_neon_consistency() { #endif } +// Float dispatch (NEON, compile-time AVX, or x86 runtime AVX2) must agree with the scalar +// reference within float rounding for every length, including sub-vector tails. +void test_float_dispatch_matches_scalar() { + std::cout << "Testing float dispatch vs scalar reference..." << std::endl; +#ifdef SQLITE_VEC_X86_RUNTIME_DISPATCH + std::cout << " x86 runtime dispatch compiled in; AVX2+FMA " + << (x86::cpu_has_avx2_fma() ? "available" : "unavailable") << std::endl; +#endif + std::mt19937 rng(1234); + std::normal_distribution dist(0.0f, 1.0f); + std::vector sizes; + for (std::size_t n = 1; n <= 70; ++n) + sizes.push_back(n); + sizes.push_back(384); + sizes.push_back(768); + for (const std::size_t n : sizes) { + std::vector a(n); + std::vector b(n); + for (std::size_t i = 0; i < n; ++i) { + a[i] = dist(rng); + b[i] = dist(rng); + } + const std::span sa(a); + const std::span sb(b); + const float tol = 1e-4f * static_cast(n); + assert(std::abs(inner_product_distance(sa, sb) - inner_product_distance_float(sa, sb)) <= + tol); + assert(std::abs(l2_distance(sa, sb) - l2_distance_float(sa, sb)) <= tol); + assert(std::abs(cosine_distance(sa, sb) - cosine_distance_float(sa, sb)) <= 1e-5f); + assert(std::abs(cosine_distance(sa, sa)) <= 1e-5f); + } + std::vector zeros(64, 0.0f); + std::vector ones(64, 1.0f); + assert(cosine_distance(std::span(zeros), std::span(ones)) == 1.0f); + std::cout << " ✓ float dispatch matches scalar" << std::endl; +} + int main() { try { test_l2_distance(); @@ -326,6 +364,7 @@ int main() { test_metric_traits(); test_simd_consistency(); test_int8_neon_consistency(); + test_float_dispatch_matches_scalar(); std::cout << "\nAll distance metric tests passed! ✓" << std::endl; return 0; From fb7da53b56d4bcadd1f1dca50e07f10377659b53 Mon Sep 17 00:00:00 2001 From: trvon Date: Tue, 22 Sep 2026 18:58:15 -0600 Subject: [PATCH 2/3] fix(sqlite): format size_t error details with %llu in sqlite3_mprintf SQLite's printf has no z length modifier: "%zu" parses as its %z (free-after-use string) conversion, so every error path that formatted a size_t dereferenced the integer as a char* and crashed. That turned invalid input into a segfault instead of an error, e.g. CREATE VIRTUAL TABLE ... vec0(embedding float[0]) and loading an HNSW index with a dangling entry point. Use %llu with explicit casts; the existing test_overflow and test_persistence_fuzz cases now pass instead of crashing. --- .../sqlite-vec-cpp/index/hnsw_persistence.hpp | 19 ++++++++++++------- include/sqlite-vec-cpp/sqlite/vec0_module.hpp | 17 ++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/include/sqlite-vec-cpp/index/hnsw_persistence.hpp b/include/sqlite-vec-cpp/index/hnsw_persistence.hpp index 2984e62..ca5d6ed 100644 --- a/include/sqlite-vec-cpp/index/hnsw_persistence.hpp +++ b/include/sqlite-vec-cpp/index/hnsw_persistence.hpp @@ -433,7 +433,8 @@ int save_hnsw_index(sqlite3* db, const char* schema, const char* table, sqlite3_finalize(stmt); sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); if (pzErr) - *pzErr = sqlite3_mprintf("Failed to save HNSW node %zu", failedNodeId); + *pzErr = sqlite3_mprintf("Failed to save HNSW node %llu", + static_cast(failedNodeId)); return rc; } @@ -530,7 +531,8 @@ HNSWIndex load_hnsw_index(sqlite3* db, const char* schema, const char if (node.id != node_id) { sqlite3_finalize(stmt); if (pzErr) - *pzErr = sqlite3_mprintf("HNSW node id mismatch for rowid %zu", node_id); + *pzErr = sqlite3_mprintf("HNSW node id mismatch for rowid %llu", + static_cast(node_id)); throw std::runtime_error("HNSW node id mismatch"); } nodes.emplace(node_id, std::move(node)); @@ -546,14 +548,15 @@ HNSWIndex load_hnsw_index(sqlite3* db, const char* schema, const char if (!nodes.empty() && nodes.find(entry_point_id) == nodes.end()) { if (pzErr) - *pzErr = sqlite3_mprintf("HNSW entry point %zu missing from nodes", entry_point_id); + *pzErr = sqlite3_mprintf("HNSW entry point %llu missing from nodes", + static_cast(entry_point_id)); throw std::runtime_error("HNSW entry point missing from nodes"); } for (auto& [id, node] : nodes) { for (auto& layer : node.edges) { - std::erase_if(layer, - [&](size_t neighbor_id) { return nodes.find(neighbor_id) == nodes.end(); }); + std::erase_if( + layer, [&](size_t neighbor_id) { return nodes.find(neighbor_id) == nodes.end(); }); } } @@ -606,7 +609,8 @@ int save_hnsw_node_incremental(sqlite3* db, const char* schema, const char* tabl sqlite3_finalize(stmt); if (rc != SQLITE_DONE && pzErr) { - *pzErr = sqlite3_mprintf("Failed to save HNSW node %zu", node.id); + *pzErr = sqlite3_mprintf("Failed to save HNSW node %llu", + static_cast(node.id)); } return rc; @@ -657,7 +661,8 @@ int save_hnsw_nodes_incremental(sqlite3* db, const char* schema, const char* tab sqlite3_finalize(stmt); sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); if (pzErr) - *pzErr = sqlite3_mprintf("Failed to save HNSW node %zu", node.id); + *pzErr = sqlite3_mprintf("Failed to save HNSW node %llu", + static_cast(node.id)); return rc; } } diff --git a/include/sqlite-vec-cpp/sqlite/vec0_module.hpp b/include/sqlite-vec-cpp/sqlite/vec0_module.hpp index 569768e..aaf9c14 100644 --- a/include/sqlite-vec-cpp/sqlite/vec0_module.hpp +++ b/include/sqlite-vec-cpp/sqlite/vec0_module.hpp @@ -16,9 +16,9 @@ #include #include "../distances/l2.hpp" #include "../index/hnsw.hpp" +#include "../index/hnsw_persistence.hpp" #include "../utils/error.hpp" #include "parsers.hpp" -#include "../index/hnsw_persistence.hpp" #include #include "value.hpp" @@ -94,9 +94,8 @@ inline void vec0_registry_remove(sqlite3* db, std::string_view schema_name, vec0_table_registry().erase(vec0_registry_key(db, schema_name, table_name)); } template -inline auto vec0_with_table(sqlite3* db, std::string_view schema_name, - std::string_view table_name, Fn&& fn) - -> decltype(fn(static_cast(nullptr))) { +inline auto vec0_with_table(sqlite3* db, std::string_view schema_name, std::string_view table_name, + Fn&& fn) -> decltype(fn(static_cast(nullptr))) { std::lock_guard lk(vec0_registry_mutex()); auto& reg = vec0_table_registry(); auto it = reg.find(vec0_registry_key(db, schema_name, table_name)); @@ -341,9 +340,8 @@ vec0_run_ann_query(Vec0Table* table, const Value& query_value, size_t k, size_t const size_t entry_count = std::min(kMaxRouteEntryPoints, ordered_rowids.size()); route_entry_points.reserve(entry_count); for (size_t i = 0; i < entry_count; ++i) { - const size_t index = entry_count == 1 - ? 0 - : i * (ordered_rowids.size() - 1) / (entry_count - 1); + const size_t index = + entry_count == 1 ? 0 : i * (ordered_rowids.size() - 1) / (entry_count - 1); route_entry_points.push_back(static_cast(ordered_rowids[index])); } } @@ -605,8 +603,9 @@ inline int vec0Create(sqlite3* db, void* pAux, int argc, const char* const* argv parse_vec0_schema(argc, argv, embedding_col, dims); if (dims == 0 || dims > kMaxVec0Dimensions) { - *pzErr = sqlite3_mprintf("vec0: dimensions must be in [1, %zu], got %zu", - kMaxVec0Dimensions, dims); + *pzErr = sqlite3_mprintf("vec0: dimensions must be in [1, %llu], got %llu", + static_cast(kMaxVec0Dimensions), + static_cast(dims)); return SQLITE_ERROR; } if (embedding_col.empty() || embedding_col.find('"') != std::string::npos) { From 0fec31ca73f8191dbcf0625d2e802dd96f49d731 Mon Sep 17 00:00:00 2001 From: trvon Date: Tue, 22 Sep 2026 18:58:16 -0600 Subject: [PATCH 3/3] test(quantization): correct the RaBitQ memory-savings expectation 384 dims pad to 512 for the FWHT rotation, so each vector costs 64 code bytes plus 8 bytes of factors (21.3x vs FP32), and with 100 vectors the fixed centroid/rotation state brings the total to ~17x. The old "> 20x (~32x expected)" bound ignored padding and fixed cost and failed on every platform; assert the achievable range instead. --- tests/test_quantization.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_quantization.cpp b/tests/test_quantization.cpp index 7e0b67a..320af68 100644 --- a/tests/test_quantization.cpp +++ b/tests/test_quantization.cpp @@ -571,7 +571,13 @@ void test_two_stage_memory_savings() { } else if (qtype == QuantizationType::LVQ4) { assert(compression > 6.0f); // ~8x expected } else if (qtype == QuantizationType::RaBitQ) { - assert(compression > 20.0f); // ~32x expected + // 384 dims pad to 512 for the FWHT rotation: 64 code bytes + 8 bytes of per-vector + // factors = 72 B vs 1536 B FP32 (21.3x per vector). With only 100 vectors the fixed + // centroid + rotation state (~1.7 KB) brings the total to ~17x. + assert(compression > 16.0f); + const float per_vector_bound = static_cast(dim * sizeof(float)) / + static_cast(512 / 8 + 2 * sizeof(float)); + assert(compression < per_vector_bound); } }