diff --git a/.github/workflows/event-sse-only-repro.yml b/.github/workflows/event-sse-only-repro.yml new file mode 100644 index 000000000..563721359 --- /dev/null +++ b/.github/workflows/event-sse-only-repro.yml @@ -0,0 +1,46 @@ +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 + # 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" 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++) {