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/**