Files
faiss/tests/test_distances_simd.cpp
ThatikondV 317a207bca ARM SVE optimization for L2 distance kernels adapted to new simd_impl structure (#5098)
Summary:
This PR introduces ARM SVE (Scalable Vector Extension) optimization for L2 metric functions. The new implementation extends the existing NEON/SVE SIMD logic with SVE vectorized kernels for distance computations, improving both indexing and search performance.

**Benchmarking Setup:**

To evaluate the performance impact of the new SVE-enabled L2 distance computations, a dedicated benchmarking script was developed. This script systematically compares the earlier FAISS version and the latest SVE-optimized version using the SIFT1M dataset with 128-dimensional vectors. The benchmarks cover all major index types like FLAT, IVF_FLAT, IVF_PQ, IVF_SQ8, and HNSW.

**Results:**
Index | OSS QPS | SVE QPS | Improvement
-- | -- | -- | --
Flat   (L2) | 888.53 | 1188.12 | 33.70%
IVF1024-Flat | 33,437.76 | 45,871.00 | 37.20%
IVF1024-PQ32x8 | 33,597.01 | 40,650.00 | 21.00%
HNSW   (M=16, ef=64) | 72,837.67 | 77,482.00 | 6.40%
IVF1024-SQ8 | 17,002.79 | 20,657.00 | 21.50%

The above results clearly demonstrate that enabling SVE vectorization yields consistent performance gains across all tested index types, where both indexing and search times improved significantly.

These improvements validate the effectiveness of the SVE implementation in accelerating L2 distance computations on modern ARM platforms.

**Co-author:** This work was jointly developed with abhijain1204fujitsu.

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

Test Plan:
Run on an aarch64 host with SVE2, `svcntw() == 4` (128-bit vectors).

**The SVE code only exists in dynamic-dispatch builds.** In the default static aarch64 Buck build, `xplat.bzl` filters `utils/simd_impl/distances_arm_sve.cpp` out of `srcs` and `SINGLE_SIMD_LEVEL` resolves to `ARM_NEON`, so a static-mode run exercises nothing here. CI runs both modes.

## Correctness

```
buck2 test -c faiss.dynamic_dispatch=true \
  fbcode//faiss/tests:test_distances_simd \
  fbcode//faiss/tests:test_distances_dispatch \
  fbcode//faiss/tests:test_pq_encoding \
  fbcode//faiss/tests:test_ivfpq_indexing \
  fbcode//faiss/tests:test_ivfpq_codec \
  fbcode//faiss/tests:test_pq_code_distance \
  fbcode//faiss/tests:test_disable_pq_sdc_tables
# Pass 32. Fail 0.

buck2 test -c faiss.dynamic_dispatch=true \
  fbcode//faiss/tests:test_build_blocks fbcode//faiss/tests:test_product_quantizer
# Pass 105. Fail 0.

buck2 test -c faiss.dynamic_dispatch=true fbcode//faiss/tests:test_index   # Pass
buck2 test fbcode//faiss/tests:test_distances_simd fbcode//faiss/tests:test_distances_dispatch
# static mode: Pass 9. Skip 6 (only one SIMD level available).
```

`buck2 build fbcode//mode/opt fbcode//faiss:faiss -c faiss.dynamic_dispatch=true` and the static build both clean. `arc f` / `arc lint` clean.

`test_distances_dispatch` forces every available level and compares against `SIMDLevel::NONE`; in DD mode on this host it reports `NONE`/`ARM_NEON`/`ARM_SVE` and no longer skips, so the `ARM_SVE` specializations are genuinely under test.

Added coverage in `tests/test_distances_simd.cpp`:
- `TestFvecL2sqrNy.D2`: `dim` swept over `{1,2,3,4,5,8,12,16,24,32,48,64}` (was `{2,4,8,12}`) so the `lanes1`..`lanes4` kernels are reached on 128/256/512-bit SVE, not just the `d1/d2/d4/d8` cases.
- Both `_y_transposed` tests: `ny` swept over `{5,8,11,16,32}` (was hardcoded `11`, which never produced a full-vector block on any SVE width, so the no-tail path was untested).

## Kernel microbenchmark

`mode/opt`, DD, pinned core, alternating runs, min-of-N x median-of-7, run-to-run spread <1%. `ny = 256` (ksub), `d = dsub`. vs this diff's parent:

| kernel | speedup range over parent |
|---|---|
| `fvec_L2sqr_ny` | +17% to +725% (dsub 3 and 6 hit the scalar fallback, unchanged) |
| `fvec_L2sqr_ny_nearest` | +48% to +130% |
| `fvec_L2sqr_ny_transposed` | +234% to +298% |
| `fvec_L2sqr_ny_nearest_y_transposed` | +318% to +658% |

## Index-level

`faiss.contrib.datasets.SyntheticDataset`, d=128, nb=400k, nq=2k, k=1, single-threaded, min/best of 3 alternating rounds. "kernels" = SVE kernels with the pre-existing A0 dispatch; "wired" = this diff.

PQ encode, `add()` seconds:

| Index | parent | kernels only | wired | speedup |
|---|---|---|---|---|
| IVF1024-PQ32x8 | 16.17 | 16.16 | 6.32 | **2.56x** |
| PQ32x8 (flat) | 14.80 | 14.82 | 4.97 | **2.98x** |
| Flat / IVF-Flat / HNSW / IVF-SQ8 | - | - | - | 1.00x |

Search QPS is unchanged everywhere (-0.9% to +0.5%, inside 0.2-3.0% run-to-run variance). recall@1 is identical to the parent for every index.

The "kernels only" column is 1.00x because `ProductQuantizer` dispatched through `with_simd_level()` = `AVAILABLE_SIMD_LEVELS_A0`, which lacks the `ARM_SVE` bit, so the `case SIMDLevel::ARM_SVE:` arm failed its `if constexpr` and fell through to `ARM_NEON`. Verified by putting `abort()` in `fvec_L2sqr_ny_nearest<ARM_SVE>`: PQ encoding completed without hitting it, while `test_distances_dispatch` (A1) aborted immediately. The three `with_simd_level_a1` call sites in `impl/ProductQuantizer.cpp` are what close that gap.

`compute_inner_prod_table` still uses A0 and so cannot reach the pre-existing `fvec_inner_products_ny<ARM_SVE>`; left alone as it is outside this diff's L2 scope and unmeasured here.

Reviewed By: junjieqi

Differential Revision: D106149015

Pulled By: mnorris11

fbshipit-source-id: c1144fc8fd0eefb30c9dcc8084b472152623a88e
2026-08-25 10:20:33 -07:00

345 lines
11 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.
*/
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <random>
#include <vector>
#include <faiss/utils/distances.h>
// reference implementations
static void fvec_inner_products_ny_ref(
float* ip,
const float* x,
const float* y,
size_t d,
size_t ny) {
for (size_t i = 0; i < ny; i++) {
ip[i] = faiss::fvec_inner_product(x, y, d);
y += d;
}
}
static void fvec_L2sqr_ny_ref(
float* dis,
const float* x,
const float* y,
size_t d,
size_t ny) {
for (size_t i = 0; i < ny; i++) {
dis[i] = faiss::fvec_L2sqr(x, y, d);
y += d;
}
}
// test templated versions of fvec_L2sqr_ny
TEST(TestFvecL2sqrNy, D2) {
// we're using int values in order to get 100% accurate
// results with floats.
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> u(0, 32);
// 1/2/4/8 hit the dedicated per-dimension kernels; the multiples of 4, 8
// and 16 reach the `lanes1`..`lanes4` kernels on 128/256/512-bit SVE
// respectively, and 3/5 fall through to the scalar tail.
for (const auto dim : {1, 2, 3, 4, 5, 8, 12, 16, 24, 32, 48, 64}) {
std::vector<float> x(dim, 0);
for (size_t i = 0; i < x.size(); i++) {
x[i] = u(rng);
}
for (const auto nrows : {1, 2, 5, 10, 15, 20, 25}) {
std::vector<float> y(nrows * dim);
for (size_t i = 0; i < y.size(); i++) {
y[i] = u(rng);
}
std::vector<float> distances(nrows, 0);
faiss::fvec_L2sqr_ny(
distances.data(), x.data(), y.data(), dim, nrows);
std::vector<float> distances_ref(nrows, 0);
fvec_L2sqr_ny_ref(
distances_ref.data(), x.data(), y.data(), dim, nrows);
ASSERT_EQ(distances, distances_ref)
<< "Mismatching results for dim = " << dim
<< ", nrows = " << nrows;
}
}
}
// fvec_inner_products_ny
TEST(TestFvecInnerProductsNy, D2) {
// we're using int values in order to get 100% accurate
// results with floats.
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> u(0, 32);
for (const auto dim : {2, 4, 8, 12}) {
std::vector<float> x(dim, 0);
for (size_t i = 0; i < x.size(); i++) {
x[i] = u(rng);
}
for (const auto nrows : {1, 2, 5, 10, 15, 20, 25}) {
std::vector<float> y(nrows * dim);
for (size_t i = 0; i < y.size(); i++) {
y[i] = u(rng);
}
std::vector<float> distances(nrows, 0);
faiss::fvec_inner_products_ny(
distances.data(), x.data(), y.data(), dim, nrows);
std::vector<float> distances_ref(nrows, 0);
fvec_inner_products_ny_ref(
distances_ref.data(), x.data(), y.data(), dim, nrows);
ASSERT_EQ(distances, distances_ref)
<< "Mismatching results for dim = " << dim
<< ", nrows = " << nrows;
}
}
}
TEST(TestFvecL2sqr, distances_L2_squared_y_transposed) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
// ny = 8/16/32 are exact multiples of the SVE vector length on 256/512/1024
// bit implementations, so they leave no partial-predicate tail; 5 and 11
// do.
for (const auto ny : {5, 8, 11, 16, 32}) {
for (const auto d : {1, 2, 3, 4, 5, 6, 7, 8, 16}) {
// initialize inputs
std::vector<float> x(d);
float x_sqlen = 0;
for (size_t i = 0; i < x.size(); i++) {
x[i] = uniform(rng);
x_sqlen += x[i] * x[i];
}
std::vector<float> y(d * ny);
std::vector<float> y_sqlens(ny, 0);
for (int i = 0; i < ny; i++) {
for (size_t j = 0; j < y.size(); j++) {
y[j] = uniform(rng);
y_sqlens[i] += y[j] * y[j];
}
}
// perform function
std::vector<float> true_distances(ny, 0);
for (int i = 0; i < ny; i++) {
float dp = 0;
for (int j = 0; j < d; j++) {
dp += x[j] * y[i + j * ny];
}
true_distances[i] = x_sqlen + y_sqlens[i] - 2 * dp;
}
std::vector<float> distances(ny);
faiss::fvec_L2sqr_ny_transposed(
distances.data(),
x.data(),
y.data(),
y_sqlens.data(),
d,
ny, // no need for special offset to test all lines of code
ny);
ASSERT_EQ(distances, true_distances)
<< "Mismatching fvec_L2sqr_ny_transposed results for d = "
<< d << ", ny = " << ny;
}
}
}
TEST(TestFvecL2sqr, nearest_L2_squared_y_transposed) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
// ny = 8/16/32 are exact multiples of the SVE vector length on 256/512/1024
// bit implementations, so they leave no partial-predicate tail; 5 and 11
// do.
for (const auto ny : {5, 8, 11, 16, 32}) {
for (const auto d : {1, 2, 3, 4, 5, 6, 7, 8, 16}) {
// initialize inputs
std::vector<float> x(d);
float x_sqlen = 0;
for (size_t i = 0; i < x.size(); i++) {
x[i] = uniform(rng);
x_sqlen += x[i] * x[i];
}
std::vector<float> y(d * ny);
std::vector<float> y_sqlens(ny, 0);
for (int i = 0; i < ny; i++) {
for (size_t j = 0; j < y.size(); j++) {
y[j] = uniform(rng);
y_sqlens[i] += y[j] * y[j];
}
}
// get distances
std::vector<float> distances(ny, 0);
for (int i = 0; i < ny; i++) {
float dp = 0;
for (int j = 0; j < d; j++) {
dp += x[j] * y[i + j * ny];
}
distances[i] = x_sqlen + y_sqlens[i] - 2 * dp;
}
// find nearest
size_t true_nearest_idx = 0;
float min_dis = HUGE_VALF;
for (int i = 0; i < ny; i++) {
if (distances[i] < min_dis) {
min_dis = distances[i];
true_nearest_idx = i;
}
}
std::vector<float> buffer(ny);
size_t nearest_idx = faiss::fvec_L2sqr_ny_nearest_y_transposed(
buffer.data(),
x.data(),
y.data(),
y_sqlens.data(),
d,
ny, // no need for special offset to test all lines of code
ny);
ASSERT_EQ(nearest_idx, true_nearest_idx)
<< "Mismatching fvec_L2sqr_ny_nearest_y_transposed results for d = "
<< d << ", ny = " << ny;
}
}
}
TEST(TestFvecL1, manhattan_distance) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
// modulo 8 results - 16 is to repeat the while loop in the function
for (const auto nrows : {8, 9, 10, 11, 12, 13, 14, 15, 16}) {
std::vector<float> x(nrows);
std::vector<float> y(nrows);
float true_distance = 0;
for (size_t i = 0; i < x.size(); i++) {
x[i] = uniform(rng);
y[i] = uniform(rng);
true_distance += std::abs(x[i] - y[i]);
}
auto distance = faiss::fvec_L1(x.data(), y.data(), x.size());
ASSERT_EQ(distance, true_distance)
<< "Mismatching fvec_L1 results for nrows = " << nrows;
}
}
TEST(TestFvecLinf, chebyshev_distance) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
// modulo 8 results - 16 is to repeat the while loop in the function
for (const auto nrows : {8, 9, 10, 11, 12, 13, 14, 15, 16}) {
std::vector<float> x(nrows);
std::vector<float> y(nrows);
float true_distance = 0;
for (size_t i = 0; i < x.size(); i++) {
x[i] = uniform(rng);
y[i] = uniform(rng);
true_distance = std::max(true_distance, std::abs(x[i] - y[i]));
}
auto distance = faiss::fvec_Linf(x.data(), y.data(), x.size());
ASSERT_EQ(distance, true_distance)
<< "Mismatching fvec_Linf results for nrows = " << nrows;
}
}
TEST(TestFvecMadd, multiple_add) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
// modulo 8 results - 16 is to repeat the while loop in the function
for (const auto nrows : {8, 9, 10, 11, 12, 13, 14, 15, 16}) {
std::vector<float> a(nrows);
std::vector<float> b(nrows);
const float bf = uniform(rng);
std::vector<float> true_distances(nrows);
for (size_t i = 0; i < a.size(); i++) {
a[i] = uniform(rng);
b[i] = uniform(rng);
true_distances[i] = a[i] + bf * b[i];
}
std::vector<float> distances(nrows);
faiss::fvec_madd(a.size(), a.data(), bf, b.data(), distances.data());
ASSERT_EQ(distances, true_distances)
<< "Mismatching fvec_madd results for nrows = " << nrows;
}
}
TEST(TestFvecAdd, add_array) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
for (const auto nrows : {1, 2, 5, 10, 15, 20, 25}) {
std::vector<float> a(nrows);
std::vector<float> b(nrows);
std::vector<float> true_distances(nrows);
for (size_t i = 0; i < a.size(); i++) {
a[i] = uniform(rng);
b[i] = uniform(rng);
true_distances[i] = a[i] + b[i];
}
std::vector<float> distances(nrows);
faiss::fvec_add(a.size(), a.data(), b.data(), distances.data());
ASSERT_EQ(distances, true_distances)
<< "Mismatching array-array fvec_add results for nrows = "
<< nrows;
}
}
TEST(TestFvecAdd, add_value) {
// ints instead of floats for 100% accuracy
std::default_random_engine rng(123);
std::uniform_int_distribution<int32_t> uniform(0, 32);
for (const auto nrows : {1, 2, 5, 10, 15, 20, 25}) {
std::vector<float> a(nrows);
const float b = uniform(rng); // value to add
std::vector<float> true_distances(nrows);
for (size_t i = 0; i < a.size(); i++) {
a[i] = uniform(rng);
true_distances[i] = a[i] + b;
}
std::vector<float> distances(nrows);
faiss::fvec_add(a.size(), a.data(), b, distances.data());
ASSERT_EQ(distances, true_distances)
<< "Mismatching array-value fvec_add results for nrows = "
<< nrows;
}
}