From 566ea83b45a9d5383f24ad4507672226b7145d5e Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Tue, 7 Jul 2026 17:40:18 +0300 Subject: [PATCH 1/2] Add SSE-only CI job + repro tests for FP32/L2 _mm_loadr_ps alignment crash [MOD-16730] The FP32 L2 SSE kernel handles residual % 4 == 3 with _mm_loadr_ps (movaps), which requires a 16-byte-aligned address. VecSimAllocator places vector data at malloc + 8 (allocation header) and the dispatcher never sets an alignment hint on this path (it requires dim % 4 == 3), so on machines without AVX any FP32/L2 query with such a dim crashes with SIGSEGV. This adds: - a kernel-level test that reproduces the crash on any x86-64 machine by placing buffers at the allocator's exact 8-mod-16 placement; - an end-to-end public-API test (create BF index, add vectors, TopK query) that crashes when the dispatcher selects the SSE tier; - a CI job that builds with the AVX-family instruction flags disabled (the build an AVX-less machine gets) and runs the spaces/bruteforce unit tests. The new job is expected to FAIL until the kernel is fixed (see MOD-16730 for the suggested two-line fix: use the IP twin's _mm_load_ss + _mm_loadh_pi pattern instead of the reversed aligned load). Co-Authored-By: Claude Fable 5 --- .github/workflows/event-sse-only-repro.yml | 43 ++++++++++++++++++++++ tests/unit/test_bruteforce.cpp | 28 ++++++++++++++ tests/unit/test_spaces.cpp | 22 +++++++++++ 3 files changed, 93 insertions(+) create mode 100644 .github/workflows/event-sse-only-repro.yml diff --git a/.github/workflows/event-sse-only-repro.yml b/.github/workflows/event-sse-only-repro.yml new file mode 100644 index 000000000..c575b667b --- /dev/null +++ b/.github/workflows/event-sse-only-repro.yml @@ -0,0 +1,43 @@ +name: SSE-only unit tests (loadr alignment repro) + +# Demonstrates MOD-16730: builds the library with the AVX-family instruction flags disabled, +# which is the build any x86-64 machine without AVX gets. The dispatcher then selects the SSE +# tier, and the FP32/L2 kernel's _mm_loadr_ps residual path (dim % 4 == 3) executes movaps on +# the unaligned pointers produced by VecSimAllocator - crashing test_spaces / test_bruteforce. +# This job is EXPECTED TO FAIL until the kernel is fixed. + +on: + pull_request: + workflow_dispatch: + +jobs: + sse-only-unit-tests: + name: SSE-only build (AVX flags off) unit tests + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v6 + with: + submodules: recursive + - name: install dependencies + run: .install/install_script.sh sudo + - name: Print CPU information + run: lscpu + - name: unit tests (SSE-only build) + env: + # Force the compiler-flag detection results off for every AVX-family tier, so no + # OPT_AVX*/OPT_F16C code is compiled and the dispatcher falls through to the SSE tiers + # at runtime - equivalent to running on a pre-AVX machine. + CMAKE_FLAGS: >- + -DCXX_AVX512VL=FALSE + -DCXX_AVX512BF16=FALSE + -DCXX_AVX512BW=FALSE + -DCXX_AVX512VBMI2=FALSE + -DCXX_AVX512FP16=FALSE + -DCXX_AVX512F=FALSE + -DCXX_AVX512VNNI=FALSE + -DCXX_AVX2=FALSE + -DCXX_AVX=FALSE + -DCXX_F16C=FALSE + -DCXX_FMA=FALSE + run: make unit_test CTEST_ARGS="-R 'spaces|bruteforce' --output-on-failure" diff --git a/tests/unit/test_bruteforce.cpp b/tests/unit/test_bruteforce.cpp index abf7e9855..62ab34d91 100644 --- a/tests/unit/test_bruteforce.cpp +++ b/tests/unit/test_bruteforce.cpp @@ -39,6 +39,34 @@ class BruteForceTest : public ::testing::Test { TYPED_TEST_SUITE(BruteForceTest, DataTypeSet); +// End-to-end repro for the _mm_loadr_ps alignment fault: full index flow - factory, dispatcher, +// preprocessor, allocator-placed vectors, TopK query. On a build/machine where the dispatcher +// selects the SSE tier for FP32/L2 (no AVX), dim 19 routes to the _mm_loadr_ps residual path +// with the allocator's 8-mod-16 pointers and crashes with SIGSEGV. +TEST(BruteForceFlowRepro, fp32_l2_dim19_sse_loadr_flow) { + constexpr size_t dim = 19; // dim % 16 == 3 -> residual 3 -> _mm_loadr_ps path on SSE tier + BFParams bfParams = {.type = VecSimType_FLOAT32, .dim = dim, .metric = VecSimMetric_L2}; + VecSimParams params = CreateParams(bfParams); + VecSimIndex *index = VecSimIndex_New(¶ms); + ASSERT_NE(index, nullptr); + + float v[dim]; + for (size_t label = 0; label < 4; label++) { + for (size_t i = 0; i < dim; i++) + v[i] = float(label + i); + VecSimIndex_AddVector(index, v, label); + } + ASSERT_EQ(VecSimIndex_IndexSize(index), 4); + + float q[dim]; + for (size_t i = 0; i < dim; i++) + q[i] = float(i) + 0.5f; + VecSimQueryReply *res = VecSimIndex_TopKQuery(index, q, 2, nullptr, BY_SCORE); + ASSERT_EQ(VecSimQueryReply_Len(res), 2); + VecSimQueryReply_Free(res); + VecSimIndex_Free(index); +} + TYPED_TEST(BruteForceTest, brute_force_vector_add_test) { size_t dim = 4; diff --git a/tests/unit/test_spaces.cpp b/tests/unit/test_spaces.cpp index 9a16bf30d..429398d59 100644 --- a/tests/unit/test_spaces.cpp +++ b/tests/unit/test_spaces.cpp @@ -581,6 +581,28 @@ TEST_F(SpacesTest, GetDistFuncSQ8FP16Asymmetric) { } #ifdef CPU_FEATURES_ARCH_X86_64 +#ifdef OPT_SSE +// Reproduces the _mm_loadr_ps alignment fault in the FP32 L2 SSE kernel (residual % 4 == 3 +// path). Buffers are placed at (16-aligned base) + 8 - exactly where VecSimAllocator::allocate() +// puts vector data when no alignment hint is set, which is always the case on this path since +// the dispatcher only sets a hint when dim % 4 == 0 and this path requires dim % 4 == 3. +TEST_F(SpacesTest, FP32_L2Sqr_SSE_loadr_misaligned) { + constexpr size_t dim = 19; // dim % 16 == 3 -> residual 3 -> _mm_loadr_ps path + alignas(16) static char raw1[16 + dim * sizeof(float)]; + alignas(16) static char raw2[16 + dim * sizeof(float)]; + float *v1 = reinterpret_cast(raw1 + 8); // address == 8 (mod 16) + float *v2 = reinterpret_cast(raw2 + 8); + for (size_t i = 0; i < dim; i++) { + v1[i] = float(i); + v2[i] = float(i) + 1.5f; + } + float baseline = FP32_L2Sqr(v1, v2, dim); + dist_func_t arch_opt_func = spaces::Choose_FP32_L2_implementation_SSE(dim); + // SIGSEGV here: the kernel's residual path executes movaps on the 8-mod-16 addresses. + ASSERT_EQ(baseline, arch_opt_func(v1, v2, dim)); +} +#endif // OPT_SSE + TEST_F(SpacesTest, smallDimChooser) { // Verify that small dimensions gets the no optimization function. for (size_t dim = 1; dim < 8; dim++) { From 7abae98f389ad157e623880a8eb8cf0425323425 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Tue, 7 Jul 2026 18:04:14 +0300 Subject: [PATCH 2/2] Fix ctest filter in SSE-only repro job (gtest case names, --no-tests=error) Co-Authored-By: Claude Fable 5 --- .github/workflows/event-sse-only-repro.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/event-sse-only-repro.yml b/.github/workflows/event-sse-only-repro.yml index c575b667b..563721359 100644 --- a/.github/workflows/event-sse-only-repro.yml +++ b/.github/workflows/event-sse-only-repro.yml @@ -40,4 +40,7 @@ jobs: -DCXX_AVX=FALSE -DCXX_F16C=FALSE -DCXX_FMA=FALSE - run: make unit_test CTEST_ARGS="-R 'spaces|bruteforce' --output-on-failure" + # ctest test names come from gtest_discover_tests, i.e. gtest case names like + # "SpacesTest.*" / "BruteForceTest/0.*". --no-tests=error guards against a regex + # matching nothing and silently passing. + run: make unit_test CTEST_ARGS="-R 'SpacesTest|BruteForce' --no-tests=error --output-on-failure"