Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions include/sqlite-vec-cpp/distances/cosine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include "../simd/neon.hpp"
#endif

#include "../simd/x86_dispatch.hpp"

namespace sqlite_vec_cpp::distances {

/// Cosine distance = 1 - cosine_similarity
Expand Down Expand Up @@ -113,6 +115,19 @@ float cosine_distance(std::span<const T> a, std::span<const T> 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<T, std::int8_t>) {
Expand Down
7 changes: 7 additions & 0 deletions include/sqlite-vec-cpp/distances/inner_product.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -161,6 +163,11 @@ float inner_product_distance(std::span<const T> a, std::span<const T> 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<T, std::int8_t>) {
Expand Down
7 changes: 7 additions & 0 deletions include/sqlite-vec-cpp/distances/l2.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,6 +140,11 @@ template <concepts::VectorElement T> float l2_distance(std::span<const T> 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<T, std::int8_t>) {
Expand Down
19 changes: 12 additions & 7 deletions include/sqlite-vec-cpp/index/hnsw_persistence.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned long long>(failedNodeId));
return rc;
}

Expand Down Expand Up @@ -530,7 +531,8 @@ HNSWIndex<T, Metric> 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<unsigned long long>(node_id));
throw std::runtime_error("HNSW node id mismatch");
}
nodes.emplace(node_id, std::move(node));
Expand All @@ -546,14 +548,15 @@ HNSWIndex<T, Metric> 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<unsigned long long>(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(); });
}
}

Expand Down Expand Up @@ -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<unsigned long long>(node.id));
}

return rc;
Expand Down Expand Up @@ -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<unsigned long long>(node.id));
return rc;
}
}
Expand Down
135 changes: 135 additions & 0 deletions include/sqlite-vec-cpp/simd/x86_dispatch.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <immintrin.h>

#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
17 changes: 8 additions & 9 deletions include/sqlite-vec-cpp/sqlite/vec0_module.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
#include <vector>
#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 <unordered_map>
#include "value.hpp"
Expand Down Expand Up @@ -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 <typename Fn>
inline auto vec0_with_table(sqlite3* db, std::string_view schema_name,
std::string_view table_name, Fn&& fn)
-> decltype(fn(static_cast<Vec0Table*>(nullptr))) {
inline auto vec0_with_table(sqlite3* db, std::string_view schema_name, std::string_view table_name,
Fn&& fn) -> decltype(fn(static_cast<Vec0Table*>(nullptr))) {
std::lock_guard<std::mutex> lk(vec0_registry_mutex());
auto& reg = vec0_table_registry();
auto it = reg.find(vec0_registry_key(db, schema_name, table_name));
Expand Down Expand Up @@ -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<size_t>(ordered_rowids[index]));
}
}
Expand Down Expand Up @@ -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<unsigned long long>(kMaxVec0Dimensions),
static_cast<unsigned long long>(dims));
return SQLITE_ERROR;
}
if (embedding_col.empty() || embedding_col.find('"') != std::string::npos) {
Expand Down
Loading
Loading