diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
index c75b6f39b..0b6bb3322 100644
--- a/.github/workflows/unit-tests.yaml
+++ b/.github/workflows/unit-tests.yaml
@@ -45,12 +45,38 @@ jobs:
- name: Set up GCC
run: |
sudo apt install -y gcc
- - name: Install Meson and Ninja
+ - name: Install Meson, Ninja, and GTest
run: |
- sudo apt update && sudo apt install -y meson ninja-build
+ sudo apt update && sudo apt install -y meson ninja-build pkg-config libgtest-dev
- uses: actions/checkout@v4
- name: Initialize Git Submodules
run: git submodule update --init
+
+ - name: Build test_simd_kernels (native C++)
+ working-directory: jvector-native/src/main/native
+ run: |
+ meson setup build --wipe
+ ninja -C build test_simd_kernels
+
+ - name: Run test_simd_kernels — no ISA cap (auto-detect)
+ if: matrix.max_isa == 'avx512f'
+ working-directory: jvector-native/src/main/native
+ run: ./build/test_simd_kernels
+
+ - name: Run test_simd_kernels — capped at avx2
+ if: matrix.max_isa == 'avx2'
+ working-directory: jvector-native/src/main/native
+ env:
+ JVECTOR_MAX_ISA: avx2
+ run: ./build/test_simd_kernels
+
+ - name: Run test_simd_kernels — capped at sse42
+ if: matrix.max_isa == 'sse42'
+ working-directory: jvector-native/src/main/native
+ env:
+ JVECTOR_MAX_ISA: sse42
+ run: ./build/test_simd_kernels
+
- name: Set up JDK ${{ matrix.jdk }}
uses: actions/setup-java@v3
with:
diff --git a/jvector-native/pom.xml b/jvector-native/pom.xml
index ab0090d8d..88073e998 100644
--- a/jvector-native/pom.xml
+++ b/jvector-native/pom.xml
@@ -141,7 +141,7 @@
${native.buildtype}
false
- ${project.basedir}/src/main/native/
+ ${project.basedir}/src/main/native/src/
diff --git a/jvector-native/src/main/native/benchmarks/bench_similarity_f32.cpp b/jvector-native/src/main/native/benchmarks/bench_similarity_f32.cpp
new file mode 100644
index 000000000..b5e31f0cc
--- /dev/null
+++ b/jvector-native/src/main/native/benchmarks/bench_similarity_f32.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Google Benchmark micro-benchmarks for the fp32 vector similarity kernels:
+// cosine_f32, dot_product_f32, euclidean_f32
+//
+// Parameterised over the realistic embedding dimensions used in production:
+// 128, 256, 512, 1024, 1536, 3072
+//
+// Build (requires google-benchmark installed or available via pkg-config):
+// meson setup build && ninja -C build bench_simd_kernels
+//
+// Run:
+// ./build/bench_simd_kernels [--benchmark_filter=]
+
+#include
+#include
+#include
+
+#include "jvector_simd.h"
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+// Deterministic, non-zero float vector: avoids degenerate cosine=NaN cases.
+static std::vector make_vec(size_t n, float seed)
+{
+ std::vector v(n);
+ for (size_t i = 0; i < n; ++i) {
+ v[i] = seed * (1.0f + static_cast(i % 7) * 0.13f);
+ if (i % 3 == 0) v[i] = -v[i];
+ v[i] += 0.5f;
+ }
+ return v;
+}
+
+// Benchmark sizes matching production embedding dimensions.
+static const std::vector kBenchSizes = {128, 256, 512, 1024, 1536, 3072};
+
+// ---------------------------------------------------------------------------
+// dot_product_f32
+// ---------------------------------------------------------------------------
+
+static void BM_dot_product_f32(benchmark::State& state)
+{
+ const size_t n = static_cast(state.range(0));
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ for (auto _ : state) {
+ float result = dot_product_f32(a.data(), 0, b.data(), 0, n);
+ benchmark::DoNotOptimize(result);
+ }
+
+ state.SetItemsProcessed(state.iterations() * static_cast(n));
+ state.SetBytesProcessed(state.iterations() * static_cast(n) * 2 * sizeof(float));
+}
+BENCHMARK(BM_dot_product_f32)->ArgsProduct({kBenchSizes});
+
+// ---------------------------------------------------------------------------
+// euclidean_f32
+// ---------------------------------------------------------------------------
+
+static void BM_euclidean_f32(benchmark::State& state)
+{
+ const size_t n = static_cast(state.range(0));
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ for (auto _ : state) {
+ float result = euclidean_f32(a.data(), 0, b.data(), 0, n);
+ benchmark::DoNotOptimize(result);
+ }
+
+ state.SetItemsProcessed(state.iterations() * static_cast(n));
+ state.SetBytesProcessed(state.iterations() * static_cast(n) * 2 * sizeof(float));
+}
+BENCHMARK(BM_euclidean_f32)->ArgsProduct({kBenchSizes});
+
+// ---------------------------------------------------------------------------
+// cosine_f32
+// ---------------------------------------------------------------------------
+
+static void BM_cosine_f32(benchmark::State& state)
+{
+ const size_t n = static_cast(state.range(0));
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ for (auto _ : state) {
+ float result = cosine_f32(a.data(), 0, b.data(), 0, n);
+ benchmark::DoNotOptimize(result);
+ }
+
+ state.SetItemsProcessed(state.iterations() * static_cast(n));
+ state.SetBytesProcessed(state.iterations() * static_cast(n) * 2 * sizeof(float));
+}
+BENCHMARK(BM_cosine_f32)->ArgsProduct({kBenchSizes});
+
+// ---------------------------------------------------------------------------
+// Entry point — benchmark::Initialize parses --benchmark_* flags.
+// ---------------------------------------------------------------------------
+
+BENCHMARK_MAIN();
diff --git a/jvector-native/src/main/native/meson.build b/jvector-native/src/main/native/meson.build
index 9744ddb69..79b591829 100644
--- a/jvector-native/src/main/native/meson.build
+++ b/jvector-native/src/main/native/meson.build
@@ -54,7 +54,7 @@ isa_libs = []
foreach isa : isa_variants
lib = static_library(
'simdKernels_' + isa['name'],
- sources : 'jvector_simd_kernels.cpp',
+ sources : 'src/jvector_simd_kernels.cpp',
include_directories: hwy_inc,
cpp_args : isa['args'] + ['-DJV_ISA=' + isa['namespace'], '-fvisibility=hidden']
)
@@ -65,7 +65,7 @@ endforeach
# set (AVX3 + VNNI, VBMI, VBMI2, IFMA, BITALG, VPOPCNTDQ, GFNI, VAES, VPCLMULQDQ).
avx3_dl_lib = static_library(
'simdKernels_avx3_dl',
- sources : 'jvector_avx3_dl_kernels.cpp',
+ sources : 'src/jvector_avx3_dl_kernels.cpp',
include_directories: hwy_inc,
cpp_args : [
'-march=icelake-server',
@@ -81,7 +81,7 @@ isa_libs += avx3_dl_lib
# Requires GCC >= 12 or Clang >= 14.
avx3_spr_lib = static_library(
'simdKernels_avx3_spr',
- sources : 'jvector_avx3_spr_kernels.cpp',
+ sources : 'src/jvector_avx3_spr_kernels.cpp',
include_directories: hwy_inc,
cpp_args : [
'-march=sapphirerapids',
@@ -100,9 +100,9 @@ isa_libs += avx3_spr_lib
# projects that manage their own dispatch (google/highway#1935).
vectorutil_lib = shared_library(
'jvector',
- sources : ['jvector_simd.cpp',
+ sources : ['src/jvector_simd.cpp',
'third_party/highway/hwy/abort.cc'],
- include_directories: [include_directories('.'), hwy_inc],
+ include_directories: [include_directories('src'), hwy_inc],
cpp_args : ['-DJVECTOR_BUILD', '-fvisibility=hidden'],
link_whole : isa_libs,
version : meson.project_version(),
@@ -112,7 +112,7 @@ vectorutil_lib = shared_library(
# Dependency object for use by executables/tests in this build tree.
vectorutil_dep = declare_dependency(
link_with : vectorutil_lib,
- include_directories: include_directories('.'),
+ include_directories: include_directories('src'),
)
## Example driver that exercises the runtime-dispatch API.
@@ -121,40 +121,33 @@ vectorutil_dep = declare_dependency(
# sources : 'examples/cpp_driver.cpp',
# dependencies: vectorutil_dep,
#)
-#
-## ---- Tests -----------------------------------------------------------------
-#gtest_dep = dependency('gtest_main', required: true)
-#
-#test_exe = executable(
-# 'test_kernels',
-# sources : [
-# 'tests/test_kernels.cpp',
-# 'tests/test_cpuFeatures.cpp',
-# ],
-# dependencies: [vectorutil_dep, gtest_dep],
-#)
-#
-#test('kernels', test_exe, protocol: 'gtest', suite: 'kernels')
-#test('cpu_features', test_exe, protocol: 'gtest', suite: 'cpu',
-# args: ['--gtest_filter=CpuFeaturesTest.*'])
-#
-## ---- Benchmarks ------------------------------------------------------------
-#gbench_dep = dependency('benchmark', required: false)
-#if gbench_dep.found()
-# executable(
-# 'bench_kernels',
-# sources : 'benchmarks/bench_kernels.cpp',
-# dependencies: [vectorutil_dep, gbench_dep],
-# cpp_args : ['-O3'],
-# )
-#endif
-#
-#rust_enabled = add_languages('rust', required: false)
-#if rust_enabled
-# executable(
-# 'rust_driver',
-# 'examples/rust_driver.rs',
-# link_with: vectorutil_lib,
-# )
-#endif
-#
\ No newline at end of file
+
+# ---- Tests -----------------------------------------------------------------
+gtest_dep = dependency('gtest_main', required: false)
+
+if gtest_dep.found()
+ simd_kernels_test = executable(
+ 'test_simd_kernels',
+ sources : [
+ 'tests/test_helpers.cpp',
+ 'tests/test_similarity.cpp',
+ 'tests/test_elementwise.cpp',
+ 'tests/test_cpu_features.cpp',
+ ],
+ dependencies: [vectorutil_dep, gtest_dep],
+ )
+
+ test('simd_kernels', simd_kernels_test, protocol: 'gtest', suite: 'simd_kernels')
+
+endif
+# ---- Benchmarks ------------------------------------------------------------
+gbench_dep = dependency('benchmark', required: false)
+
+if gbench_dep.found()
+ executable(
+ 'bench_simd_kernels',
+ sources : 'benchmarks/bench_similarity_f32.cpp',
+ dependencies: [vectorutil_dep, gbench_dep],
+ cpp_args : ['-O3'],
+ )
+endif
\ No newline at end of file
diff --git a/jvector-native/src/main/native/assert_hwy_targets.h b/jvector-native/src/main/native/src/assert_hwy_targets.h
similarity index 100%
rename from jvector-native/src/main/native/assert_hwy_targets.h
rename to jvector-native/src/main/native/src/assert_hwy_targets.h
diff --git a/jvector-native/src/main/native/jextract_vector_simd.sh b/jvector-native/src/main/native/src/jextract_vector_simd.sh
similarity index 74%
rename from jvector-native/src/main/native/jextract_vector_simd.sh
rename to jvector-native/src/main/native/src/jextract_vector_simd.sh
index a2d704c83..be891a7c6 100755
--- a/jvector-native/src/main/native/jextract_vector_simd.sh
+++ b/jvector-native/src/main/native/src/jextract_vector_simd.sh
@@ -2,6 +2,8 @@
# fail on error
set -e
+# print commands as they are executed
+set +x
# Copyright DataStax, Inc.
#
@@ -17,6 +19,22 @@ set -e
# See the License for the specific language governing permissions and
# limitations under the License.
+# ---------------------------------------------------------------------------
+# Path anchors — all derived from the git repository root so the script works
+# regardless of the working directory it is invoked from (Maven sets
+# workingDirectory to the src directory, but developers may run it from
+# anywhere inside the repo).
+# ---------------------------------------------------------------------------
+REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
+SCRIPT_DIR="${REPO_ROOT}/jvector-native/src/main/native/src"
+NATIVE_DIR="${REPO_ROOT}/jvector-native/src/main/native"
+MODULE_ROOT="${REPO_ROOT}/jvector-native"
+
+HIGHWAY_DIR="${NATIVE_DIR}/third_party/highway"
+BUILD_DIR="${MODULE_ROOT}/target/meson-build"
+RESOURCES_DIR="${MODULE_ROOT}/src/main/resources"
+JAVA_OUT_DIR="${MODULE_ROOT}/src/main/java"
+
if [ "$1" == "--auto-install-deps" ] ; then AUTO_INSTALL_DEPS=true ; shift ; fi
printf "AUTO_INSTALL_DEPS=%s\n" "${AUTO_INSTALL_DEPS}"
@@ -29,13 +47,13 @@ if [ "$BUILDTYPE" != "release" ] && [ "$BUILDTYPE" != "debug" ] && [ "$BUILDTYPE
fi
printf "BUILDTYPE=%s\n" "${BUILDTYPE}"
-mkdir -p ../resources
+mkdir -p "${RESOURCES_DIR}"
+
# compile jvector_simd_check.cpp as x86-64
# compile jvector_simd.cpp as skylake-avx512
# produce one shared library
# Check that the Google Highway submodule has been initialised
-HIGHWAY_DIR="third_party/highway"
if [ ! -f "${HIGHWAY_DIR}/hwy/highway.h" ]; then
echo "ERROR: Google Highway submodule not found at ${HIGHWAY_DIR}."
echo " Run the following command from the repository root to fix this:"
@@ -80,24 +98,23 @@ if [ "$(printf '%s\n' "$MIN_GCC_VERSION" "$CURRENT_GPP_VERSION" | sort -V | head
exit 1
fi
-BUILD_DIR="../../../target/meson-build"
-rm -rf ../resources/libjvector.so
+rm -rf "${RESOURCES_DIR}/libjvector.so"
# Configure (--wipe resets any stale configuration) then compile
-meson setup "${BUILD_DIR}" \
+meson setup "${BUILD_DIR}" "${NATIVE_DIR}" \
--wipe \
--buildtype="${BUILDTYPE}"
meson compile -C "${BUILD_DIR}"
# The versioned .so (e.g. libjvector.so.0.1.0) is the real file; symlinks point to it.
-# Copy it to ../resources/ as the plain libjvector.so for Java System.load().
+# Copy it to src/main/resources/ so Maven packages it into the jar for LibraryLoader.
SOFILE=$(find "${BUILD_DIR}" -maxdepth 1 -name 'libjvector.so.*' -type f | head -1)
if [ -z "${SOFILE}" ]; then
echo "ERROR: libjvector.so not found in ${BUILD_DIR} after build."
exit 1
fi
-cp "${SOFILE}" ../resources/libjvector.so
+cp "${SOFILE}" "${RESOURCES_DIR}/libjvector.so"
# Generate Java source code
# Should only be run when c header changes
@@ -109,11 +126,12 @@ then
fi
jextract \
- --output ../java \
+ --output "${JAVA_OUT_DIR}" \
-t io.github.jbellis.jvector.vector.cnative \
- -I . \
+ -I "${SCRIPT_DIR}" \
--header-class-name NativeSimdOps \
- jvector_simd.h
+ "${SCRIPT_DIR}/jvector_simd.h"
# Set critical linker option with heap-based segments for all generated methods
-sed -i 's/DESC)/DESC, Linker.Option.critical(true))/g' ../java/io/github/jbellis/jvector/vector/cnative/NativeSimdOps.java
+sed -i 's/DESC)/DESC, Linker.Option.critical(true))/g' \
+ "${JAVA_OUT_DIR}/io/github/jbellis/jvector/vector/cnative/NativeSimdOps.java"
diff --git a/jvector-native/src/main/native/jvector_avx3_dl_kernels.cpp b/jvector-native/src/main/native/src/jvector_avx3_dl_kernels.cpp
similarity index 100%
rename from jvector-native/src/main/native/jvector_avx3_dl_kernels.cpp
rename to jvector-native/src/main/native/src/jvector_avx3_dl_kernels.cpp
diff --git a/jvector-native/src/main/native/jvector_avx3_spr_kernels.cpp b/jvector-native/src/main/native/src/jvector_avx3_spr_kernels.cpp
similarity index 100%
rename from jvector-native/src/main/native/jvector_avx3_spr_kernels.cpp
rename to jvector-native/src/main/native/src/jvector_avx3_spr_kernels.cpp
diff --git a/jvector-native/src/main/native/jvector_cpu_features.h b/jvector-native/src/main/native/src/jvector_cpu_features.h
similarity index 100%
rename from jvector-native/src/main/native/jvector_cpu_features.h
rename to jvector-native/src/main/native/src/jvector_cpu_features.h
diff --git a/jvector-native/src/main/native/jvector_simd.cpp b/jvector-native/src/main/native/src/jvector_simd.cpp
similarity index 100%
rename from jvector-native/src/main/native/jvector_simd.cpp
rename to jvector-native/src/main/native/src/jvector_simd.cpp
diff --git a/jvector-native/src/main/native/jvector_simd.h b/jvector-native/src/main/native/src/jvector_simd.h
similarity index 100%
rename from jvector-native/src/main/native/jvector_simd.h
rename to jvector-native/src/main/native/src/jvector_simd.h
diff --git a/jvector-native/src/main/native/jvector_simd_kernel_list.h b/jvector-native/src/main/native/src/jvector_simd_kernel_list.h
similarity index 100%
rename from jvector-native/src/main/native/jvector_simd_kernel_list.h
rename to jvector-native/src/main/native/src/jvector_simd_kernel_list.h
diff --git a/jvector-native/src/main/native/jvector_simd_kernels.cpp b/jvector-native/src/main/native/src/jvector_simd_kernels.cpp
similarity index 100%
rename from jvector-native/src/main/native/jvector_simd_kernels.cpp
rename to jvector-native/src/main/native/src/jvector_simd_kernels.cpp
diff --git a/jvector-native/src/main/native/jvector_simd_kernels.h b/jvector-native/src/main/native/src/jvector_simd_kernels.h
similarity index 100%
rename from jvector-native/src/main/native/jvector_simd_kernels.h
rename to jvector-native/src/main/native/src/jvector_simd_kernels.h
diff --git a/jvector-native/src/main/native/tests/test_cpu_features.cpp b/jvector-native/src/main/native/tests/test_cpu_features.cpp
new file mode 100644
index 000000000..a921660b6
--- /dev/null
+++ b/jvector-native/src/main/native/tests/test_cpu_features.cpp
@@ -0,0 +1,263 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Validates that the native dispatcher selects the ISA tier that matches the
+// CPU capabilities reported in /proc/cpuinfo, respecting any JVECTOR_MAX_ISA cap.
+//
+// Logic mirrors DispatcherCpuFlagsTest.java and the C implementation in
+// jvector_cpu_features.h / jvector_simd.cpp exactly.
+//
+// /proc/cpuinfo is the authoritative ground-truth: the kernel only exposes a
+// flag when the OS context-switch support (XCR0) is also in place, so checking
+// it is equivalent to checking CPUID + XCR0 together.
+
+#include "test_helpers.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// /proc/cpuinfo helpers — mirrors DispatcherCpuFlagsTest.java
+// ---------------------------------------------------------------------------
+
+// Parse the flags line from the first processor entry in /proc/cpuinfo.
+// Returns an empty set if unavailable (non-Linux, non-x86, or unreadable).
+static std::unordered_set parse_cpuinfo_flags()
+{
+ std::unordered_set flags;
+ std::ifstream f("/proc/cpuinfo");
+ if (!f.is_open()) return flags;
+
+ std::string line;
+ while (std::getline(f, line)) {
+ if (line.rfind("flags", 0) != 0) continue;
+ auto colon = line.find(':');
+ if (colon == std::string::npos) continue;
+ std::istringstream iss(line.substr(colon + 1));
+ std::string token;
+ while (iss >> token) flags.insert(token);
+ break;
+ }
+ return flags;
+}
+
+// Tier names in ascending capability order — index is ordinal (mirrors Java).
+static const std::vector kIsaTiers = {
+ "sse42", "avx2", "avx3", "avx3_dl", "avx3_spr"
+};
+
+static int tier_index(const std::string& name)
+{
+ auto it = std::find(kIsaTiers.begin(), kIsaTiers.end(), name);
+ return (it == kIsaTiers.end()) ? -1 : static_cast(it - kIsaTiers.begin());
+}
+
+// ---- Composite tier predicates (flag names from /proc/cpuinfo) ----
+// These must stay in sync with DispatcherCpuFlagsTest.java and
+// jvector_cpu_features.h. The kernel's naming is inconsistent:
+// no underscore: avx512f bw cd dq vl, avx512vbmi, avx512ifma
+// with underscore: avx512_vnni, avx512_vbmi2, avx512_bitalg,
+// avx512_vpopcntdq, avx512_fp16
+// gfni / vaes / vpclmulqdq have no avx512 prefix at all.
+
+static bool has_avx3(const std::unordered_set& f)
+{
+ return f.count("avx512f") && f.count("avx512bw")
+ && f.count("avx512cd") && f.count("avx512dq")
+ && f.count("avx512vl");
+}
+
+static bool has_avx3_dl(const std::unordered_set& f)
+{
+ return has_avx3(f)
+ && f.count("avx512_vnni") && f.count("avx512vbmi")
+ && f.count("avx512_vbmi2") && f.count("avx512ifma")
+ && f.count("avx512_bitalg") && f.count("avx512_vpopcntdq")
+ && f.count("gfni") && f.count("vaes")
+ && f.count("vpclmulqdq");
+}
+
+static bool has_avx3_spr(const std::unordered_set& f)
+{
+ return has_avx3_dl(f) && f.count("avx512_fp16");
+}
+
+// Compute the expected ISA tier from /proc/cpuinfo flags and the cap,
+// mirroring expectedIsaFromCpuInfo() in DispatcherCpuFlagsTest.java.
+static std::string expected_isa(const std::unordered_set& flags,
+ const std::string& cap)
+{
+ std::string best;
+ if (has_avx3_spr(flags)) best = "avx3_spr";
+ else if (has_avx3_dl(flags)) best = "avx3_dl";
+ else if (has_avx3(flags)) best = "avx3";
+ else if (flags.count("avx2")) best = "avx2";
+ else best = "sse42";
+
+ // Clamp down to cap if set and below best.
+ if (!cap.empty() && tier_index(cap) < tier_index(best))
+ return cap;
+ return best;
+}
+
+// ---------------------------------------------------------------------------
+// Fixture — state shared across all tests
+// ---------------------------------------------------------------------------
+
+class CpuFeaturesTest : public ::testing::Test
+{
+protected:
+ static void SetUpTestSuite()
+ {
+ s_flags = parse_cpuinfo_flags();
+
+ const char* active = jvector_simd_get_active_isa();
+ const char* cap_c = jvector_simd_get_max_isa_env();
+ s_active = active ? active : "";
+ s_cap = cap_c ? cap_c : "";
+
+ // Detect CPU emulators (e.g. Intel SDE): they intercept CPUID and
+ // return synthetic features, but /proc/cpuinfo still reflects the host.
+ // When the active ISA cannot be explained by the host's cpuinfo flags
+ // the comparison tests are meaningless, so we skip them.
+ std::string host_expected = expected_isa(s_flags, s_cap);
+ s_under_emulator = !s_flags.empty()
+ && tier_index(s_active) > tier_index(host_expected);
+
+ s_available = !s_flags.empty() && !s_under_emulator;
+
+ std::printf("[ CPU ] active_isa=%s JVECTOR_MAX_ISA=%s "
+ "cpuinfo_flags=%zu emulator=%s\n",
+ s_active.c_str(),
+ s_cap.empty() ? "(unset)" : s_cap.c_str(),
+ s_flags.size(),
+ s_under_emulator ? "yes (cpuinfo skipped)" : "no");
+ }
+
+ static bool isCappedBelow(const std::string& tier)
+ {
+ return !s_cap.empty() && tier_index(s_cap) < tier_index(tier);
+ }
+
+ static std::unordered_set s_flags;
+ static std::string s_active;
+ static std::string s_cap;
+ static bool s_available;
+ static bool s_under_emulator;
+};
+
+std::unordered_set CpuFeaturesTest::s_flags;
+std::string CpuFeaturesTest::s_active;
+std::string CpuFeaturesTest::s_cap;
+bool CpuFeaturesTest::s_available = false;
+bool CpuFeaturesTest::s_under_emulator = false;
+
+#define SKIP_IF_UNAVAILABLE() \
+ do { \
+ if (s_flags.empty()) GTEST_SKIP() << "/proc/cpuinfo unavailable"; \
+ if (s_under_emulator) GTEST_SKIP() << "CPU emulator detected (SDE?): " \
+ "active_isa=" << s_active << " exceeds host cpuinfo capability"; \
+ } while (0)
+
+// ---------------------------------------------------------------------------
+// Tests — mirror each @Test method in DispatcherCpuFlagsTest.java
+// ---------------------------------------------------------------------------
+
+// AVX2 tier is selected when avx2 is present and the cap allows it.
+TEST_F(CpuFeaturesTest, Avx2Detection)
+{
+ SKIP_IF_UNAVAILABLE();
+ bool cpu_has_avx2 = s_flags.count("avx2") > 0;
+
+ if (cpu_has_avx2 && !isCappedBelow("avx2")) {
+ EXPECT_GE(tier_index(s_active), tier_index("avx2"))
+ << "Expected AVX2 or higher when avx2 flag present, got: " << s_active;
+ } else if (!cpu_has_avx2) {
+ EXPECT_EQ(s_active, "sse42")
+ << "Expected sse42 when avx2 flag absent, got: " << s_active;
+ }
+}
+
+// AVX3 tier is selected when all avx512 baseline flags are present.
+TEST_F(CpuFeaturesTest, Avx3Detection)
+{
+ SKIP_IF_UNAVAILABLE();
+ bool cpu_has_avx3 = has_avx3(s_flags);
+
+ if (cpu_has_avx3 && !isCappedBelow("avx3")) {
+ EXPECT_GE(tier_index(s_active), tier_index("avx3"))
+ << "Expected AVX3 or higher when avx512 baseline flags present, got: " << s_active;
+ } else if (!cpu_has_avx3) {
+ EXPECT_LT(tier_index(s_active), tier_index("avx3"))
+ << "Expected below AVX3 when avx512 baseline flags absent, got: " << s_active;
+ }
+}
+
+// AVX3_DL tier is selected when all ICX flags are present on top of AVX3.
+TEST_F(CpuFeaturesTest, Avx3DlDetection)
+{
+ SKIP_IF_UNAVAILABLE();
+ bool cpu_has_avx3_dl = has_avx3_dl(s_flags);
+
+ if (cpu_has_avx3_dl && !isCappedBelow("avx3_dl")) {
+ EXPECT_GE(tier_index(s_active), tier_index("avx3_dl"))
+ << "Expected AVX3_DL or higher when all ICX flags present, got: " << s_active;
+ } else if (!cpu_has_avx3_dl) {
+ EXPECT_LT(tier_index(s_active), tier_index("avx3_dl"))
+ << "Expected below AVX3_DL when ICX flags absent, got: " << s_active;
+ }
+}
+
+// AVX3_SPR tier is selected when avx512_fp16 and all ICX flags are present.
+TEST_F(CpuFeaturesTest, Avx3SprDetection)
+{
+ SKIP_IF_UNAVAILABLE();
+ bool cpu_has_avx3_spr = has_avx3_spr(s_flags);
+
+ if (cpu_has_avx3_spr && !isCappedBelow("avx3_spr")) {
+ EXPECT_EQ(s_active, "avx3_spr")
+ << "Expected avx3_spr when fp16 + all ICX flags present and uncapped";
+ } else if (!cpu_has_avx3_spr) {
+ EXPECT_LT(tier_index(s_active), tier_index("avx3_spr"))
+ << "Expected below AVX3_SPR when avx512_fp16 absent, got: " << s_active;
+ }
+}
+
+// End-to-end: the tier the dispatcher chose must match what /proc/cpuinfo implies.
+TEST_F(CpuFeaturesTest, DispatcherMatchesCpuInfo)
+{
+ SKIP_IF_UNAVAILABLE();
+
+ std::string exp = expected_isa(s_flags, s_cap);
+
+ // Collect all flags for the failure message.
+ std::string all_flags = std::accumulate(
+ s_flags.begin(), s_flags.end(), std::string{},
+ [](const std::string& a, const std::string& b) {
+ return a.empty() ? b : a + " " + b;
+ });
+
+ EXPECT_EQ(s_active, exp)
+ << "Dispatcher chose '" << s_active
+ << "' but /proc/cpuinfo implies '" << exp << "'."
+ << "\nCPU flags: " << all_flags;
+}
diff --git a/jvector-native/src/main/native/tests/test_elementwise.cpp b/jvector-native/src/main/native/tests/test_elementwise.cpp
new file mode 100644
index 000000000..04c836461
--- /dev/null
+++ b/jvector-native/src/main/native/tests/test_elementwise.cpp
@@ -0,0 +1,193 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Tests for element-wise in-place arithmetic kernels:
+// add_in_place_f32, add_scalar_in_place_f32,
+// sub_in_place_f32, sub_scalar_in_place_f32,
+// max_f32, min_in_place_f32.
+//
+// Vector sizes cover the same ISA boundary / tail matrix as test_similarity.cpp:
+// size 1 — tail only
+// size 3 — tail only
+// size 4 — SSE42 exact / AVX2+AVX512 capped path
+// size 7 — SSE42 1 full + 3-tail
+// size 8 — AVX2 exact / AVX512 capped
+// size 15 — AVX2 1 full + 7-tail
+// size 16 — SSE42 4× main exact / AVX512 1 full
+// size 17 — SSE42 4× main + 1-tail
+// size 32 — AVX2 4× main exact
+// size 37 — AVX2 4× main + 5-tail
+// size 64 — AVX512 4× main exact
+// size 71 — AVX512 4× main + 7-tail
+// size 100, 128, 255 — large mixed / power-of-2 / odd
+
+#include "test_helpers.h"
+
+// ---------------------------------------------------------------------------
+// Fixture
+// ---------------------------------------------------------------------------
+
+class ElementWiseTest : public ::testing::TestWithParam {};
+
+// ---------------------------------------------------------------------------
+// add_in_place_f32: v1[i] += v2[i]
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, AddInPlace)
+{
+ const size_t n = GetParam().length;
+ auto v1 = make_vec(n, 1.1f);
+ auto v2 = make_vec(n, 0.7f);
+ auto want = v1;
+ for (size_t i = 0; i < n; ++i) want[i] += v2[i];
+
+ auto got = v1;
+ add_in_place_f32(got.data(), v2.data(), n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(got[i], want[i], 1e-5f) << "add_in_place_f32[" << i << "]";
+}
+
+// ---------------------------------------------------------------------------
+// add_scalar_in_place_f32: v1[i] += scalar
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, AddScalarInPlace)
+{
+ const size_t n = GetParam().length;
+ const float scalar = 3.14f;
+ auto v1 = make_vec(n, 1.1f);
+ auto want = v1;
+ for (size_t i = 0; i < n; ++i) want[i] += scalar;
+
+ auto got = v1;
+ add_scalar_in_place_f32(got.data(), scalar, n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(got[i], want[i], 1e-5f) << "add_scalar_in_place_f32[" << i << "]";
+}
+
+// ---------------------------------------------------------------------------
+// sub_in_place_f32: v1[i] -= v2[i]
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, SubInPlace)
+{
+ const size_t n = GetParam().length;
+ auto v1 = make_vec(n, 1.1f);
+ auto v2 = make_vec(n, 0.7f);
+ auto want = v1;
+ for (size_t i = 0; i < n; ++i) want[i] -= v2[i];
+
+ auto got = v1;
+ sub_in_place_f32(got.data(), v2.data(), n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(got[i], want[i], 1e-5f) << "sub_in_place_f32[" << i << "]";
+}
+
+// ---------------------------------------------------------------------------
+// sub_scalar_in_place_f32: v1[i] -= scalar
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, SubScalarInPlace)
+{
+ const size_t n = GetParam().length;
+ const float scalar = 2.71f;
+ auto v1 = make_vec(n, 1.1f);
+ auto want = v1;
+ for (size_t i = 0; i < n; ++i) want[i] -= scalar;
+
+ auto got = v1;
+ sub_scalar_in_place_f32(got.data(), scalar, n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(got[i], want[i], 1e-5f) << "sub_scalar_in_place_f32[" << i << "]";
+}
+
+// ---------------------------------------------------------------------------
+// max_f32: returns the maximum element
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, MaxF32)
+{
+ const size_t n = GetParam().length;
+ auto v = make_vec(n, 0.9f);
+
+ float want = *std::max_element(v.begin(), v.end());
+ float got = max_f32(v.data(), n);
+
+ EXPECT_FLOAT_EQ(got, want);
+}
+
+// max_f32 on a vector with a known maximum at the last position (tail element)
+TEST_P(ElementWiseTest, MaxF32TailElement)
+{
+ const size_t n = GetParam().length;
+ auto v = make_vec(n, 0.5f);
+ // Place the global maximum in the very last element — exercises tail path.
+ v.back() = 1e6f;
+
+ float got = max_f32(v.data(), n);
+
+ EXPECT_FLOAT_EQ(got, 1e6f);
+}
+
+// ---------------------------------------------------------------------------
+// min_in_place_f32: v1[i] = min(v1[i], v2[i])
+// ---------------------------------------------------------------------------
+
+TEST_P(ElementWiseTest, MinInPlace)
+{
+ const size_t n = GetParam().length;
+ auto v1 = make_vec(n, 1.1f);
+ auto v2 = make_vec(n, 0.7f);
+ auto want = v1;
+ for (size_t i = 0; i < n; ++i) want[i] = std::min(want[i], v2[i]);
+
+ auto got = v1;
+ min_in_place_f32(got.data(), v2.data(), n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(got[i], want[i], 1e-5f) << "min_in_place_f32[" << i << "]";
+}
+
+// add then sub back — result must equal the original vector
+TEST_P(ElementWiseTest, AddSubRoundTrip)
+{
+ const size_t n = GetParam().length;
+ auto original = make_vec(n, 1.3f);
+ auto delta = make_vec(n, 0.4f);
+
+ auto v = original;
+ add_in_place_f32(v.data(), delta.data(), n);
+ sub_in_place_f32(v.data(), delta.data(), n);
+
+ for (size_t i = 0; i < n; ++i)
+ EXPECT_NEAR(v[i], original[i], 1e-5f) << "add_sub_roundtrip[" << i << "]";
+}
+
+// ---------------------------------------------------------------------------
+// Instantiation
+// ---------------------------------------------------------------------------
+
+INSTANTIATE_TEST_SUITE_P(
+ AllSizes,
+ ElementWiseTest,
+ ::testing::ValuesIn(kKernelTestParams),
+ [](const ::testing::TestParamInfo& info) {
+ return info.param.description;
+ });
diff --git a/jvector-native/src/main/native/tests/test_helpers.cpp b/jvector-native/src/main/native/tests/test_helpers.cpp
new file mode 100644
index 000000000..957e42947
--- /dev/null
+++ b/jvector-native/src/main/native/tests/test_helpers.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "test_helpers.h"
+
+// ---------------------------------------------------------------------------
+// Global test environment — prints the active ISA once for the whole binary.
+// Registered via AddGlobalTestEnvironment at static-init time so it fires
+// before any test suite runs, regardless of which .cpp files are linked.
+// ---------------------------------------------------------------------------
+
+class JVectorIsaEnvironment : public ::testing::Environment
+{
+public:
+ void SetUp() override
+ {
+ std::printf("[ ISA ] Active dispatch tier: %s\n",
+ jvector_simd_get_active_isa());
+ }
+};
+
+static ::testing::Environment* const kIsaEnv =
+ ::testing::AddGlobalTestEnvironment(new JVectorIsaEnvironment);
+
+// ---------------------------------------------------------------------------
+// make_vec: deterministic test vectors.
+// Values in roughly (-2, 2] with a mix of signs so no element is zero.
+// ---------------------------------------------------------------------------
+
+// ---------------------------------------------------------------------------
+// Canonical test sizes — shared by all parametrised suites.
+// Covers tail-only, single-register, and 4x-unrolled main-loop paths for
+// SSE42 (4 lanes), AVX2 (8 lanes), and AVX512 (16 lanes).
+// ---------------------------------------------------------------------------
+
+const std::vector kKernelTestParams = {
+ // ---- tail-only (< 4 lanes for any ISA) --------------------------------
+ { 1, "tail_1_all_isa"},
+ { 3, "tail_3_all_isa"},
+ // ---- SSE42 boundary (4-lane register) ----------------------------------
+ { 4, "sse42_exact_4"},
+ { 5, "sse42_1full_tail_1"},
+ { 7, "sse42_1full_tail_3"},
+ // ---- AVX2 boundary (8-lane register) -----------------------------------
+ { 8, "avx2_exact_8"},
+ { 9, "avx2_1full_tail_1"},
+ { 15, "avx2_1full_tail_7"},
+ // ---- SSE42 4x-unrolled main loop (16 elements = 4 × 4 lanes) ----------
+ { 16, "sse42_4x_main_exact"},
+ { 17, "sse42_4x_main_tail_1"},
+ { 19, "sse42_4x_main_tail_3"},
+ // ---- AVX2 4x-unrolled main loop (32 elements = 4 × 8 lanes) -----------
+ { 32, "avx2_4x_main_exact"},
+ { 33, "avx2_4x_main_tail_1"},
+ { 37, "avx2_4x_main_tail_5"},
+ // ---- AVX512 boundary (16-lane register) --------------------------------
+ { 64, "avx512_4x_main_exact"},
+ { 71, "avx512_4x_main_tail_7"},
+ // ---- Odd large size exercising all loop stages -------------------------
+ {100, "large_mixed_tail"},
+ {128, "large_power_of_2"},
+ {255, "large_odd_tail_15"},
+};
+
+std::vector make_vec(size_t n, float seed)
+{
+ std::vector v(n);
+ for (size_t i = 0; i < n; ++i) {
+ v[i] = seed * (1.0f + static_cast(i % 7) * 0.13f);
+ if (i % 3 == 0) v[i] = -v[i]; // mix of signs
+ v[i] += 0.5f; // ensure non-zero even after sign flip
+ }
+ return v;
+}
+
diff --git a/jvector-native/src/main/native/tests/test_helpers.h b/jvector-native/src/main/native/tests/test_helpers.h
new file mode 100644
index 000000000..a48ea47cf
--- /dev/null
+++ b/jvector-native/src/main/native/tests/test_helpers.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Shared helpers for the test_simd_kernels test binary.
+// Included by each test .cpp file; defined in test_helpers.cpp.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "jvector_simd.h"
+
+// ---------------------------------------------------------------------------
+// Deterministic test vectors.
+// make_vec(n, seed) produces n floats with a mix of signs and magnitudes
+// so that no element is exactly zero (important for cosine tests).
+// ---------------------------------------------------------------------------
+
+std::vector make_vec(size_t n, float seed);
+
+// ---------------------------------------------------------------------------
+// Shared test parameter — vector length + human-readable path description.
+// Used by every parametrised test suite in the binary so the same set of
+// sizes exercises each kernel.
+// ---------------------------------------------------------------------------
+
+struct KernelTestParam {
+ size_t length;
+ std::string description;
+};
+
+// The canonical set of sizes that hits every code path across ISA tiers.
+// See the top-of-file comment in test_similarity.cpp for the full breakdown.
+extern const std::vector kKernelTestParams;
diff --git a/jvector-native/src/main/native/tests/test_similarity.cpp b/jvector-native/src/main/native/tests/test_similarity.cpp
new file mode 100644
index 000000000..c08947e4f
--- /dev/null
+++ b/jvector-native/src/main/native/tests/test_similarity.cpp
@@ -0,0 +1,265 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Tests for vector similarity kernels: cosine_f32, dot_product_f32, euclidean_f32.
+//
+// The library dispatches to the best ISA available on the host CPU at
+// static-init time. Vector sizes are chosen to hit every code path in the
+// kernel loops regardless of which ISA is selected:
+//
+// SSE42 (4 lanes):
+// sizes 1, 3 — tail only (< 4)
+// size 4 — exact one vector, no tail
+// size 7 — one full + 3-element tail
+// size 16 — 4x unrolled main loop, no tail
+// size 19 — 4x main + 3-element tail
+//
+// AVX2 (8 lanes):
+// sizes 1, 3 — capped fast path (≤4), tail only
+// size 4 — capped fast path (≤4), one vector no tail
+// size 7 — capped fast path (≤8), tail = 7 < 8
+// size 8 — capped fast path (≤8), exact no tail
+// size 15 — one full + 7-element tail
+// size 32 — 4x unrolled main loop, no tail
+// size 37 — 4x main + 5-element tail
+//
+// AVX3/AVX512 (16 lanes):
+// sizes 1, 3 — capped (≤4), tail only
+// size 4 — capped (≤4), exact
+// size 8 — capped (≤8), exact
+// size 15 — one full (16 lanes) – 1 = tail
+// size 16 — exact one full register
+// size 64 — 4x unrolled, no tail
+// size 71 — 4x main + 7-element tail
+
+#include "test_helpers.h"
+
+// ---------------------------------------------------------------------------
+// Reference scalar implementations
+// ---------------------------------------------------------------------------
+
+static float ref_dot(const std::vector& a, const std::vector& b)
+{
+ float s = 0.0f;
+ for (size_t i = 0; i < a.size(); ++i) s += a[i] * b[i];
+ return s;
+}
+
+static float ref_euclidean(const std::vector& a, const std::vector& b)
+{
+ float s = 0.0f;
+ for (size_t i = 0; i < a.size(); ++i) {
+ float d = a[i] - b[i];
+ s += d * d;
+ }
+ return s;
+}
+
+static float ref_cosine(const std::vector& a, const std::vector& b)
+{
+ float ab = 0.0f, aa = 0.0f, bb = 0.0f;
+ for (size_t i = 0; i < a.size(); ++i) {
+ ab += a[i] * b[i];
+ aa += a[i] * a[i];
+ bb += b[i] * b[i];
+ }
+ return ab / std::sqrt(aa * bb);
+}
+
+// ---------------------------------------------------------------------------
+// Parametrised test fixture
+// ---------------------------------------------------------------------------
+
+class SimilarityTest : public ::testing::TestWithParam {};
+
+// ---------------------------------------------------------------------------
+// dot_product_f32 — SIMD result must match the scalar reference
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, DotProduct)
+{
+ const size_t n = GetParam().length;
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ float want = ref_dot(a, b);
+ float got = dot_product_f32(a.data(), 0, b.data(), 0, n);
+
+ EXPECT_NEAR(got, want, 1e-4f * std::abs(want));
+}
+
+// ---------------------------------------------------------------------------
+// dot_product_f32 with non-zero offsets — exercises the aoffset/boffset path
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, DotProductWithOffset)
+{
+ const size_t n = GetParam().length;
+ const size_t prefix = 3; // arbitrary prefix that must be ignored
+
+ // Pad the front with values that must not contribute to the result.
+ std::vector a_pad(prefix + n);
+ std::vector b_pad(prefix + n);
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+ std::copy(a.begin(), a.end(), a_pad.begin() + prefix);
+ std::copy(b.begin(), b.end(), b_pad.begin() + prefix);
+
+ float want = ref_dot(a, b);
+ float got = dot_product_f32(a_pad.data(), prefix, b_pad.data(), prefix, n);
+
+ EXPECT_NEAR(got, want, 1e-4f * std::abs(want));
+}
+
+// ---------------------------------------------------------------------------
+// euclidean_f32 — squared L2 distance
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, Euclidean)
+{
+ const size_t n = GetParam().length;
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ float want = ref_euclidean(a, b);
+ float got = euclidean_f32(a.data(), 0, b.data(), 0, n);
+
+ EXPECT_NEAR(got, want, 1e-4f * std::abs(want));
+}
+
+// ---------------------------------------------------------------------------
+// euclidean_f32 — identical vectors should give exactly 0.0
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, EuclideanSameVector)
+{
+ const size_t n = GetParam().length;
+ auto a = make_vec(n, 0.9f);
+
+ float got = euclidean_f32(a.data(), 0, a.data(), 0, n);
+
+ // Exact zero is expected since a == b; scale tolerance with length to
+ // allow for FMA reassociation differences across ISAs.
+ EXPECT_NEAR(got, 0.0f, 1e-6f * static_cast(n));
+}
+
+// ---------------------------------------------------------------------------
+// cosine_f32 — cosine similarity
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, Cosine)
+{
+ const size_t n = GetParam().length;
+ auto a = make_vec(n, 0.7f);
+ auto b = make_vec(n, 1.3f);
+
+ float want = ref_cosine(a, b);
+ float got = cosine_f32(a.data(), 0, b.data(), 0, n);
+
+ EXPECT_NEAR(got, want, 1e-4f * std::abs(want));
+}
+
+// ---------------------------------------------------------------------------
+// cosine_f32 — parallel vectors should give similarity = 1.0
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, CosineParallelVectors)
+{
+ const size_t n = GetParam().length;
+ auto a = make_vec(n, 1.0f);
+
+ // b = 2*a — same direction, different magnitude → cosine = 1.0
+ std::vector b(n);
+ for (size_t i = 0; i < n; ++i) b[i] = 2.0f * a[i];
+
+ float got = cosine_f32(a.data(), 0, b.data(), 0, n);
+
+ EXPECT_NEAR(got, 1.0f, 1e-5f);
+}
+
+// ---------------------------------------------------------------------------
+// cosine_f32 — orthogonal vectors should give similarity ≈ 0.0
+//
+// Orthogonality is constructed analytically for even n (alternating +/-):
+// a = [+1, +1, +1, ...]
+// b = [+1, -1, +1, -1, ...] — then a·b = 0 if n is even.
+// For odd n we only use n-1 elements (prefix) so the dot is still zero.
+// ---------------------------------------------------------------------------
+
+TEST_P(SimilarityTest, CosineOrthogonalVectors)
+{
+ const size_t n = GetParam().length;
+ if (n < 2) GTEST_SKIP() << "need at least 2 elements for orthogonality";
+
+ const size_t even_n = n - (n % 2); // largest even prefix
+
+ std::vector a(n, 0.0f), b(n, 0.0f);
+ for (size_t i = 0; i < even_n; ++i) {
+ a[i] = 1.0f;
+ b[i] = (i % 2 == 0) ? 1.0f : -1.0f;
+ }
+
+ float got = cosine_f32(a.data(), 0, b.data(), 0, n);
+
+ // Generous tolerance: FP accumulation order differs between ISA tiers.
+ EXPECT_NEAR(got, 0.0f, 1e-4f);
+}
+
+// ---------------------------------------------------------------------------
+// Instantiation — named using the description field
+// ---------------------------------------------------------------------------
+
+INSTANTIATE_TEST_SUITE_P(
+ AllSizes,
+ SimilarityTest,
+ ::testing::ValuesIn(kKernelTestParams),
+ [](const ::testing::TestParamInfo& info) {
+ return info.param.description;
+ });
+
+// ---------------------------------------------------------------------------
+// ISA-tier sanity test: confirm JVECTOR_MAX_ISA cap is respected when set
+// ---------------------------------------------------------------------------
+
+TEST(IsaDispatch, MaxIsaEnvHonoured)
+{
+ const char* env = jvector_simd_get_max_isa_env();
+ const char* active = jvector_simd_get_active_isa();
+
+ if (env == nullptr) {
+ // No override — just report which tier was auto-detected.
+ SUCCEED() << "No JVECTOR_MAX_ISA set; auto-selected: " << active;
+ return;
+ }
+
+ // Tiers ordered by capability (ascending index = lower capability).
+ static const char* kOrder[] = {"sse42", "avx2", "avx3", "avx3_dl", "avx3_spr"};
+ auto tier_idx = [](const char* name) -> int {
+ for (int i = 0; i < 5; ++i)
+ if (std::strcmp(kOrder[i], name) == 0) return i;
+ return -1;
+ };
+
+ int env_idx = tier_idx(env);
+ int active_idx = tier_idx(active);
+
+ ASSERT_GE(env_idx, 0) << "Unrecognised JVECTOR_MAX_ISA value: " << env;
+ ASSERT_GE(active_idx, 0) << "Unrecognised active ISA: " << active;
+
+ // Active tier must be <= requested cap.
+ EXPECT_LE(active_idx, env_idx)
+ << "Active ISA (" << active << ") exceeds requested cap (" << env << ")";
+}
diff --git a/rat-excludes.txt b/rat-excludes.txt
index bcdf05ae4..4d0eb0740 100644
--- a/rat-excludes.txt
+++ b/rat-excludes.txt
@@ -37,3 +37,5 @@ local_datasets/**
**/datasets/**
jvector-native/src/main/native/third_party/**
src/main/native/third_party/**
+jvector-native/src/target/meson-build/**
+jvector-native/target/meson-build/**