feat(rvv): add vector arithmetic kernels (#5655)

Summary:
On RISC-V, `fvec_add` and `fvec_sub` dispatch through `with_simd_level_256bit`. The level mask of that dispatcher (`AVAILABLE_SIMD_LEVELS_AVX2_NEON`) holds no `RISCV_RVV` bit. RVV hosts therefore use the scalar NONE implementation.

This PR adds native RVV kernels for the two functions and fixes the dispatch. The kernels follow the vector-length-agnostic approach of the RVV kernels in the tree. The kernels use `__riscv_vsetvl` (m8) and adapt to any VLEN at runtime. The kernels assume no fixed vector width.

## Changes

- Add RVV kernels in `faiss/utils/simd_impl/distances_rvv.cpp` for:
  - `fvec_add` (vector plus vector, and vector plus scalar)
  - `fvec_sub`
- Add the `AVAILABLE_SIMD_LEVELS_BASE_NO_AVX512` mask in `faiss/impl/simd_dispatch.h`.
  - The mask holds NONE, AVX2, ARM_NEON, and RISCV_RVV.
  - The mask holds no AVX512 bit, so AVX512 machines keep the AVX2 path.
- Route the three dispatchers (`fvec_sub`, `fvec_add`, scalar `fvec_add`) in `faiss/utils/distances_dispatch.h` through the new mask.
- Stable already provides the `compute_PQ_dis_tables_dsub2` RVV kernel and the `ProductQuantizer` RISC-V path, so this PR does not change them.
- This PR changes no behavior on other platforms.

## Performance

The benchmark ran on the original patch. The original patch also held the `compute_PQ_dis_tables_dsub2` kernel. Measured on SG2044 (64-core RISC-V, RVV 1.0), Faiss C++ benchmark suite, rcq-search case:

| Metric | Baseline | Patched | Change |
|---|---|---|---|
| instructions | 7.25 T | 6.87 T | -5.2% |
| cycles | 4.60 T | 4.30 T | -6.5% |
| wall time | 238.1 s | 223.6 s | -6.1% |
| branches | 334.6 G | 305.0 G | -8.8% |

The `fvec_add` hotspot in the perf record drops from 1.06% (scalar NONE) to 0.18% (RVV) of samples.

## Correctness

- `sq-accuracy` reconstruction errors match the baseline on all 9 quantizer types.
- `ndiff_for_idempotence` is 0.
- The correctness run used the original patch.

### Co-authors

- ihb2032 <[email protected]>
- lyd1992 <[email protected]>
- Yuansheng <[email protected]>

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

Test Plan:
`buck2 build fbcode//faiss:faiss` passes.

`buck2 test` passes on the distance tests `test_distances_dispatch` and `test_distances_simd`: 9 pass, 0 fail, 8 skip. The skips occur because the host provides one SIMD level.

`buck2 test` passes on the PQ tests `test_pq_encoding`, `test_pq_code_distance`, `test_ivfpq_indexing`, and `test_disable_pq_sdc_tables`: 17 pass, 0 fail.

Buck does not compile `faiss/utils/simd_impl/distances_rvv.cpp`. The host has no RISC-V toolchain. The new kernels still await review on RVV hardware.

Reviewed By: alibeklfc

Differential Revision: D121445525

Pulled By: mnorris11

fbshipit-source-id: ae3c1685e319bbb9523d87a3cbce8b1fd5310ce3
This commit is contained in:
Zhang Jinhan
2026-09-25 14:10:46 -07:00
committed by meta-codesync[bot]
parent 7f6bf10b1c
commit e24a371a70
3 changed files with 64 additions and 12 deletions
+7
View File
@@ -36,6 +36,13 @@ constexpr int AVAILABLE_SIMD_LEVELS_AVX2_NEON = AVAILABLE_SIMD_LEVELS_NONE |
constexpr int AVAILABLE_SIMD_LEVELS_BASE = AVAILABLE_SIMD_LEVELS_AVX2_NEON |
(1 << int(SIMDLevel::AVX512)) | (1 << int(SIMDLevel::RISCV_RVV));
// BASE_NO_AVX512: BASE minus AVX512, for functions implemented at NONE,
// AVX2, ARM_NEON and RISCV_RVV but with no AVX512 specialization. AVX512
// machines fall through to AVX2, while RISC-V dispatches directly to the
// native vector-length-agnostic RVV kernels.
constexpr int AVAILABLE_SIMD_LEVELS_BASE_NO_AVX512 =
AVAILABLE_SIMD_LEVELS_BASE & ~(1 << int(SIMDLevel::AVX512));
// BASE_WITH_VPOPCNT: BASE + AVX512_VPOPCNT, for kernels that need only
// VPOPCNTDQ on top of baseline AVX-512 (Ice Lake, Zen 4, Zen 5).
constexpr int AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT =
+3 -3
View File
@@ -184,7 +184,7 @@ inline void fvec_sub_dispatch(
const float* a,
const float* b,
float* c) {
with_simd_level_256bit(
with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_BASE_NO_AVX512>(
[&]<SIMDLevel level>() { fvec_sub<level>(d, a, b, c); });
}
@@ -193,7 +193,7 @@ inline void fvec_add_dispatch(
const float* a,
const float* b,
float* c) {
with_simd_level_256bit(
with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_BASE_NO_AVX512>(
[&]<SIMDLevel level>() { fvec_add<level>(d, a, b, c); });
}
@@ -202,7 +202,7 @@ inline void fvec_add_scalar_dispatch(
const float* a,
float b,
float* c) {
with_simd_level_256bit(
with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_BASE_NO_AVX512>(
[&]<SIMDLevel level>() { fvec_add<level>(d, a, b, c); });
}
+54 -9
View File
@@ -11,6 +11,7 @@
#ifdef COMPILE_SIMD_RISCV_RVV
#include <faiss/impl/FaissAssert.h>
#include <faiss/utils/extra_distances.h>
#include <riscv_vector.h>
#include <vector>
@@ -363,9 +364,7 @@ void fvec_L2sqr_ny_transposed<SIMDLevel::RISCV_RVV>(
// dis[i] = x_sqlen + y_sqlen[i] - 2 * sum_j x[j] * y[j * d_offset + i]
//
// The code accumulates the dot product first, and it applies the factor
// of 2 afterwards. A factor of -2 on x[j] can overflow to an infinity.
// That infinity then multiplies an element of y that is 0, and the
// product is a NaN. The SIMDLevel::NONE implementation scales the
// of 2 afterwards. Matches SIMDLevel::NONE implementation which scales the
// accumulated dot product in the same way.
for (; i + chunk <= ny; i += chunk) {
vfloat32m8_t dp = __riscv_vfmv_v_f_f32m8(0.0f, chunk);
@@ -411,10 +410,7 @@ void fvec_inner_products_ny<SIMDLevel::RISCV_RVV>(
size_t d,
size_t ny) {
// The loop below divides the ny dimension into chunks. Each chunk uses
// e32m4. An LMUL sweep showed that e32m4 is the fastest choice. It
// balances the throughput of the strided load vlse32 against the use of
// the register file. That load uses a stride of d*4 bytes. The chunk size
// is VLMAX, so the kernel fills the whole vector register at every VLEN.
// e32m4.
// ip[i] = sum_{j=0}^{d-1} x[j] * y[i * d + j]
const size_t chunk = __riscv_vsetvlmax_e32m4();
const ptrdiff_t stride_bytes = (ptrdiff_t)d * (ptrdiff_t)sizeof(float);
@@ -458,8 +454,7 @@ void fvec_L2sqr_ny<SIMDLevel::RISCV_RVV>(
size_t d,
size_t ny) {
// The loop below divides the ny dimension into chunks. Each chunk uses
// e32m8. The chunk size is VLMAX, so the kernel fills the whole vector
// register at every VLEN.
// e32m8.
const size_t chunk = __riscv_vsetvlmax_e32m8();
const ptrdiff_t stride_bytes = (ptrdiff_t)d * (ptrdiff_t)sizeof(float);
size_t i = 0;
@@ -544,6 +539,56 @@ int fvec_madd_and_argmin<SIMDLevel::RISCV_RVV>(
return j < n ? static_cast<int>(j) : -1;
}
template <>
void fvec_add<SIMDLevel::RISCV_RVV>(
size_t d,
const float* a,
const float* b,
float* c) {
size_t i = 0;
while (i < d) {
size_t vl = __riscv_vsetvl_e32m8(d - i);
vfloat32m8_t va = __riscv_vle32_v_f32m8(a + i, vl);
vfloat32m8_t vb = __riscv_vle32_v_f32m8(b + i, vl);
va = __riscv_vfadd_vv_f32m8(va, vb, vl);
__riscv_vse32_v_f32m8(c + i, va, vl);
i += vl;
}
}
template <>
void fvec_add<SIMDLevel::RISCV_RVV>(
size_t d,
const float* a,
float b,
float* c) {
size_t i = 0;
while (i < d) {
size_t vl = __riscv_vsetvl_e32m8(d - i);
vfloat32m8_t va = __riscv_vle32_v_f32m8(a + i, vl);
va = __riscv_vfadd_vf_f32m8(va, b, vl);
__riscv_vse32_v_f32m8(c + i, va, vl);
i += vl;
}
}
template <>
void fvec_sub<SIMDLevel::RISCV_RVV>(
size_t d,
const float* a,
const float* b,
float* c) {
size_t i = 0;
while (i < d) {
size_t vl = __riscv_vsetvl_e32m8(d - i);
vfloat32m8_t va = __riscv_vle32_v_f32m8(a + i, vl);
vfloat32m8_t vb = __riscv_vle32_v_f32m8(b + i, vl);
va = __riscv_vfsub_vv_f32m8(va, vb, vl);
__riscv_vse32_v_f32m8(c + i, va, vl);
i += vl;
}
}
#define DEFINE_VECTOR_DISTANCE_RVV_FALLBACK(metric) \
template <> \
float VectorDistance<metric, SIMDLevel::RISCV_RVV>::operator()( \