Files
faiss/tests/test_distances_dispatch.cpp
T
lyang24 e6bd1d252c Add AVX512 VPOPCNT dynamic dispatch level (#5531)
Summary:
## What

  Split the existing VPOPCNTDQ RaBitQ and Hamming kernels from the full Sapphire Rapids SIMD level.

  DD builds now expose an `AVX512_VPOPCNT` capability for CPUs such as Ice Lake and Zen 4. The fallback chain is:

  `AVX512_SPR -> AVX512_VPOPCNT -> AVX512 -> AVX2 -> NONE`

  The ordinary static AVX512 target remains unchanged and contains no VPOPCNT instructions.

  ## Why

  The VPOPCNT kernels only require baseline AVX-512 plus `AVX512_VPOPCNTDQ`. Tying them to the SPR level unnecessarily excluded CPUs that support VPOPCNTDQ but not AVX512-FP16, notably Zen 4.

  ## Changes made while importing

  Two changes were needed on top of the pull request.

  **Feature detection read the wrong CPUID leaf.** `AVX512_VPOPCNTDQ` and `AVX512_VNNI` were read from leaf 1 ECX. Both live in leaf 7 subleaf 0 ECX, at bits 14 and 11. Leaf 1 ECX holds unrelated bits at those positions, so the new level was never selected on any CPU. Measured on an AMD Genoa host before the fix: the level reported as unavailable and detection returned `AVX512`. After the fix the same host reports `AVX512_VPOPCNT`.

  **The build definitions outside CMake were not updated.** The pull request renames three files. The other build description still named the old paths and had no entry for the new level, so the library did not build. This adds the level constant, its compiler flags, its `COMPILE_SIMD_AVX512_VPOPCNT` define, and the renamed paths.

**The new level was not registered in the compiled-level mask.** `compiled_simd_levels()` reports which levels a binary holds, and `FAISS_SIMD_LEVEL` walks down from the requested level until it finds one that is present. The new level had no entry, so the override always stepped past it to `AVX512` and printed that the level was not compiled in. This was reported by `test_dispatch_with_env_var` on the open-source dynamic-dispatch build.

  **The test helper inferred available levels from `/proc/cpuinfo`.** A CPU feature does not imply that a level is compiled into the build, so the helper listed a level the override then refused. It now asks `SIMDConfig::is_simd_level_available()`, which reports compiled and supported. That is a subset of compiled, so every level it returns is one the override accepts.

  The mask and helper names also follow the naming used in the parent diff, so `AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT` and `with_simd_level_with_vpopcnt`.

Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5531

Test Plan:
```
buck2 test fbcode//mode/opt -c faiss.dynamic_dispatch=true \
  fbcode//faiss/tests:test_simd_levels fbcode//faiss/tests:test_rabitq_simd
```
Result: all pass.

The build failures the imported version reported are cleared. All three configurations build:
```
buck2 build fbcode//mode/dev fbcode//faiss:faiss fbcode//faiss:faiss_no_multithreading fbcode//faiss:faiss_omp_mock
buck2 build fbcode//mode/opt -c faiss.dynamic_dispatch=true <same three targets>
buck2 build fbcode//mode/dev -c fbcode.arch=aarch64 <same three targets>
```

Detection on an AMD Genoa host, which has AVX512_VPOPCNTDQ and AVX512_BF16 but not AVX512_FP16:

| | before the CPUID fix | after |
| --- | --- | --- |
| detected level | `AVX512` | `AVX512_VPOPCNT` |
| `AVX512_VPOPCNT` available | 0 | 1 |
| `AVX512_SPR` available | 0 | 0 |

Reviewed By: alibeklfc

Differential Revision: D118969754

Pulled By: mnorris11

fbshipit-source-id: d1ca52b614829a3d4b322c9fecf0c004f2174dab
2026-09-10 09:32:36 -07:00

369 lines
12 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// Per-SIMD-level cross-equivalence tests for the templated distance
// functions in faiss/utils/distances.h:
// fvec_L1, fvec_Linf, fvec_L2sqr_batch_4, fvec_inner_product_batch_4,
// fvec_L2sqr_ny_transposed, fvec_L2sqr_ny_nearest.
//
// IndexFlat exercises these transitively via search, but transitive
// coverage can mask tail-handling bugs. This test runs each function at
// every SIMDConfig-available level and asserts equivalence against the
// SIMDLevel::NONE reference. Sweeps dimensions {1, 3, 7, 8, 9, 15, 16,
// 17, 31, 32, 33, 64} so non-multiple-of-8 / non-multiple-of-16 dims
// exercise the tail paths.
//
// Tolerances:
// * Functions returning an integer (fvec_L2sqr_ny_nearest's chosen
// index) are compared bit-exactly via EXPECT_EQ.
// * Functions returning a single float (fvec_L1, fvec_Linf) accumulate
// over d terms; rounding-order differences across SIMD widths can
// reach a few ULPs. Comparison uses an absolute tolerance scaled by
// d to bound worst-case FP-reassociation drift.
// * Functions returning multiple floats (the batch_4 variants and
// fvec_L2sqr_ny_transposed) use the same scaled tolerance per element.
//
// NaN/Inf inputs are out of scope: these paths are not exercised by
// random uniform input in [-1, 1].
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <random>
#include <vector>
#include <faiss/utils/distances.h>
#include <faiss/utils/simd_levels.h>
using namespace faiss;
namespace {
class SIMDLevelGuard {
public:
SIMDLevelGuard() : prev_(SIMDConfig::get_level()) {}
~SIMDLevelGuard() {
SIMDConfig::set_level(prev_);
}
private:
SIMDLevel prev_;
};
std::vector<SIMDLevel> available_levels() {
static const std::vector<SIMDLevel> all = {
SIMDLevel::NONE,
SIMDLevel::AVX2,
SIMDLevel::AVX512,
SIMDLevel::AVX512_SPR,
SIMDLevel::AVX512_VPOPCNT,
SIMDLevel::ARM_NEON,
SIMDLevel::ARM_SVE,
};
std::vector<SIMDLevel> out;
for (auto lv : all) {
if (SIMDConfig::is_simd_level_available(lv)) {
out.push_back(lv);
}
}
return out;
}
std::vector<float> rand_vec(size_t n, uint32_t seed) {
std::mt19937 rng(seed);
std::uniform_real_distribution<float> u(-1.f, 1.f);
std::vector<float> v(n);
for (auto& x : v) {
x = u(rng);
}
return v;
}
// dimensions covering aligned (multiple of 8 / 16) and tail cases.
const std::vector<size_t> kDims = {1, 3, 7, 8, 9, 15, 16, 17, 31, 32, 33, 64};
// Tolerance per accumulator term. With inputs in [-1, 1], each squared term
// is in [0, 4]; FP-reassociation differences across SIMD widths grow at
// most linearly with d. 1e-6 per term gives a generous bound (e.g. 6.4e-5
// at d=64) that is still many orders of magnitude tighter than any real
// dispatch bug would produce.
constexpr float kPerTermEps = 1e-6f;
float tol_for_d(size_t d) {
// Floor at 1e-6 so d=1 still has a sensible (non-zero) tolerance.
return std::max(static_cast<float>(d) * kPerTermEps, 1e-6f);
}
#define SKIP_IF_SINGLE_LEVEL(levels) \
if ((levels).size() <= 1) { \
GTEST_SKIP() << "only one SIMD level available; nothing to compare"; \
}
template <typename Fn>
void check_floats_at_levels(
Fn fn,
const std::vector<SIMDLevel>& levels,
float tol) {
auto ref = (SIMDConfig::set_level(SIMDLevel::NONE), fn());
for (auto lv : levels) {
if (lv == SIMDLevel::NONE) {
continue;
}
SIMDConfig::set_level(lv);
auto got = fn();
ASSERT_EQ(ref.size(), got.size())
<< "size mismatch at level " << static_cast<int>(lv);
for (size_t i = 0; i < ref.size(); ++i) {
EXPECT_NEAR(ref[i], got[i], tol)
<< "diverged at level " << static_cast<int>(lv) << " idx "
<< i;
}
}
}
// For functions returning an integer index (cast to size_t). Bit-exact
// comparison: a correct dispatch must return the same chosen index.
template <typename Fn>
void check_index_at_levels(Fn fn, const std::vector<SIMDLevel>& levels) {
auto ref = (SIMDConfig::set_level(SIMDLevel::NONE), fn());
for (auto lv : levels) {
if (lv == SIMDLevel::NONE) {
continue;
}
SIMDConfig::set_level(lv);
auto got = fn();
EXPECT_EQ(ref, got)
<< "index diverged at level " << static_cast<int>(lv);
}
}
} // namespace
TEST(DistancesDispatch, FvecL1_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
for (size_t d : kDims) {
auto x = rand_vec(d, 1);
auto y = rand_vec(d, 2);
check_floats_at_levels(
[&]() {
return std::vector<float>{fvec_L1(x.data(), y.data(), d)};
},
levels,
tol_for_d(d));
}
}
TEST(DistancesDispatch, FvecLinf_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
for (size_t d : kDims) {
auto x = rand_vec(d, 3);
auto y = rand_vec(d, 4);
// Linf is a max, not a sum — no FP-reassociation drift; bit-exact.
check_floats_at_levels(
[&]() {
return std::vector<float>{fvec_Linf(x.data(), y.data(), d)};
},
levels,
0.0f);
}
}
TEST(DistancesDispatch, FvecL2sqrBatch4_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
for (size_t d : kDims) {
auto x = rand_vec(d, 10);
auto y0 = rand_vec(d, 11);
auto y1 = rand_vec(d, 12);
auto y2 = rand_vec(d, 13);
auto y3 = rand_vec(d, 14);
check_floats_at_levels(
[&]() {
float d0, d1, d2, d3;
fvec_L2sqr_batch_4(
x.data(),
y0.data(),
y1.data(),
y2.data(),
y3.data(),
d,
d0,
d1,
d2,
d3);
return std::vector<float>{d0, d1, d2, d3};
},
levels,
tol_for_d(d));
}
}
TEST(DistancesDispatch, FvecInnerProductBatch4_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
for (size_t d : kDims) {
auto x = rand_vec(d, 20);
auto y0 = rand_vec(d, 21);
auto y1 = rand_vec(d, 22);
auto y2 = rand_vec(d, 23);
auto y3 = rand_vec(d, 24);
check_floats_at_levels(
[&]() {
float d0, d1, d2, d3;
fvec_inner_product_batch_4(
x.data(),
y0.data(),
y1.data(),
y2.data(),
y3.data(),
d,
d0,
d1,
d2,
d3);
return std::vector<float>{d0, d1, d2, d3};
},
levels,
tol_for_d(d));
}
}
TEST(DistancesDispatch, FvecL2sqrNyTransposed_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
constexpr size_t ny = 13;
for (size_t d : kDims) {
// d_offset is the stride between consecutive components in the
// transposed y layout; pick > ny to exercise non-trivial striding
// and to cover all (i, k) accesses (i in [0, ny), k in [0, d)).
const size_t d_offset = ny + 5;
auto x = rand_vec(d, 30);
// y[i + k * d_offset] is the k-th component of the i-th vector
// (per fvec_L2sqr_ny_transposed's contract). Required size is
// (d - 1) * d_offset + ny.
auto y = rand_vec((d == 0 ? 0 : (d - 1) * d_offset) + ny, 31);
std::vector<float> y_sqlen(ny);
for (size_t i = 0; i < ny; ++i) {
float s = 0;
for (size_t k = 0; k < d; ++k) {
float v = y[i + k * d_offset];
s += v * v;
}
y_sqlen[i] = s;
}
check_floats_at_levels(
[&]() {
std::vector<float> dis(ny);
fvec_L2sqr_ny_transposed(
dis.data(),
x.data(),
y.data(),
y_sqlen.data(),
d,
d_offset,
ny);
return dis;
},
levels,
tol_for_d(d));
}
}
TEST(DistancesDispatch, FvecL2sqrNyNearest_AllLevels) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
constexpr size_t ny = 23;
for (size_t d : kDims) {
auto x = rand_vec(d, 40);
auto y = rand_vec(d * ny, 41);
check_index_at_levels(
[&]() {
// distances_tmp_buffer is an internal scratch; only the
// returned index is part of the public contract. Some
// SIMD impls don't write into the buffer.
std::vector<float> tmp(ny);
return fvec_L2sqr_ny_nearest(
tmp.data(), x.data(), y.data(), d, ny);
},
levels);
}
}
// The low-dimensional nearest path (d in {2,4,8}, one SIMD lane per
// centroid) must agree with the generic implementation for any ny, including
// values around common SIMD widths and non-multiple tails.
TEST(DistancesDispatch, FvecL2sqrNyNearest_LowDim) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
constexpr size_t kLowDims[] = {2, 4, 8};
constexpr size_t kNy[] = {
1,
2,
3,
7,
8,
9,
15,
16,
17,
23,
31,
32,
33,
63,
64,
65,
255,
256,
257};
for (size_t d : kLowDims) {
for (size_t ny : kNy) {
auto x = rand_vec(d, 40);
auto y = rand_vec(d * ny, 41);
check_index_at_levels(
[&]() {
std::vector<float> tmp(ny);
return fvec_L2sqr_ny_nearest(
tmp.data(), x.data(), y.data(), d, ny);
},
levels);
}
}
}
TEST(DistancesDispatch, FvecL2sqrNyNearest_LowDimKeepsFirstTie) {
SIMDLevelGuard guard;
auto levels = available_levels();
SKIP_IF_SINGLE_LEVEL(levels);
constexpr size_t kLowDims[] = {2, 4, 8};
constexpr size_t ny = 23;
constexpr size_t expected = 3;
for (size_t d : kLowDims) {
std::vector<float> x(d, 0.0f);
std::vector<float> y(d * ny, 1.0f);
std::fill_n(y.data() + expected * d, d, 0.0f);
std::fill_n(y.data() + 17 * d, d, 0.0f);
auto nearest = [&]() {
std::vector<float> tmp(ny);
return fvec_L2sqr_ny_nearest(tmp.data(), x.data(), y.data(), d, ny);
};
SIMDConfig::set_level(SIMDLevel::NONE);
EXPECT_EQ(expected, nearest());
check_index_at_levels(nearest, levels);
}
}