mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-11 22:50:00 +00:00
Add RVV SIMD specialization for product quantizer distance-table computation (#5543)
Summary:
Add a RISC-V Vector Extension implementation for the product-quantizer
distance-table computation, and wire it into the SIMD dispatch path. The
kernels are runtime vector-length aware (vsetvl-driven, e32m1) rather than
assuming a fixed vector width.
Changes
- compute_PQ_dis_tables_dsub2<RISCV_RVV> (utils/distances_rvv.cpp): RVV
kernel for both inner-product and L2 dsub=2 distance tables. Centroids are
first split into even/odd dimension planes (c0_all / c1_all) so each
subquantizer's ksub entries load as contiguous vectors; the hot loop
processes two subquantizers per iteration:
IP: r = c0*x0 + c1*x1 (vfmul + vfmacc)
L2: r = (c0-x0)^2 + (c1-x1)^2 (vfsub + vfmul + vfadd)
- Add RVV specializations for the accompanying distance primitives:
fvec_norm_L2sqr, fvec_L2sqr_ny, fvec_L2sqr_ny_transposed and
fvec_inner_products_ny are vectorized (strided vlse32 over ny); the
remaining fvec_* and VectorDistance specializations fall back to
SIMDLevel::NONE.
- compute_distance_tables / compute_inner_prod_tables: route dsub == 2 &&
nbits >= 3 && nbits < 8 through compute_PQ_dis_tables_dsub2 when
COMPILE_SIMD_RISCV_RVV is defined, matching the AVX2 / ARM_NEON fast path.
- compute_PQ_dis_tables_dsub2_dispatch: extend the dispatch mask to include
RISCV_RVV.
### Review fixes
The AI reviewers reported 6 valid defects. This version corrects all of them.
- The RVV `compute_PQ_dis_tables_dsub2` unrolled the subquantizer loop by 2
and had no tail. An odd `M` left the last `ksub` table entries unwritten.
The kernel now writes the last subquantizer in a tail loop.
- `fvec_inner_products_ny<RISCV_RVV>` loaded every chunk from `y + j`. The
base pointer missed the `i * d` offset. Each chunk after the first repeated
the first vectors. The kernel now loads from `y + i * d + j`.
- `fvec_norm_L2sqr<RISCV_RVV>` ran one `vfredusum` per vector chunk. A
`d=768` input at VLEN=128 cost 24 serialized reductions. The function again
keeps a tail-undisturbed accumulator, and it reduces once after the loop.
- The `dsub2` kernels require `ksub % 8 == 0`. The dispatch condition accepted
every `nbits < 8`, so an `nbits` of 1 or 2 made `compute_distance_tables`
throw. The condition now also requires `nbits >= 3`. This defect also
affected the AVX2 and the ARM NEON paths.
- `search_sdc` dropped the per-thread `q_row` pointer precomputation. That
change added `M` multiplications per database vector on every architecture.
This version restores the precomputation.
- The `ny` kernels fixed the chunk width at the VLEN=128 value. They now read
`VLMAX` with `vsetvlmax`. Hardware with a larger VLEN uses the whole vector
register.
The diff also removes 2 unreadable comments from the RVV kernel.
Caveat: the benchmark below ran on the previous version. That version included
the `search_sdc` change that this version reverts.
### Performance (sg2044)
Cmd: 1M 768 <PQ> 32 100 16 1 1000 1 <metric> (search ns/query, lower is better)
| # | CONFIG | M | NPROBE | METRIC | F1(ns) | PQ1(ns) | F2(ns) | PQ2(ns) | Δ_PQ% | Δ_F% |
|----|----------|-----|--------|--------|---------|---------|---------|---------|---------|--------|
| 01 | PQ8x4 | 4 | 8 | L2 | 6656870 | 536988 | 6582134 | 495970 | -7.64% | -1.12% |
| 02 | PQ8x4 | 4 | 8 | IP | 6228497 | 475320 | 6164081 | 481118 | +1.22% | -1.03% |
| 03 | PQ8x8 | 8 | 8 | L2 | 6626171 | 545230 | 6580586 | 500343 | -8.23% | -0.69% |
| 04 | PQ8x8 | 8 | 8 | IP | 6228452 | 519710 | 6165707 | 479864 | -7.67% | -1.01% |
| 05 | PQ8x16 | 16 | 8 | L2 | 6615305 | 697070 | 6582239 | 624059 | -10.47% | -0.50% |
| 06 | PQ8x16 | 16 | 8 | IP | 6227067 | 642949 | 6164572 | 587271 | -8.66% | -1.00% |
| 07 | PQ8x32 | 32 | 8 | L2 | 6624323 | 1185655 | 6581329 | 902298 | -23.90% | -0.65% |
| 08 | PQ8x32 | 32 | 8 | IP | 6239930 | 1155545 | 6164198 | 785403 | -32.03% | -1.21% |
| 09 | PQ8x64 | 64 | 8 | L2 | 6624196 | 2006275 | 6578815 | 1687191 | -15.90% | -0.69% |
| 10 | PQ8x64 | 64 | 8 | IP | 6225719 | 1800678 | 6164767 | 1512760 | -15.99% | -0.98% |
| 11 | PQ8x96 | 96 | 8 | L2 | 6617525 | 3108377 | 6582946 | 2701758 | -13.08% | -0.52% |
| 12 | PQ8x96 | 96 | 8 | IP | 6239711 | 2657958 | 6170006 | 2320803 | -12.68% | -1.12% |
| 13 | PQ8x384 | 384 | 8 | L2 | 6620602 | 14029275| 6583820 | 12318712| -12.19% | -0.56% |
| 14 | PQ8x384 | 384 | 8 | IP | 6228776 | 12089134| 6166143 | 10509785| -13.06% | -1.01% |
| 15 | PQ8x32 | 32 | 4 | L2 | 3321625 | 773663 | 3298874 | 491225 | -36.51% | -0.68% |
| 16 | PQ8x32 | 32 | 16 | L2 |13223858 | 2184906 |13152640 | 1645444 | -24.69% | -0.54% |
| 17 | PQ8x32 | 32 | 32 | L2 |26430233 | 3405062 |26331426 | 3138563 | -7.83% | -0.37% |
| 18 | PQ8x64 | 64 | 4 | L2 | 3320850 | 1191054 | 3298426 | 969309 | -18.62% | -0.68% |
| 19 | PQ8x64 | 64 | 16 | L2 |13207340 | 3944868 |13149902 | 3252654 | -17.55% | -0.43% |
| 20 | PQ8x64 | 64 | 32 | L2 |26467696 | 6725098 |26335327 | 6667493 | -0.86% | -0.50% |
| 21 | PQ8x16 | 16 | 4 | L2 | 3316496 | 427653 | 3304051 | 367407 | -14.09% | -0.38% |
| 22 | PQ8x16 | 16 | 16 | L2 |13203491 | 1198002 |13150061 | 1121421 | -6.39% | -0.40% |
| 23 | PQ8x16 | 16 | 32 | L2 |26438401 | 2199527 |26280149 | 2143973 | -2.53% | -0.60% |
Δ_PQ%: PQ baseline -> RVV; Δ_F%: Flat baseline -> RVV.
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5543
Test Plan:
`buck build fbcode//faiss:faiss` passes.
`buck2 test` passes on the PQ and the distance C++ tests:
`test_disable_pq_sdc_tables`, `test_distances_dispatch`, `test_distances_simd`,
`test_ivfpq_codec`, `test_ivfpq_indexing`, `test_pq_encoding` and
`test_pq_code_distance`. The result is 27 pass and 0 fail. The host is aarch64,
so 8 tests skip. Those tests need a second SIMD level to compare against.
This diff adds 2 tests to `faiss/tests/test_product_quantizer.py`:
- `test_dsub2_4bit` covers `dsub=2` with `nbits=4`. `ksub` is 16, so the SIMD
`dsub2` kernel runs.
- `test_dsub2_2bit` covers `dsub=2` with `nbits=2`. `ksub` is 4, so the generic
path must run instead.
I checked the `nbits >= 3` fix with a temporary C++ test on ARM NEON. The test
compares `compute_distance_tables` and `compute_inner_prod_tables` against
`compute_distance_table` and `compute_inner_prod_table`, for `nbits` of 1, 2, 3
and 4. The test fails with a `ksub % 8 == 0` assertion before the fix. The test
passes after the fix. I removed the temporary test before submit.
Limits of the local verification:
- I did not run `faiss/tests/test_product_quantizer.py` on this devserver. The
`fbcode//faiss/python:pyfaiss-py-gen` SWIG step fails on aarch64 with
`Exec format error`. The failure also reproduces without my changes. CI runs
the Python tests on x86.
- I did not compile `utils/simd_impl/distances_rvv.cpp`. This devserver has no
RISC-V toolchain. `xplat.bzl` registers no RVV sources, so Buck and internal
CI do not build that file either. Only the OSS CMake build on rv64 compiles
it. A reviewer with RVV hardware should confirm the 4 RVV kernel fixes.
Reviewed By: juancarpio27
Differential Revision: D117914440
Pulled By: mnorris11
fbshipit-source-id: 3539a116d47f4a96c1051bc10e6440deae8c5302
This commit is contained in:
committed by
meta-codesync[bot]
parent
b1c1496545
commit
1768a5a8e9
@@ -489,8 +489,12 @@ void ProductQuantizer::compute_distance_tables(
|
||||
const float* x,
|
||||
float* dis_tables) const {
|
||||
int64_t nx_signed = nx;
|
||||
#if defined(COMPILE_SIMD_AVX2) || defined(COMPILE_SIMD_ARM_NEON)
|
||||
if (dsub == 2 && nbits < 8) { // interesting for a narrow range of settings
|
||||
#if defined(COMPILE_SIMD_AVX2) || defined(COMPILE_SIMD_ARM_NEON) || \
|
||||
defined(COMPILE_SIMD_RISCV_RVV)
|
||||
// The dsub2 kernels help for a narrow range of settings. They require
|
||||
// ksub = 1 << nbits to be a multiple of 8. Therefore nbits must be 3 or
|
||||
// more.
|
||||
if (dsub == 2 && nbits >= 3 && nbits < 8) {
|
||||
compute_PQ_dis_tables_dsub2(
|
||||
d, ksub, centroids.data(), nx, x, false, dis_tables);
|
||||
} else
|
||||
@@ -524,8 +528,9 @@ void ProductQuantizer::compute_inner_prod_tables(
|
||||
const float* x,
|
||||
float* dis_tables) const {
|
||||
int64_t nx_signed = nx;
|
||||
#if defined(COMPILE_SIMD_AVX2) || defined(COMPILE_SIMD_ARM_NEON)
|
||||
if (dsub == 2 && nbits < 8) {
|
||||
#if defined(COMPILE_SIMD_AVX2) || defined(COMPILE_SIMD_ARM_NEON) || \
|
||||
defined(COMPILE_SIMD_RISCV_RVV)
|
||||
if (dsub == 2 && nbits >= 3 && nbits < 8) {
|
||||
compute_PQ_dis_tables_dsub2(
|
||||
d, ksub, centroids.data(), nx, x, true, dis_tables);
|
||||
} else
|
||||
@@ -872,8 +877,9 @@ void ProductQuantizer::search_sdc(
|
||||
float* heap_dis = res->val + i * k;
|
||||
const uint8_t* qcode = qcodes + i * code_size;
|
||||
|
||||
if (init_finalize_heap)
|
||||
if (init_finalize_heap) {
|
||||
maxheap_heapify(k, heap_dis, heap_ids);
|
||||
}
|
||||
|
||||
// Precompute per-subquantizer row pointers: q_row[m] points to
|
||||
// sdc_table[m*ksub^2 + qcode[m]*ksub], eliminating M
|
||||
@@ -897,8 +903,9 @@ void ProductQuantizer::search_sdc(
|
||||
bcode += code_size;
|
||||
}
|
||||
|
||||
if (init_finalize_heap)
|
||||
if (init_finalize_heap) {
|
||||
maxheap_reorder(k, heap_dis, heap_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,9 @@ inline void compute_PQ_dis_tables_dsub2_dispatch(
|
||||
const float* x,
|
||||
bool is_inner_product,
|
||||
float* dis_tables) {
|
||||
with_simd_level_256bit([&]<SIMDLevel level>() {
|
||||
constexpr int dsub2_mask =
|
||||
AVAILABLE_SIMD_LEVELS_AVX2_NEON | (1 << int(SIMDLevel::RISCV_RVV));
|
||||
with_selected_simd_levels<dsub2_mask>([&]<SIMDLevel level>() {
|
||||
compute_PQ_dis_tables_dsub2<level>(
|
||||
d, ksub, centroids, nx, x, is_inner_product, dis_tables);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <faiss/utils/extra_distances.h>
|
||||
#include <riscv_vector.h>
|
||||
#include <vector>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
@@ -52,6 +53,126 @@ static inline size_t rvv_argmin(const float* values, size_t n) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Write the ksub table entries of one subquantizer that has dsub = 2.
|
||||
// The function computes one of these 2 results:
|
||||
// inner product: out[k] = x0 * c0[k] + x1 * c1[k]
|
||||
// L2: out[k] = (c0[k] - x0)^2 + (c1[k] - x1)^2
|
||||
template <bool is_inner_product>
|
||||
static inline void rvv_pq2_row(
|
||||
const float* c0,
|
||||
const float* c1,
|
||||
float x0,
|
||||
float x1,
|
||||
size_t ksub,
|
||||
size_t vl,
|
||||
float* out) {
|
||||
for (size_t k = 0; k < ksub; k += vl) {
|
||||
vfloat32m1_t vc0 = __riscv_vle32_v_f32m1(c0 + k, vl);
|
||||
vfloat32m1_t vc1 = __riscv_vle32_v_f32m1(c1 + k, vl);
|
||||
if constexpr (is_inner_product) {
|
||||
vfloat32m1_t r = __riscv_vfmacc_vf_f32m1(
|
||||
__riscv_vfmul_vf_f32m1(vc0, x0, vl), x1, vc1, vl);
|
||||
__riscv_vse32_v_f32m1(out + k, r, vl);
|
||||
} else {
|
||||
vfloat32m1_t d0 = __riscv_vfsub_vf_f32m1(vc0, x0, vl);
|
||||
vfloat32m1_t d1 = __riscv_vfsub_vf_f32m1(vc1, x1, vl);
|
||||
d0 = __riscv_vfmul_vv_f32m1(d0, d0, vl);
|
||||
d1 = __riscv_vfmul_vv_f32m1(d1, d1, vl);
|
||||
vfloat32m1_t r = __riscv_vfadd_vv_f32m1(d0, d1, vl);
|
||||
__riscv_vse32_v_f32m1(out + k, r, vl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <bool is_inner_product>
|
||||
static void rvv_pq2_tables(
|
||||
size_t M,
|
||||
size_t ksub,
|
||||
const float* c0_all,
|
||||
const float* c1_all,
|
||||
size_t nx,
|
||||
const float* x,
|
||||
size_t vl,
|
||||
float* dis_tables) {
|
||||
const size_t d = 2 * M;
|
||||
for (size_t i = 0; i < nx; i++) {
|
||||
const float* xi = x + i * d;
|
||||
float* oi = dis_tables + i * M * ksub;
|
||||
size_t m = 0;
|
||||
// The loop computes 2 subquantizers per iteration. The 2 computations
|
||||
// are independent, so the processor can run them at the same time.
|
||||
for (; m + 1 < M; m += 2) {
|
||||
rvv_pq2_row<is_inner_product>(
|
||||
c0_all + m * ksub,
|
||||
c1_all + m * ksub,
|
||||
xi[2 * m],
|
||||
xi[2 * m + 1],
|
||||
ksub,
|
||||
vl,
|
||||
oi + m * ksub);
|
||||
rvv_pq2_row<is_inner_product>(
|
||||
c0_all + (m + 1) * ksub,
|
||||
c1_all + (m + 1) * ksub,
|
||||
xi[2 * (m + 1)],
|
||||
xi[2 * (m + 1) + 1],
|
||||
ksub,
|
||||
vl,
|
||||
oi + (m + 1) * ksub);
|
||||
}
|
||||
// If M is odd, the loop above skips the last subquantizer. This loop
|
||||
// computes it.
|
||||
for (; m < M; m++) {
|
||||
rvv_pq2_row<is_inner_product>(
|
||||
c0_all + m * ksub,
|
||||
c1_all + m * ksub,
|
||||
xi[2 * m],
|
||||
xi[2 * m + 1],
|
||||
ksub,
|
||||
vl,
|
||||
oi + m * ksub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
void compute_PQ_dis_tables_dsub2<SIMDLevel::RISCV_RVV>(
|
||||
size_t d,
|
||||
size_t ksub,
|
||||
const float* all_centroids,
|
||||
size_t nx,
|
||||
const float* x,
|
||||
bool is_inner_product,
|
||||
float* dis_tables) {
|
||||
const size_t M = d / 2;
|
||||
FAISS_THROW_IF_NOT(ksub % 8 == 0);
|
||||
|
||||
// Separate the centroids into 2 planes. Each plane holds one component of
|
||||
// every centroid, so a unit-stride load can read the plane.
|
||||
std::vector<float> c0_all(M * ksub);
|
||||
std::vector<float> c1_all(M * ksub);
|
||||
for (size_t m = 0; m < M; m++) {
|
||||
const float* cm = all_centroids + m * ksub * 2;
|
||||
float* c0m = c0_all.data() + m * ksub;
|
||||
float* c1m = c1_all.data() + m * ksub;
|
||||
for (size_t k = 0; k < ksub; k++) {
|
||||
c0m[k] = cm[2 * k];
|
||||
c1m[k] = cm[2 * k + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// ksub is a power of 2, and ksub is a multiple of 8. Therefore vl divides
|
||||
// ksub exactly.
|
||||
const size_t vl = __riscv_vsetvl_e32m1(ksub);
|
||||
|
||||
if (is_inner_product) {
|
||||
rvv_pq2_tables<true>(
|
||||
M, ksub, c0_all.data(), c1_all.data(), nx, x, vl, dis_tables);
|
||||
} else {
|
||||
rvv_pq2_tables<false>(
|
||||
M, ksub, c0_all.data(), c1_all.data(), nx, x, vl, dis_tables);
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
float fvec_norm_L2sqr<SIMDLevel::RISCV_RVV>(const float* x, size_t d) {
|
||||
size_t vlmax = __riscv_vsetvlmax_e32m8();
|
||||
@@ -227,31 +348,58 @@ void fvec_L2sqr_ny_transposed<SIMDLevel::RISCV_RVV>(
|
||||
size_t d,
|
||||
size_t d_offset,
|
||||
size_t ny) {
|
||||
size_t vlmax = __riscv_vsetvlmax_e32m8();
|
||||
vfloat32m8_t acc = __riscv_vfmv_v_f_f32m8(0.0f, vlmax);
|
||||
size_t i = 0;
|
||||
while (i < d) {
|
||||
size_t vl = __riscv_vsetvl_e32m8(d - i);
|
||||
vfloat32m8_t vx = __riscv_vle32_v_f32m8(x + i, vl);
|
||||
acc = __riscv_vfmacc_vv_f32m8_tu(acc, vx, vx, vl);
|
||||
i += vl;
|
||||
// Compute the squared length of the query subvector.
|
||||
float x_sqlen = 0;
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
x_sqlen += x[j] * x[j];
|
||||
}
|
||||
float x_sqlen =
|
||||
rvv_reduce(acc, vlmax, 0.0f, __riscv_vfredusum_vs_f32m8_f32m1);
|
||||
i = 0;
|
||||
while (i < ny) {
|
||||
size_t vl = __riscv_vsetvl_e32m8(ny - i);
|
||||
acc = __riscv_vfmv_v_f_f32m8(0.0f, vl);
|
||||
|
||||
// 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.
|
||||
const size_t chunk = __riscv_vsetvlmax_e32m8();
|
||||
size_t i = 0;
|
||||
|
||||
// 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
|
||||
// accumulated dot product in the same way.
|
||||
for (; i + chunk <= ny; i += chunk) {
|
||||
vfloat32m8_t dp = __riscv_vfmv_v_f_f32m8(0.0f, chunk);
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m8_t vy = __riscv_vle32_v_f32m8(y + j * d_offset + i, vl);
|
||||
acc = __riscv_vfmacc_vf_f32m8(acc, x[j], vy, vl);
|
||||
vfloat32m8_t y_vec =
|
||||
__riscv_vle32_v_f32m8(y + j * d_offset + i, chunk);
|
||||
dp = __riscv_vfmacc_vf_f32m8(dp, x[j], y_vec, chunk);
|
||||
}
|
||||
vfloat32m8_t vres = __riscv_vle32_v_f32m8(y_sqlen + i, vl);
|
||||
vres = __riscv_vfadd_vf_f32m8(vres, x_sqlen, vl);
|
||||
acc = __riscv_vfmul_vf_f32m8(acc, 2.0f, vl);
|
||||
vres = __riscv_vfsub_vv_f32m8(vres, acc, vl);
|
||||
__riscv_vse32_v_f32m8(dis + i, vres, vl);
|
||||
i += vl;
|
||||
|
||||
vfloat32m8_t acc = __riscv_vle32_v_f32m8(y_sqlen + i, chunk);
|
||||
acc = __riscv_vfadd_vf_f32m8(acc, x_sqlen, chunk);
|
||||
dp = __riscv_vfmul_vf_f32m8(dp, 2.0f, chunk);
|
||||
acc = __riscv_vfsub_vv_f32m8(acc, dp, chunk);
|
||||
|
||||
__riscv_vse32_v_f32m8(dis + i, acc, chunk);
|
||||
}
|
||||
|
||||
// This block computes the last ny % chunk elements.
|
||||
if (i < ny) {
|
||||
size_t vl = __riscv_vsetvl_e32m8(ny - i);
|
||||
|
||||
vfloat32m8_t dp = __riscv_vfmv_v_f_f32m8(0.0f, vl);
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m8_t y_vec =
|
||||
__riscv_vle32_v_f32m8(y + j * d_offset + i, vl);
|
||||
dp = __riscv_vfmacc_vf_f32m8(dp, x[j], y_vec, vl);
|
||||
}
|
||||
|
||||
vfloat32m8_t acc = __riscv_vle32_v_f32m8(y_sqlen + i, vl);
|
||||
acc = __riscv_vfadd_vf_f32m8(acc, x_sqlen, vl);
|
||||
dp = __riscv_vfmul_vf_f32m8(dp, 2.0f, vl);
|
||||
acc = __riscv_vfsub_vv_f32m8(acc, dp, vl);
|
||||
|
||||
__riscv_vse32_v_f32m8(dis + i, acc, vl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +410,43 @@ void fvec_inner_products_ny<SIMDLevel::RISCV_RVV>(
|
||||
const float* y,
|
||||
size_t d,
|
||||
size_t ny) {
|
||||
for (size_t i = 0; i < ny; i++) {
|
||||
ip[i] = fvec_inner_product<SIMDLevel::RISCV_RVV>(x, y, d);
|
||||
y += d;
|
||||
// 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.
|
||||
// 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);
|
||||
size_t i = 0;
|
||||
|
||||
for (; i + chunk <= ny; i += chunk) {
|
||||
vfloat32m4_t acc = __riscv_vfmv_v_f_f32m4(0.0f, chunk);
|
||||
const float* yb = y + i * d; // the first vector of this chunk
|
||||
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m4_t y_vec =
|
||||
__riscv_vlse32_v_f32m4(yb + j, stride_bytes, chunk);
|
||||
acc = __riscv_vfmacc_vf_f32m4(acc, x[j], y_vec, chunk);
|
||||
}
|
||||
|
||||
__riscv_vse32_v_f32m4(ip + i, acc, chunk);
|
||||
}
|
||||
|
||||
// This block computes the last ny % chunk elements.
|
||||
if (i < ny) {
|
||||
size_t vl = __riscv_vsetvl_e32m4(ny - i);
|
||||
|
||||
vfloat32m4_t acc = __riscv_vfmv_v_f_f32m4(0.0f, vl);
|
||||
const float* yb = y + i * d;
|
||||
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m4_t y_vec =
|
||||
__riscv_vlse32_v_f32m4(yb + j, stride_bytes, vl);
|
||||
acc = __riscv_vfmacc_vf_f32m4(acc, x[j], y_vec, vl);
|
||||
}
|
||||
|
||||
__riscv_vse32_v_f32m4(ip + i, acc, vl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +457,36 @@ void fvec_L2sqr_ny<SIMDLevel::RISCV_RVV>(
|
||||
const float* y,
|
||||
size_t d,
|
||||
size_t ny) {
|
||||
for (size_t i = 0; i < ny; i++) {
|
||||
dis[i] = fvec_L2sqr<SIMDLevel::RISCV_RVV>(x, y, d);
|
||||
y += d;
|
||||
// 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.
|
||||
const size_t chunk = __riscv_vsetvlmax_e32m8();
|
||||
const ptrdiff_t stride_bytes = (ptrdiff_t)d * (ptrdiff_t)sizeof(float);
|
||||
size_t i = 0;
|
||||
|
||||
for (; i + chunk <= ny; i += chunk) {
|
||||
vfloat32m8_t acc = __riscv_vfmv_v_f_f32m8(0.0f, chunk);
|
||||
const float* yb = y + i * d; // the first vector of this chunk
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m8_t y_vec =
|
||||
__riscv_vlse32_v_f32m8(yb + j, stride_bytes, chunk);
|
||||
vfloat32m8_t diff = __riscv_vfsub_vf_f32m8(y_vec, x[j], chunk);
|
||||
acc = __riscv_vfmacc_vv_f32m8(acc, diff, diff, chunk);
|
||||
}
|
||||
__riscv_vse32_v_f32m8(dis + i, acc, chunk);
|
||||
}
|
||||
|
||||
if (i < ny) {
|
||||
size_t vl = __riscv_vsetvl_e32m8(ny - i);
|
||||
vfloat32m8_t acc = __riscv_vfmv_v_f_f32m8(0.0f, vl);
|
||||
const float* yb = y + i * d;
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
vfloat32m8_t y_vec =
|
||||
__riscv_vlse32_v_f32m8(yb + j, stride_bytes, vl);
|
||||
vfloat32m8_t diff = __riscv_vfsub_vf_f32m8(y_vec, x[j], vl);
|
||||
acc = __riscv_vfmacc_vv_f32m8(acc, diff, diff, vl);
|
||||
}
|
||||
__riscv_vse32_v_f32m8(dis + i, acc, vl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,9 +497,8 @@ size_t fvec_L2sqr_ny_nearest<SIMDLevel::RISCV_RVV>(
|
||||
const float* y,
|
||||
size_t d,
|
||||
size_t ny) {
|
||||
fvec_L2sqr_ny<SIMDLevel::RISCV_RVV>(distances_tmp_buffer, x, y, d, ny);
|
||||
const size_t j = rvv_argmin(distances_tmp_buffer, ny);
|
||||
return j < ny ? j : 0;
|
||||
return fvec_L2sqr_ny_nearest<SIMDLevel::NONE>(
|
||||
distances_tmp_buffer, x, y, d, ny);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -302,10 +510,8 @@ size_t fvec_L2sqr_ny_nearest_y_transposed<SIMDLevel::RISCV_RVV>(
|
||||
size_t d,
|
||||
size_t d_offset,
|
||||
size_t ny) {
|
||||
fvec_L2sqr_ny_transposed<SIMDLevel::RISCV_RVV>(
|
||||
return fvec_L2sqr_ny_nearest_y_transposed<SIMDLevel::NONE>(
|
||||
distances_tmp_buffer, x, y, y_sqlen, d, d_offset, ny);
|
||||
const size_t j = rvv_argmin(distances_tmp_buffer, ny);
|
||||
return j < ny ? j : 0;
|
||||
}
|
||||
|
||||
template <>
|
||||
|
||||
@@ -212,3 +212,18 @@ class TestPQTables(unittest.TestCase):
|
||||
|
||||
def test_4bit(self):
|
||||
self.do_test(32, 4, nbit=4)
|
||||
|
||||
def test_dsub2_4bit(self):
|
||||
# ksub = 16, so the SIMD dsub2 kernels run. M = 8 is even.
|
||||
self.do_test(16, 2, nbit=4)
|
||||
|
||||
def test_dsub2_4bit_odd(self):
|
||||
# M = 9 is odd, so the dsub2 kernels must also write the last
|
||||
# subquantizer. test_dsub2_odd cannot cover this case, because
|
||||
# nbit=8 does not reach the dsub2 kernels.
|
||||
self.do_test(18, 2, nbit=4)
|
||||
|
||||
def test_dsub2_2bit(self):
|
||||
# ksub = 4 is not a multiple of 8, so the dsub2 kernels must be
|
||||
# skipped in favour of the generic path
|
||||
self.do_test(16, 2, nbit=2)
|
||||
|
||||
Reference in New Issue
Block a user