Files
faiss/tests/test_sq_rvv_tail_and_semantics.cpp
郑紫阳10295048 0f23e6a2ef Add RVV SIMD specialization for scalar quantizer distance compute (#5539)
Summary:
This PR adds RISC-V Vector Extension SIMD specialization for the scalar
quantizer distance computation, following the codec/quantizer/similarity
specialization already present for other SIMD levels. The kernels use the
runtime vector-length aware m8 vector configuration and are shared across
all RVV hardware widths, instead of assuming a fixed vector length.

Changes
Add RVV specialized L2 / inner-product distance kernels for the scalar
quantizer codecs:

- QT_4bit / QT_4bit_uniform
- QT_6bit
- QT_8bit / QT_8bit_uniform
- QT_8bit_direct / QT_8bit_direct_signed
- QT_bf16
- QT_fp16

Each kernel reuses the reconstructed (quantizer) L2 / IP formulation that
avoids per-dimension reconstruction, keeping the hot loop in the integer
domain and applying the final scale at the tail.

Performance
Cmd: 1M 768 <SQ> 32 100 16 1 1000 1 <metric>  (search ms/query, lower is better)
Δ_SQ%: SQ scalar baseline -> RVV;
Δ_F%: Faiss flat baseline -> RVV.

| 序号 | QT_TYPE               | Metric | F1(ms) | SQ1(ms) | F2(ms) | SQ2(ms) | Δ_SQ%   | Δ_F%   |
|------|-----------------------|--------|--------|---------|--------|---------|---------|--------|
| 01   | QT_4bit               | L2     | 1.841  | 6.231   | 1.824  | 1.450   | -76.73% | -0.92% |
| 02   | QT_4bit               | IP     | 1.652  | 6.086   | 1.674  | 1.273   | -79.08% | +1.33% |
| 03   | QT_4bit_uniform       | L2     | 1.806  | 2.560   | 1.806  | 1.135   | -55.66% | +0.00% |
| 04   | QT_4bit_uniform       | IP     | 1.612  | 6.007   | 1.597  | 1.244   | -79.29% | -0.93% |
| 05   | QT_6bit               | L2     | 1.801  | 5.701   | 1.805  | 1.500   | -73.69% | +0.22% |
| 06   | QT_6bit               | IP     | 1.608  | 5.706   | 1.619  | 1.249   | -78.11% | +0.68% |
| 07   | QT_8bit               | L2     | 1.821  | 3.736   | 1.907  | 1.394   | -62.69% | +4.72% |
| 08   | QT_8bit               | IP     | 1.704  | 3.378   | 1.730  | 1.277   | -62.20% | +1.53% |
| 09   | QT_8bit_direct        | L2     | 1.977  | 0.280   | 1.947  | 0.054   | -80.71% | -1.52% |
| 10   | QT_8bit_direct        | IP     | 1.709  | 0.458   | 1.740  | 0.099   | -78.38% | +1.81% |
| 11   | QT_8bit_direct_signed | L2     | 1.581  | 3.234   | 1.572  | 0.761   | -76.47% | -0.57% |
| 12   | QT_8bit_direct_signed | IP     | 1.740  | 0.414   | 1.748  | 0.148   | -64.25% | +0.46% |
| 13   | QT_8bit_uniform       | L2     | 1.943  | 3.204   | 1.928  | 0.927   | -71.07% | -0.77% |
| 14   | QT_8bit_uniform       | IP     | 1.727  | 3.121   | 1.744  | 0.947   | -69.66% | +0.98% |
| 15   | QT_bf16               | L2     | 1.862  | 2.889   | 1.894  | 1.466   | -49.26% | +1.72% |
| 16   | QT_bf16               | IP     | 1.619  | 2.846   | 1.793  | 1.204   | -57.70% | +10.75% |
| 17   | QT_fp16               | L2     | 2.011  | 5.691   | 1.950  | 1.570   | -72.41% | -3.03% |
| 18   | QT_fp16               | IP     | 1.797  | 5.584   | 1.790  | 1.189   | -78.71% | -0.39% |

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

Reviewed By: mnorris11

Differential Revision: D117914328

Pulled By: juancarpio27

fbshipit-source-id: 879863b9ab4da6d1c8685892ccc3d64190975181
2026-09-17 13:34:43 -07:00

2071 lines
81 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.
*/
/*
*
* SQ-RVV review-response tests (see
* docs/sq_rvv_review_analysis_and_test_plan.md for the full analysis):
*
* D1 CounterexampleRegression — uniform L2 half-grid semantics (the
* review's literal q == recon(c) counterexample)
* D2 TailBoundaryParity — d past VLMAX so the tail pass runs
* (may pass on well-behaved hardware; regression + VLEN sensitivity)
* D3 DirectBitExact — in-contract integer data => exactly equal
* D5 OverflowGuard — large-d float-domain parity for the direct
* codecs (the kernels accumulate in f32; f32 saturates near 3.4e38,
* so there is no integer-wraparound failure mode to guard)
* D5b LargeDimParity — large-d direct-codec parity vs the exact
* double reference (regression net for the old per-lane i32/u32
* wraparound and the signed-IP query-bias overflow, both gone now
* that the kernels are float-domain)
* D6 ZeroDim — d == 0 must return 0, not hang; each case
* runs in a forked child under alarm() so a hang only fails its own
* case (POSIX only; skipped on Windows hosts)
* D7 SpecialQueries — clamp boundaries / systematic-value
* queries / huge-but-finite magnitudes
* D8 SmallDimensionParity — odd & very small d (1/2/3/5/7) across every
* qtype x metric (packing tail: 4-bit odd-d padding, 6-bit ng==0)
* D9 ReconQuery — q == recon(code) => L2 distance ~ 0, for
* every qtype (not just uniform)
* D10 FloatQueryParity — direct codecs retain FLOAT-query
* semantics: fractional queries must match the scalar NONE
* reference (the review's literal q=0.75 counterexample; an
* earlier integer-domain kernel truncated the query and ranked
* candidates differently)
*
* Expected status against the CURRENT sq-rvv.cpp (all codecs, direct
* included, are float-domain in the query):
* D1: PASS (uniform L2/IP float-domain semantics)
* D2: PASS expected on board
* D3: PASS expected (in-contract integer data keeps f32 sums exact)
* D5: PASS (float-domain, no wraparound)
* D5b: PASS (float-domain, no wraparound / bias overflow)
* D6: PASS (all kernels guard vsetvl(0) with `d > 0 ? d : 1`)
* D7: PASS (exact float-domain)
* D8: PASS expected
* D9: PASS expected
* D10: PASS (float-query semantics retained)
*/
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <memory>
#include <random>
#include <vector>
#ifndef _WIN32
#include <sys/wait.h>
#include <unistd.h>
#include <csignal>
#endif
// D5b derives the test dimensions from the board's real VLMAX.
#if defined(__riscv) && defined(COMPILE_SIMD_RISCV_RVV)
#include <riscv_vector.h>
#define FAISS_RVV_HAS_INTRINSICS 1
#endif
#include <faiss/IndexIVF.h>
#include <faiss/impl/ScalarQuantizer.h>
#include <faiss/impl/scalar_quantizer/distance_computers.h>
#include <faiss/impl/scalar_quantizer/quantizers.h>
#include <faiss/impl/scalar_quantizer/scanners.h>
#include <faiss/utils/distances.h>
#include <faiss/utils/random.h>
#include <faiss/utils/simd_levels.h>
namespace {
using SQ = faiss::ScalarQuantizer;
using faiss::METRIC_INNER_PRODUCT;
using faiss::METRIC_L2;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
struct ScopedSIMDLevel {
faiss::SIMDLevel original;
explicit ScopedSIMDLevel(faiss::SIMDLevel level)
: original(faiss::SIMDConfig::get_level()) {
faiss::SIMDConfig::set_level(level);
}
~ScopedSIMDLevel() {
faiss::SIMDConfig::set_level(original);
}
};
std::vector<float> make_random_vectors(size_t n, size_t d, int seed = 1234) {
std::vector<float> x(n * d);
faiss::float_randn(x.data(), x.size(), seed);
return x;
}
/// Integer-valued floats uniformly in [lo, hi] — exact in f32.
static std::vector<float> random_int_floats(
size_t n,
size_t d,
int lo,
int hi,
int seed) {
std::mt19937 rng(seed);
std::uniform_int_distribution<int> dist(lo, hi);
std::vector<float> x(n * d);
for (size_t i = 0; i < x.size(); i++)
x[i] = static_cast<float>(dist(rng));
return x;
}
static const char* metric_name(faiss::MetricType m) {
return m == METRIC_L2 ? "L2" : "IP";
}
/// Codec family classification (drives data/query policy in D2/D7).
enum class QClass { FloatDomain, UniformApprox, Direct };
static QClass class_of(SQ::QuantizerType qtype) {
switch (qtype) {
case SQ::QT_4bit_uniform:
case SQ::QT_8bit_uniform:
return QClass::UniformApprox;
case SQ::QT_8bit_direct:
case SQ::QT_8bit_direct_signed:
return QClass::Direct;
default:
return QClass::FloatDomain;
}
}
static bool is_direct(SQ::QuantizerType qtype) {
return class_of(qtype) == QClass::Direct;
}
/// All SQ qtypes with RVV specializations.
static const std::vector<SQ::QuantizerType>& all_rvv_qtypes() {
static const std::vector<SQ::QuantizerType> qtypes = {
SQ::QT_4bit,
SQ::QT_4bit_uniform,
SQ::QT_6bit,
SQ::QT_8bit,
SQ::QT_8bit_uniform,
SQ::QT_8bit_direct,
SQ::QT_8bit_direct_signed,
SQ::QT_fp16,
SQ::QT_bf16,
};
return qtypes;
}
/// One scalar (NONE) + one RVV distance computer over the same trained
/// parameters and code array. codes may be null when only query_to_code on
/// caller-supplied bytes is used.
struct DcPair {
std::unique_ptr<SQ::SQDistanceComputer> scalar_dc;
std::unique_ptr<SQ::SQDistanceComputer> rvv_dc;
};
static DcPair make_dc_pair(
SQ::QuantizerType qtype,
faiss::MetricType metric,
size_t d,
const std::vector<float>& trained,
const uint8_t* codes,
size_t code_size) {
DcPair p;
p.scalar_dc.reset(
faiss::scalar_quantizer::sq_select_distance_computer<
faiss::SIMDLevel::NONE>(metric, qtype, d, trained));
{
ScopedSIMDLevel _(faiss::SIMDLevel::RISCV_RVV);
p.rvv_dc.reset(
faiss::scalar_quantizer::sq_select_distance_computer<
faiss::SIMDLevel::RISCV_RVV>(
metric, qtype, d, trained));
}
EXPECT_NE(p.scalar_dc, nullptr);
EXPECT_NE(p.rvv_dc, nullptr);
if (p.scalar_dc) {
p.scalar_dc->codes = codes;
p.scalar_dc->code_size = code_size;
}
if (p.rvv_dc) {
p.rvv_dc->codes = codes;
p.rvv_dc->code_size = code_size;
}
return p;
}
/// trained-layout access: uniform = [vmin, vdiff]; nonuniform = [vmin(d),
/// vdiff(d)]; fp16/bf16/direct have no trained range.
static void trained_range(
SQ::QuantizerType qtype,
size_t d,
const std::vector<float>& trained,
float& vmin,
float& vdiff,
bool& has_range) {
has_range = false;
vmin = 0.0f;
vdiff = 1.0f;
switch (class_of(qtype)) {
case QClass::UniformApprox:
if (trained.size() >= 2) {
vmin = trained[0];
vdiff = trained[1];
has_range = true;
}
break;
case QClass::FloatDomain:
// fp16/bf16 have empty trained; 4/6/8bit nonuniform have 2*d
if (trained.size() >= 2 * d && d > 0) {
vmin = trained[0];
vdiff = trained[d];
has_range = true;
}
break;
case QClass::Direct:
break;
}
}
// ===========================================================================
// D1 — Uniform L2 must keep the scalar float-domain semantics.
//
// Scalar: L2(q, code) = (q - recon(c))^2 with recon(c) = vmin +
// vdiff*(c+0.5)/max_code — a HALF-INTEGER grid in code units.
//
// The float-domain kernels retain the original query (no integer-grid
// pre-quantization), so they must match the scalar argmin and distance.
// Sweep A pins the review's literal counterexample (q = recon(c) must be
// distance 0 to code c); sweep B pins full parity at integer queries.
// ===========================================================================
struct UniformL2Params {
SQ::QuantizerType qtype;
int nbits; // 4 or 8
const char* name;
};
class SQRVVCounterexample : public ::testing::TestWithParam<UniformL2Params> {};
TEST_P(SQRVVCounterexample, HalfGridCounterexample) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const int max_code = (1 << p.nbits) - 1; // 15 or 255
const size_t d = 1;
// vmin=0, vdiff=max_code => recon(c) = c + 0.5, code-unit step exactly 1
std::vector<float> trained = {0.0f, float(max_code)};
// code_size: d=1 -> 1 byte for both codecs
const size_t code_size = 1;
DcPair dc =
make_dc_pair(p.qtype, METRIC_L2, d, trained, nullptr, code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
// Codes: dim 0 lives in the low nibble for Codec4bit; byte value == code.
std::vector<uint8_t> bytes(max_code + 1);
for (int c = 0; c <= max_code; c++) {
bytes[c] = static_cast<uint8_t>(c);
}
// ---- Sweep A: q = recon(c) = c + 0.5 (the review counterexample) ----
for (int code = 0; code <= max_code; code++) {
std::vector<float> q = {float(code) + 0.5f};
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
std::vector<float> ref(max_code + 1), tst(max_code + 1);
for (int c = 0; c <= max_code; c++) {
ref[c] = dc.scalar_dc->query_to_code(&bytes[c]);
tst[c] = dc.rvv_dc->query_to_code(&bytes[c]);
}
// (a) distance to the reconstruction point of `code` is minimal
int ref_argmin =
int(std::min_element(ref.begin(), ref.end()) - ref.begin());
int tst_argmin =
int(std::min_element(tst.begin(), tst.end()) - tst.begin());
EXPECT_EQ(ref_argmin, code)
<< p.name << " sweep A: scalar argmin wrong at code=" << code;
EXPECT_EQ(tst_argmin, code)
<< p.name << " sweep A: RVV argmin=" << tst_argmin
<< ", expected " << code << " (half-grid semantics violated)";
// (b) RVV distance at the reconstruction point is ~0
EXPECT_NEAR(tst[code], 0.0f, 1e-3f)
<< p.name << " sweep A: q=recon(" << code
<< ") but RVV distance to code " << code << " = " << tst[code];
}
// ---- Sweep B: q = c (integer code units) — full parity required ----
for (int code = 0; code <= max_code; code++) {
std::vector<float> q = {float(code)};
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
for (int c = 0; c <= max_code; c++) {
float ref = dc.scalar_dc->query_to_code(&bytes[c]);
float tst = dc.rvv_dc->query_to_code(&bytes[c]);
// tie cases (c == code +/- 0.5 recon) are exact for scalar too
EXPECT_NEAR(tst, ref, 1e-3f)
<< p.name << " sweep B: q=" << code << " code=" << c
<< " ref=" << ref << " rvv=" << tst;
}
}
}
INSTANTIATE_TEST_SUITE_P(
D1,
SQRVVCounterexample,
::testing::Values(
UniformL2Params{SQ::QT_4bit_uniform, 4, "4bit_uniform_L2"},
UniformL2Params{SQ::QT_8bit_uniform, 8, "8bit_uniform_L2"}));
/// vdiff == 0: the trained range collapses to a single point recon(c) ==
/// vmin for every code. The scalar distance is therefore NOT the all-zero
/// defect:
/// L2(q, code) = sum_i (q_i - vmin)^2
/// IP(q, code) = vmin * sum_i q_i
/// Covers every uniform codec under both metrics — the float-domain kernels
/// must match the scalar NONE reference to reassociation precision.
/// Case A: all-zero code. Case B: max code (0xFF / 0x0F) with a huge
/// finite query — the review's literal counterexample. An intermediate
/// sum(q_i*c_i) overflows to inf there, and the retired factorization
/// K_q + scale*sum(q_i*c_i) evaluated 0*inf == NaN; the per-dim
/// pre-scaled form K_q + sum((q_i*scale)*c_i) must return exactly what
/// the scalar reference returns (0 for vmin==0, vmin*sum(q) otherwise),
/// finite in every case.
struct ZeroVdiffParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
const char* name;
};
class SQRVVZeroVdiff : public ::testing::TestWithParam<ZeroVdiffParams> {};
TEST_P(SQRVVZeroVdiff, UniformVdiffZero) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t d = 8;
const float vmin = 3.25f;
std::vector<float> trained = {vmin, 0.0f}; // vdiff == 0
std::vector<float> xq = make_random_vectors(1, d, 99);
// code_size: 4-bit packs two dims per byte, 8-bit one byte per dim.
const size_t code_size = (p.qtype == SQ::QT_4bit_uniform) ? (d + 1) / 2 : d;
DcPair dc = make_dc_pair(p.qtype, p.metric, d, trained, nullptr, code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
dc.scalar_dc->set_query(xq.data());
dc.rvv_dc->set_query(xq.data());
// ---- Case A: all-zero code (recon == vmin for every dim) ----
{
std::vector<uint8_t> zero_code(code_size, 0);
float ref = dc.scalar_dc->query_to_code(zero_code.data());
float tst = dc.rvv_dc->query_to_code(zero_code.data());
float expect = 0.0f;
for (size_t i = 0; i < d; i++) {
if (p.metric == METRIC_L2) {
float diff = xq[i] - vmin;
expect += diff * diff;
} else {
expect += xq[i] * vmin;
}
}
const float tol = 1e-3f * std::max(1.0f, std::abs(expect));
EXPECT_NEAR(ref, expect, tol) << p.name << ": scalar reference drifted";
EXPECT_NEAR(tst, expect, tol)
<< p.name << ": vdiff==0 RVV returned " << tst << ", expected "
<< expect << " (all-distances-zero defect)";
}
// ---- Case B: max code + huge finite query (review counterexample) ----
// d=1, trained={0,0}, q=FLT_MAX, code 15/255. IP: the scalar per-dim
// form is q_i * recon_i = q_i * 0 == 0, so the exact answer is 0; the
// retired factorization K_q + scale*sum(q_i*c_i) returned NaN there
// (intermediate sum overflowed to inf, then 0*inf). L2 legitimately
// overflows in BOTH implementations ((FLT_MAX)^2 -> inf, per-dim
// semantics), so it only asserts scalar/RVV agreement.
{
const size_t d1 = 1;
std::vector<float> trained1 = {0.0f, 0.0f}; // vdiff == 0, vmin == 0
DcPair dc1 = make_dc_pair(p.qtype, p.metric, d1, trained1, nullptr, 1);
ASSERT_TRUE(dc1.scalar_dc && dc1.rvv_dc);
std::vector<float> q1 = {std::numeric_limits<float>::max()};
dc1.scalar_dc->set_query(q1.data());
dc1.rvv_dc->set_query(q1.data());
std::vector<uint8_t> max_code1(1, 0xFF);
float ref = dc1.scalar_dc->query_to_code(max_code1.data());
float tst = dc1.rvv_dc->query_to_code(max_code1.data());
if (p.metric == METRIC_INNER_PRODUCT) {
EXPECT_EQ(ref, 0.0f) << p.name << ": scalar reference drifted";
EXPECT_EQ(tst, 0.0f)
<< p.name << ": expected exact 0 (recon==0, huge q), got "
<< tst << " (0*inf NaN from sum-then-multiply?)";
} else {
EXPECT_FALSE(std::isfinite(ref))
<< p.name << ": scalar L2 should overflow";
EXPECT_EQ(tst, ref)
<< p.name << ": L2 overflow must agree with scalar";
}
}
// ---- Case C: max code + huge finite query, vmin != 0 (IP only) ----
// vdiff == 0, vmin = 0.5: the true IP is finite and nonzero
// (vmin * q), while an overflowing sum(q_i*c_i) intermediate would
// still yield NaN — catches kernels that collapse the result to
// zero as well as the NaN defect.
if (p.metric == METRIC_INNER_PRODUCT) {
const size_t d1 = 1;
const float vmin1 = 0.5f;
std::vector<float> trained1 = {vmin1, 0.0f}; // vdiff == 0
DcPair dc1 = make_dc_pair(p.qtype, p.metric, d1, trained1, nullptr, 1);
ASSERT_TRUE(dc1.scalar_dc && dc1.rvv_dc);
std::vector<float> q1 = {3.0e36f}; // 3e36 * 255 overflows f32
dc1.scalar_dc->set_query(q1.data());
dc1.rvv_dc->set_query(q1.data());
std::vector<uint8_t> max_code1(1, 0xFF);
float ref = dc1.scalar_dc->query_to_code(max_code1.data());
float tst = dc1.rvv_dc->query_to_code(max_code1.data());
EXPECT_TRUE(std::isfinite(tst))
<< p.name << ": Case C returned non-finite " << tst;
EXPECT_NEAR(tst, ref, std::abs(ref) * 1e-6f)
<< p.name << ": Case C parity, expected " << ref;
EXPECT_NEAR(ref, vmin1 * q1[0], std::abs(vmin1 * q1[0]) * 1e-6f)
<< p.name << ": Case C scalar reference drifted";
}
}
INSTANTIATE_TEST_SUITE_P(
D1,
SQRVVZeroVdiff,
::testing::Values(
ZeroVdiffParams{
SQ::QT_8bit_uniform,
METRIC_L2,
"8bit_uniform_L2"},
ZeroVdiffParams{
SQ::QT_4bit_uniform,
METRIC_L2,
"4bit_uniform_L2"},
ZeroVdiffParams{
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
"8bit_uniform_IP"},
ZeroVdiffParams{
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
"4bit_uniform_IP"}),
[](const ::testing::TestParamInfo<ZeroVdiffParams>& info) {
return std::string(info.param.name);
});
// ===========================================================================
// D1c — Uniform IP query-constant overflow semantics. IP = K_q + S with
// K_q = sum_i (q_i * c0) accumulated PER DIM. The retired form
// K_q = c0 * sum(q) overflowed the bare sum(q) to inf first
// (multi-dim huge finite query), then c0 == 0 produced 0*inf ==
// NaN; the scalar per-dim form returns exactly 0 (every term
// q_i * 0 == 0). Same contract checked for both uniform codecs
// with c0 == 0 (vmin == 0, vdiff == 0).
// ===========================================================================
TEST(SQRVVQueryConstOverflow, HugeQueryZeroC0ReturnsZero) {
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
// d=8 so sum(q) = 8*FLT_MAX overflows f32; each individual q_i*c0
// term stays exactly 0.
const size_t d = 8;
std::vector<float> trained = {0.0f, 0.0f}; // vdiff == 0, vmin == 0
std::vector<float> q(d, std::numeric_limits<float>::max());
const size_t code_size_8 = d;
const size_t code_size_4 = (d + 1) / 2;
struct Case {
SQ::QuantizerType qtype;
faiss::MetricType metric;
size_t code_size;
const char* name;
};
const Case cases[] = {
{SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
code_size_8,
"8bit_uniform_IP"},
{SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
code_size_4,
"4bit_uniform_IP"},
};
for (const auto& c : cases) {
DcPair dc = make_dc_pair(
c.qtype, c.metric, d, trained, nullptr, c.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
std::vector<uint8_t> code(c.code_size, 0xFF);
float ref = dc.scalar_dc->query_to_code(code.data());
float tst = dc.rvv_dc->query_to_code(code.data());
EXPECT_EQ(ref, 0.0f) << c.name << ": scalar per-dim reference drifted";
EXPECT_EQ(tst, 0.0f) << c.name << ": huge q with c0==0 returned " << tst
<< " (0*inf NaN from c0*sum(q)?)";
}
}
// ===========================================================================
// D1b — symmetric_dis overflow semantics. Identical codes must return
// exactly 0. The retired 8bit_uniform-L2 factored form
// float(sum(delta_c^2)) * (a*a) returned 0 * inf == NaN when
// a = vdiff/255 was so large that a*a overflowed; the per-dim
// scaled form (diff = a*(c1-c2) before squaring) keeps identical
// codes at exact 0. Same contract checked for all uniform codecs.
// ===========================================================================
TEST(SQRVVSymmetricOverflow, HugeVdiffIdenticalCodesReturnZero) {
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t d = 8;
// a = vdiff/255 ~ 1.18e36, a*a ~ 1.4e72 -> inf in f32. Finite codes
// stay well inside the byte grid; recon itself does not overflow
// (a*255 == vdiff <= ~3e38).
const float huge_vdiff = 3.0e38f;
const float vmin = 0.0f;
struct Case {
SQ::QuantizerType qtype;
size_t code_size;
const char* name;
};
const Case cases[] = {
{SQ::QT_8bit_uniform, d, "8bit_uniform_L2"},
{SQ::QT_4bit_uniform, (d + 1) / 2, "4bit_uniform_L2"},
};
for (const auto& c : cases) {
std::vector<float> trained = {vmin, huge_vdiff};
DcPair dc = make_dc_pair(
c.qtype, METRIC_L2, d, trained, nullptr, c.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
// Two all-zero codes (identical pair (0,0) and the degenerate
// pair (0,1): both dims equal in every code).
std::vector<uint8_t> zero_codes(2 * c.code_size, 0);
dc.scalar_dc->codes = zero_codes.data();
dc.scalar_dc->code_size = c.code_size;
dc.rvv_dc->codes = zero_codes.data();
dc.rvv_dc->code_size = c.code_size;
// (a) identical codes -> exact 0 (the review's counterexample)
for (size_t i = 0; i < 2; i++) {
float ref = dc.scalar_dc->symmetric_dis(i, i);
float tst = dc.rvv_dc->symmetric_dis(i, i);
EXPECT_EQ(ref, 0.0f) << c.name << ": scalar identical-code drifted";
EXPECT_EQ(tst, 0.0f)
<< c.name << ": identical codes returned " << tst
<< " (0*inf NaN from float(sum)*(a*a)?)";
}
// (b) distinct codes -> parity with the scalar per-dim form
float ref = dc.scalar_dc->symmetric_dis(0, 1);
float tst = dc.rvv_dc->symmetric_dis(0, 1);
EXPECT_EQ(tst, ref) << c.name << ": distinct-code parity rvv=" << tst
<< " scalar=" << ref;
}
}
// ===========================================================================
// D2 — Tail-boundary parity: dimensions that split the vector loop into
// (full chunks) + (short tail). The tail pass must not corrupt lanes
// covered by the final reduction. On well-behaved hardware this may
// pass even with the tail-agnostic bug; it is the regression net and
// the VLEN-sensitivity probe (run also under QEMU vlen=256/512).
// ===========================================================================
struct TailParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
size_t d;
float tol;
const char* name;
};
class SQRVVTailBoundary : public ::testing::TestWithParam<TailParams> {};
TEST_P(SQRVVTailBoundary, ParityAtTailDimensions) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t n_train = 128;
const size_t n_db = 4;
// direct codecs: encoder casts float->uint8; keep ALL data/query values
// on the in-contract integer grid (out-of-contract values make the
// scalar float-domain reference and the clamping integer kernel
// incomparable by design).
const bool direct = is_direct(p.qtype);
const int dlo = (p.qtype == SQ::QT_8bit_direct_signed) ? -128 : 0;
const int dhi = (p.qtype == SQ::QT_8bit_direct_signed) ? 127 : 255;
std::vector<float> xb = direct
? random_int_floats(n_train, p.d, dlo, dhi, 42)
: make_random_vectors(n_train, p.d, 42);
std::vector<float> xq = direct ? random_int_floats(1, p.d, dlo, dhi, 99)
: make_random_vectors(1, p.d, 99);
if (p.metric == METRIC_INNER_PRODUCT && !direct) {
// Normalize for IP parity. NOTE: skip for direct codecs — renorm
// would leave the integer grid and break the in-contract premise
// (both computers still see identical data either way).
faiss::fvec_renorm_L2(p.d, n_train, xb.data());
faiss::fvec_renorm_L2(p.d, 1, xq.data());
}
SQ sq(p.d, p.qtype);
sq.train(n_train, xb.data());
std::vector<uint8_t> db_codes(sq.code_size * n_db, 0);
sq.compute_codes(xb.data(), db_codes.data(), n_db);
DcPair dc = make_dc_pair(
p.qtype, p.metric, p.d, sq.trained, db_codes.data(), sq.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
dc.scalar_dc->set_query(xq.data());
dc.rvv_dc->set_query(xq.data());
SCOPED_TRACE(
::testing::Message()
<< p.name << " metric=" << metric_name(p.metric) << " d=" << p.d);
// query_to_code
for (size_t i = 0; i < n_db; i++) {
const uint8_t* code = db_codes.data() + i * sq.code_size;
float ref = dc.scalar_dc->query_to_code(code);
float tst = dc.rvv_dc->query_to_code(code);
ASSERT_TRUE(std::isfinite(tst)) << p.name << " non-finite";
ASSERT_TRUE(std::isfinite(ref)) << p.name << " non-finite ref";
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * 1e-4f)
<< p.name << " query_to_code code=" << i;
}
// zero + max codes
{
std::vector<uint8_t> zero_code(sq.code_size, 0);
float ref = dc.scalar_dc->query_to_code(zero_code.data());
float tst = dc.rvv_dc->query_to_code(zero_code.data());
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * 1e-4f)
<< p.name << " zero code";
}
if (p.qtype != SQ::QT_fp16 && p.qtype != SQ::QT_bf16) {
std::vector<uint8_t> max_code(sq.code_size, 0xff);
float ref = dc.scalar_dc->query_to_code(max_code.data());
float tst = dc.rvv_dc->query_to_code(max_code.data());
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * 1e-4f)
<< p.name << " max code";
}
// symmetric_dis
for (size_t i = 0; i + 1 < n_db; i += 2) {
float ref = dc.scalar_dc->symmetric_dis(i, i + 1);
float tst = dc.rvv_dc->symmetric_dis(i, i + 1);
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * 1e-4f)
<< p.name << " symmetric_dis(" << i << "," << (i + 1) << ")";
}
// batch_4
{
const uint8_t *c0 = db_codes.data(),
*c1 = db_codes.data() + sq.code_size,
*c2 = db_codes.data() + 2 * sq.code_size,
*c3 = db_codes.data() + 3 * sq.code_size;
float r0, r1, r2, r3, t0, t1, t2, t3;
dc.scalar_dc->query_to_codes_batch_4(c0, c1, c2, c3, r0, r1, r2, r3);
dc.rvv_dc->query_to_codes_batch_4(c0, c1, c2, c3, t0, t1, t2, t3);
EXPECT_NEAR(r0, t0, p.tol + std::abs(r0) * 1e-4f)
<< p.name << " batch_4[0]";
EXPECT_NEAR(r1, t1, p.tol + std::abs(r1) * 1e-4f)
<< p.name << " batch_4[1]";
EXPECT_NEAR(r2, t2, p.tol + std::abs(r2) * 1e-4f)
<< p.name << " batch_4[2]";
EXPECT_NEAR(r3, t3, p.tol + std::abs(r3) * 1e-4f)
<< p.name << " batch_4[3]";
}
// scanner parity
std::unique_ptr<faiss::InvertedListScanner> scalar_scanner(
faiss::scalar_quantizer::sq_select_InvertedListScanner<
faiss::SIMDLevel::NONE>(
p.qtype,
p.metric,
p.d,
sq.code_size,
sq.trained,
nullptr,
false,
nullptr,
false));
ASSERT_NE(scalar_scanner, nullptr);
scalar_scanner->set_query(xq.data());
scalar_scanner->set_list(0, 0.0f);
std::unique_ptr<faiss::InvertedListScanner> rvv_scanner;
{
ScopedSIMDLevel _(faiss::SIMDLevel::RISCV_RVV);
rvv_scanner.reset(sq.select_InvertedListScanner(
p.metric, nullptr, false, nullptr, false));
}
ASSERT_NE(rvv_scanner, nullptr);
rvv_scanner->set_query(xq.data());
rvv_scanner->set_list(0, 0.0f);
for (size_t i = 0; i < n_db; i++) {
const uint8_t* code = db_codes.data() + i * sq.code_size;
float ref = scalar_scanner->distance_to_code(code);
float tst = rvv_scanner->distance_to_code(code);
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * 1e-4f)
<< p.name << " scanner code=" << i;
}
}
/*
* Dimension selection (VLEN=128 board): each case guarantees
* ">=1 full chunk + 0 < tail < VLMAX" for that kernel's SEW/LMUL.
* e8m2 on nb=ceil(d/2) bytes (4bit_uniform L2): d=94 -> 47B = 32+15,
* d=127 -> 63B = 32+31, d=191 -> 95B = 64+31
* e8m2 on d (8bit_uniform L2/IP): 33 = 32+1, 97 = 96+1, 129 = 128+1
* e8m1 on nb bytes (4bit nonuniform L2/IP): 65 -> 33B = 32+1,
* 94 -> 47B = 32+15, 129 -> 65B = 64+1
* e8m1 on d (8bit nonuniform): 17 = 16+1, 33 = 32+1, 81 = 80+1
* 6bit on ng=d/4 groups: 68 -> 17g = 16+1, 69 -> +1 scalar dim,
* 127 -> 31g = 16+15 + 3 scalar dims
* bf16 e16m2: 17 = 16+1, 33 = 32+1; fp16 e16m1: 9, 17, 23
* Tolerances: all float-domain codecs (uniform and non-uniform) 1e-3
* (reassociation); direct in-contract integer data 1e-3 (exact integer
* sums). Uniform L2/IP are now float-domain — their semantic contract is
* pinned tightly by D1.
*/
INSTANTIATE_TEST_SUITE_P(
D2,
SQRVVTailBoundary,
::testing::Values(
// ---- QT_4bit (nonuniform): e8m1 on ceil(d/2) bytes
TailParams{SQ::QT_4bit, METRIC_L2, 65, 1e-3f, "4bit/d65"},
TailParams{SQ::QT_4bit, METRIC_L2, 94, 1e-3f, "4bit/d94"},
TailParams{SQ::QT_4bit, METRIC_L2, 129, 1e-3f, "4bit/d129"},
TailParams{
SQ::QT_4bit,
METRIC_INNER_PRODUCT,
65,
1e-3f,
"4bit_IP/d65"},
TailParams{
SQ::QT_4bit,
METRIC_INNER_PRODUCT,
94,
1e-3f,
"4bit_IP/d94"},
TailParams{
SQ::QT_4bit,
METRIC_INNER_PRODUCT,
129,
1e-3f,
"4bit_IP/d129"},
// ---- QT_4bit_uniform: L2 e8m2 on nbv bytes (odd-d
// padding byte is excluded -> nbv = (d+1)/2 - 1),
// IP e8m1 on nb bytes
TailParams{
SQ::QT_4bit_uniform,
METRIC_L2,
94,
1e-3f,
"4bit_uniform/d94"},
TailParams{
SQ::QT_4bit_uniform,
METRIC_L2,
127,
1e-3f,
"4bit_uniform/d127"},
TailParams{
SQ::QT_4bit_uniform,
METRIC_L2,
191,
1e-3f,
"4bit_uniform/d191"},
TailParams{
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
65,
1e-3f,
"4bit_uniform_IP/d65"},
TailParams{
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
129,
1e-3f,
"4bit_uniform_IP/d129"},
// ---- QT_6bit: vl=16 groups; d%4 scalar tail at 69/127
TailParams{SQ::QT_6bit, METRIC_L2, 68, 1e-3f, "6bit/d68"},
TailParams{SQ::QT_6bit, METRIC_L2, 69, 1e-3f, "6bit/d69"},
TailParams{SQ::QT_6bit, METRIC_L2, 127, 1e-3f, "6bit/d127"},
TailParams{
SQ::QT_6bit,
METRIC_INNER_PRODUCT,
68,
1e-3f,
"6bit_IP/d68"},
TailParams{
SQ::QT_6bit,
METRIC_INNER_PRODUCT,
127,
1e-3f,
"6bit_IP/d127"},
// ---- QT_8bit (nonuniform): e8m1
TailParams{SQ::QT_8bit, METRIC_L2, 17, 1e-3f, "8bit/d17"},
TailParams{SQ::QT_8bit, METRIC_L2, 33, 1e-3f, "8bit/d33"},
TailParams{SQ::QT_8bit, METRIC_L2, 81, 1e-3f, "8bit/d81"},
TailParams{
SQ::QT_8bit,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"8bit_IP/d17"},
TailParams{
SQ::QT_8bit,
METRIC_INNER_PRODUCT,
81,
1e-3f,
"8bit_IP/d81"},
// ---- QT_8bit_uniform: e8m2 on d
TailParams{
SQ::QT_8bit_uniform,
METRIC_L2,
33,
1e-3f,
"8bit_uniform/d33"},
TailParams{
SQ::QT_8bit_uniform,
METRIC_L2,
97,
1e-3f,
"8bit_uniform/d97"},
TailParams{
SQ::QT_8bit_uniform,
METRIC_L2,
129,
1e-3f,
"8bit_uniform/d129"},
TailParams{
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
33,
1e-3f,
"8bit_uniform_IP/d33"},
TailParams{
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
129,
1e-3f,
"8bit_uniform_IP/d129"},
// ---- QT_8bit_direct (in-contract data -> ~exact)
TailParams{
SQ::QT_8bit_direct,
METRIC_L2,
33,
1e-3f,
"8bit_direct/d33"},
TailParams{
SQ::QT_8bit_direct,
METRIC_L2,
129,
1e-3f,
"8bit_direct/d129"},
TailParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
33,
1e-3f,
"8bit_direct_IP/d33"},
TailParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
129,
1e-3f,
"8bit_direct_IP/d129"},
// ---- QT_8bit_direct_signed
TailParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
33,
1e-3f,
"8bit_direct_signed/d33"},
TailParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
129,
1e-3f,
"8bit_direct_signed/d129"},
TailParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
33,
1e-3f,
"8bit_direct_signed_IP/d33"},
TailParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
129,
1e-3f,
"8bit_direct_signed_IP/d129"},
// ---- QT_bf16: e16m2 (vl=16)
TailParams{SQ::QT_bf16, METRIC_L2, 17, 1e-3f, "bf16/d17"},
TailParams{SQ::QT_bf16, METRIC_L2, 33, 1e-3f, "bf16/d33"},
TailParams{
SQ::QT_bf16,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"bf16_IP/d17"},
TailParams{
SQ::QT_bf16,
METRIC_INNER_PRODUCT,
33,
1e-3f,
"bf16_IP/d33"},
// ---- QT_fp16: e16m1 (vl=8)
TailParams{SQ::QT_fp16, METRIC_L2, 9, 1e-3f, "fp16/d9"},
TailParams{SQ::QT_fp16, METRIC_L2, 17, 1e-3f, "fp16/d17"},
TailParams{SQ::QT_fp16, METRIC_L2, 23, 1e-3f, "fp16/d23"},
TailParams{
SQ::QT_fp16,
METRIC_INNER_PRODUCT,
9,
1e-3f,
"fp16_IP/d9"},
TailParams{
SQ::QT_fp16,
METRIC_INNER_PRODUCT,
23,
1e-3f,
"fp16_IP/d23"}));
// ===========================================================================
// D3 — Direct codecs: in-contract (integer-grid) data must give EXACTLY
// equal results (both kernels are float-domain now; integer-grid
// distances and their partial sums stay < 2^24 at these d, so f32
// represents them exactly and the f32 accumulators cannot round).
// ===========================================================================
struct DirectExactParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
size_t d;
const char* name;
};
class SQRVVDirectBitExact : public ::testing::TestWithParam<DirectExactParams> {
};
TEST_P(SQRVVDirectBitExact, InContractExact) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t n_train = 64;
const size_t n_db = 4;
const int lo = (p.qtype == SQ::QT_8bit_direct_signed) ? -128 : 0;
const int hi = (p.qtype == SQ::QT_8bit_direct_signed) ? 127 : 255;
std::vector<float> xb = random_int_floats(n_train, p.d, lo, hi, 42);
std::vector<float> xq = random_int_floats(1, p.d, lo, hi, 99);
SQ sq(p.d, p.qtype);
sq.train(n_train, xb.data());
std::vector<uint8_t> codes(sq.code_size * n_db, 0);
sq.compute_codes(xb.data(), codes.data(), n_db);
DcPair dc = make_dc_pair(
p.qtype, p.metric, p.d, sq.trained, codes.data(), sq.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
// queries: random in-contract + the two clamp edges (all integer-grid)
std::vector<std::vector<float>> queries;
queries.push_back(xq);
{
std::vector<float> q_edge(p.d);
for (size_t i = 0; i < p.d; i++) {
q_edge[i] = (i % 2 == 0) ? float(lo) : float(hi);
}
queries.push_back(std::move(q_edge));
}
for (size_t qi = 0; qi < queries.size(); qi++) {
dc.scalar_dc->set_query(queries[qi].data());
dc.rvv_dc->set_query(queries[qi].data());
for (size_t i = 0; i < n_db; i++) {
const uint8_t* code = codes.data() + i * sq.code_size;
float ref = dc.scalar_dc->query_to_code(code);
float tst = dc.rvv_dc->query_to_code(code);
ASSERT_NEAR(tst, ref, 0.0f)
<< p.name << " query=" << qi << " code=" << i
<< " ref=" << ref << " rvv=" << tst
<< " — in-contract data must be bit-exact";
}
float rs = dc.scalar_dc->symmetric_dis(0, 1);
float ts = dc.rvv_dc->symmetric_dis(0, 1);
EXPECT_EQ(rs, ts) << p.name << " query=" << qi << " symmetric_dis";
}
}
INSTANTIATE_TEST_SUITE_P(
D3,
SQRVVDirectBitExact,
::testing::Values(
DirectExactParams{
SQ::QT_8bit_direct,
METRIC_L2,
33,
"8bit_direct_L2_d33"},
DirectExactParams{
SQ::QT_8bit_direct,
METRIC_L2,
96,
"8bit_direct_L2_d96"},
DirectExactParams{
SQ::QT_8bit_direct,
METRIC_L2,
129,
"8bit_direct_L2_d129"},
DirectExactParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
33,
"8bit_direct_IP_d33"},
DirectExactParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
129,
"8bit_direct_IP_d129"},
DirectExactParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
33,
"8bit_direct_signed_L2_d33"},
DirectExactParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
96,
"8bit_direct_signed_L2_d96"},
DirectExactParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
129,
"8bit_direct_signed_L2_d129"},
DirectExactParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
33,
"8bit_direct_signed_IP_d33"},
DirectExactParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
129,
"8bit_direct_signed_IP_d129"}));
// ===========================================================================
// D5 — Large-d parity for the direct codecs. The kernels are float-domain
// (f32 accumulators saturate near 3.4e38), so magnitudes that used to
// overflow 32-bit integer accumulators must simply stay in parity with
// the scalar reference.
// ===========================================================================
static void run_overflow_case(
SQ::QuantizerType qtype,
faiss::MetricType metric,
size_t d,
uint8_t code_byte,
float q_value,
const char* name,
double expected_exact) {
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
// direct codecs ignore trained params; d=34000 training would be
// wasteful, so hand the computer a minimal trained vector.
std::vector<float> trained;
DcPair dc = make_dc_pair(qtype, metric, d, trained, nullptr, d);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
std::vector<float> q(d, q_value);
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
std::vector<uint8_t> code(d, code_byte);
float ref = dc.scalar_dc->query_to_code(code.data());
float tst = dc.rvv_dc->query_to_code(code.data());
// Scalar reference accumulates in f32 — allow 1e-3 relative slack;
// the RVV float kernel matches it to reassociation precision.
const float tol = float(expected_exact * 1e-3);
EXPECT_NEAR(tst, expected_exact, tol)
<< name << " d=" << d << " rvv=" << tst
<< " expected=" << expected_exact;
EXPECT_NEAR(ref, expected_exact, tol) << name << " scalar ref drifted";
}
TEST(D5Overflow, DirectL2LargeDim) {
// 65025 per dim — magnitudes that overflowed the old i32 accumulator
// at d > ~33025; the float kernel must track the scalar reference.
const size_t d = 34000;
run_overflow_case(
SQ::QT_8bit_direct,
METRIC_L2,
d,
0x00,
255.0f,
"8bit_direct_L2",
double(d) * 65025.0);
}
TEST(D5Overflow, DirectIPLargeDim) {
const size_t d = 68000;
run_overflow_case(
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
d,
0xff,
255.0f,
"8bit_direct_IP",
double(d) * 65025.0);
}
TEST(D5Overflow, DirectSignedL2LargeDim) {
// query 127 in value space <-> storage byte 127+128 = 255;
// code byte 0x00 (value -128): diff 127-(-128) = 255 -> 65025 per dim.
const size_t d = 34000;
run_overflow_case(
SQ::QT_8bit_direct_signed,
METRIC_L2,
d,
0x00,
127.0f,
"8bit_direct_signed_L2",
double(d) * 65025.0);
}
// ===========================================================================
// D5b — Large-d parity at (and past) the old per-lane i32/u32 wrap point
// and the signed-IP query-bias overflow point. These dimensions sat
// exactly past the wrap thresholds of the retired integer-domain
// kernels; they are a regression net pinning the float-domain
// semantics at scale (RVV vs scalar parity + exact double reference).
// The chunk count is parameterized per case so EVERY integer
// accumulator a regression could reintroduce is pushed past its own
// wrap threshold:
// - L2 / signed i32 lanes: ceil(2^31 / 65025) = 33026
// - IP / unsigned u32 lanes: ceil(2^32 / 65025) = 66052
// - signed-IP worst-case term ceil(2^31 / 32640) = 65794
// (qs = -128 * code value +127 -> |term| = 32640 per chunk)
// ===========================================================================
static void run_flush_case(
SQ::QuantizerType qtype,
faiss::MetricType metric,
uint8_t code_byte,
float q_value,
size_t n_chunks,
const char* name,
double per_dim) {
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
// VLMAX at this VLEN, derived at runtime so the dimensions below sit
// past the old wrap thresholds on VLEN=128/256/... boards alike.
#if defined(FAISS_RVV_HAS_INTRINSICS)
const size_t vl = __riscv_vsetvlmax_e8m2();
#else
GTEST_SKIP() << "no RVV intrinsics at test compile time";
const size_t vl = 32;
#endif
// d = n_chunks full per-lane chunks + 1 tail dim. n_chunks must be
// chosen per case to cross THAT case's integer-accumulator threshold
// (see the derivation in the block comment above); the tail dim adds
// one extra term to lane 0 only and never rescues an undershoot.
const size_t d = vl * n_chunks + 1;
std::vector<float> trained;
DcPair dc = make_dc_pair(qtype, metric, d, trained, nullptr, d);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
std::vector<float> q(d, q_value);
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
std::vector<uint8_t> code(d, code_byte);
// Exact reference computed in the test (double: no drift at these
// magnitudes). Both kernels accumulate in f32, so the sequential /
// lane-reassociated sums drift by up to ~n*ulp(intermediate) — about
// 1e-2 relative at d ~ 5e5 (ulp ~4096 once the partial sum passes
// 2^35). That drift is still 100x tighter than a 2^31 integer
// wraparound (>= 100% error), which is the failure mode this test is
// a regression net for.
const double expected_exact = per_dim * double(d);
const float ref = dc.scalar_dc->query_to_code(code.data());
const float tst = dc.rvv_dc->query_to_code(code.data());
const double tol = std::fabs(expected_exact) * 2e-2;
EXPECT_NEAR(tst, expected_exact, tol)
<< name << " d=" << d << " rvv=" << tst
<< " expected=" << expected_exact;
EXPECT_NEAR(ref, tst, tol)
<< name << " d=" << d << " rvv=" << tst << " scalar=" << ref;
}
TEST(D5bLargeDim, DirectL2ParityPastOldWrap) {
// q=255, code=0: 65025 per dim. 33026 chunks push a per-lane signed
// i32 accumulator past 2^31 (33026*65025 = 2,147,565,650).
run_flush_case(
SQ::QT_8bit_direct,
METRIC_L2,
0x00,
255.0f,
33026,
"8bit_direct_L2_large",
65025.0);
}
TEST(D5bLargeDim, DirectIPParityPastOldWrap) {
// q=255, code=255: 65025 per dim, unsigned domain. An unsigned u32
// lane accumulator only wraps at 2^32: 66052 chunks are needed
// (66052*65025 = 4,295,215,800 > 2^32; 66051 falls short).
run_flush_case(
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
0xff,
255.0f,
66052,
"8bit_direct_IP_large",
65025.0);
}
TEST(D5bLargeDim, DirectSignedL2ParityPastOldWrap) {
// q=-128 vs code 0xff (value 127): diff -128-127 = -255 -> 65025/dim.
// Signed i32 lanes wrap past 2^31 at 33026 chunks.
run_flush_case(
SQ::QT_8bit_direct_signed,
METRIC_L2,
0xff,
-128.0f,
33026,
"8bit_direct_signed_L2_large",
65025.0);
}
TEST(D5bLargeDim, DirectSignedIPParityPastWorstTermWrap) {
// Worst-case SIGNED-IP decomposition term: query -128 against code
// 0xff (value +127) -> q_s * byte = -128*255 = -32640 per dim, the
// largest magnitude a per-lane i32 accumulator sees before the bias
// split. Signed i32 lanes wrap past 2^31 at 65794 chunks
// (65793*32640 = 2,147,483,520 < 2^31; 65794 crosses). The exact
// value-domain result stays -16256 * d.
run_flush_case(
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
0xff,
-128.0f,
65794,
"8bit_direct_signed_IP_large",
-16256.0);
}
TEST(D5bLargeDim, DirectSignedIPParityPastOldBiasOverflow) {
// An all-127 query makes the old integer bias 128*sum(q) cross 2^31
// at d = 132105 (int32_t UB there). The float kernel must stay in
// parity: exact = d * 127 * (0 - 128) = -16256 * d.
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t d = 132105;
std::vector<float> trained;
DcPair dc = make_dc_pair(
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
d,
trained,
nullptr,
d);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
std::vector<float> q(d, 127.0f);
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
std::vector<uint8_t> code(d, 0x00); // value -128: qs*(c-128) = 127*128
const double expected_exact = -16256.0 * double(d);
const float ref = dc.scalar_dc->query_to_code(code.data());
const float tst = dc.rvv_dc->query_to_code(code.data());
// f32 accumulation drift bound: ~n*ulp(intermediate), ~2e-2 relative
// here (vs the >=100% error a 2^31 bias overflow would produce).
const double tol = std::fabs(expected_exact) * 2e-2;
EXPECT_NEAR(tst, expected_exact, tol)
<< "signed-IP bias overflow at d=" << d << " rvv=" << tst;
EXPECT_NEAR(ref, tst, tol) << "signed-IP parity at d=" << d
<< " rvv=" << tst << " scalar=" << ref;
}
// ===========================================================================
// D10 — Direct codecs retain FLOAT-query semantics. The review's literal
// counterexample: d=1, q=0.75, codes {0, 1} — the scalar L2
// distances are 0.5625 / 0.0625 so code 1 must win. An earlier
// integer-domain RVV kernel truncated the query to 0, flipping the
// ranking. Fractional queries must match the scalar NONE reference
// for all four direct codec x metric combinations.
// ===========================================================================
static void run_float_query_case(
SQ::QuantizerType qtype,
faiss::MetricType metric,
const char* name) {
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t d = 1;
std::vector<float> trained;
DcPair dc = make_dc_pair(qtype, metric, d, trained, nullptr, d);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
// ---- The review's literal counterexample ----
// codes 0 and 1 (storage byte = value for direct; value + 128 for
// signed direct). q = 0.75 must sit strictly closer to code 1.
{
uint8_t bytes[2];
float r[2], t[2];
if (qtype == SQ::QT_8bit_direct_signed) {
bytes[0] = 0 + 128; // value 0
bytes[1] = 1 + 128; // value 1
} else {
bytes[0] = 0;
bytes[1] = 1;
}
std::vector<float> q = {0.75f};
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
r[0] = dc.scalar_dc->query_to_code(&bytes[0]);
r[1] = dc.scalar_dc->query_to_code(&bytes[1]);
t[0] = dc.rvv_dc->query_to_code(&bytes[0]);
t[1] = dc.rvv_dc->query_to_code(&bytes[1]);
// Scalar L2 reference: 0.5625 and 0.0625.
if (metric == METRIC_L2) {
EXPECT_NEAR(r[0], 0.5625f, 1e-6f)
<< name << " scalar d(q=0.75, code0)";
EXPECT_NEAR(r[1], 0.0625f, 1e-6f)
<< name << " scalar d(q=0.75, code1)";
}
EXPECT_NEAR(t[0], r[0], 1e-5f)
<< name << " RVV d(q=0.75, code0)=" << t[0]
<< " scalar=" << r[0] << " (query truncated?)";
EXPECT_NEAR(t[1], r[1], 1e-5f)
<< name << " RVV d(q=0.75, code1)=" << t[1]
<< " scalar=" << r[1] << " (query truncated?)";
// The ranking must match the scalar reference: code 1 wins under
// L2; under IP, code 1 has the larger score.
if (metric == METRIC_L2) {
EXPECT_LT(t[1], t[0]) << name
<< " ranking flipped for q=0.75 (truncated "
"query picked code 0)";
} else {
EXPECT_GT(t[1], t[0]) << name << " ranking flipped for q=0.75";
}
}
// ---- Random fractional queries vs both candidate codes ----
{
std::mt19937 rng(7);
std::uniform_real_distribution<float> frac(-10.0f, 265.0f);
for (int trial = 0; trial < 64; trial++) {
std::vector<float> q = {frac(rng)};
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
for (int c = 0; c <= 255; c += 1) {
uint8_t byte;
float value;
if (qtype == SQ::QT_8bit_direct_signed) {
byte = uint8_t(c + 128);
value = float(c - 128);
} else {
byte = uint8_t(c);
value = float(c);
}
float ref = dc.scalar_dc->query_to_code(&byte);
float tst = dc.rvv_dc->query_to_code(&byte);
ASSERT_TRUE(std::isfinite(ref) && std::isfinite(tst))
<< name << " non-finite at q=" << q[0] << " code=" << c;
EXPECT_NEAR(tst, ref, 1e-3f + std::abs(ref) * 1e-4f)
<< name << " q=" << q[0] << " code value=" << value;
}
}
}
}
TEST(D10FloatQuery, DirectL2Counterexample) {
run_float_query_case(SQ::QT_8bit_direct, METRIC_L2, "8bit_direct_L2");
}
TEST(D10FloatQuery, DirectIPFractionalParity) {
run_float_query_case(
SQ::QT_8bit_direct, METRIC_INNER_PRODUCT, "8bit_direct_IP");
}
TEST(D10FloatQuery, DirectSignedL2Counterexample) {
run_float_query_case(
SQ::QT_8bit_direct_signed, METRIC_L2, "8bit_direct_signed_L2");
}
TEST(D10FloatQuery, DirectSignedIPFractionalParity) {
run_float_query_case(
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
"8bit_direct_signed_IP");
}
// ===========================================================================
// D6 — d == 0 must return 0 without hanging. Kernels guard vsetvl with
// `d > 0 ? d : 1` and the tail with `remaining > 0`, so d == 0 must
// terminate immediately. Each case runs in a forked child under
// alarm() so a hang only fails its own case (POSIX only).
// ===========================================================================
struct ZeroDimParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
const char* name;
};
class SQRVVZeroDim : public ::testing::TestWithParam<ZeroDimParams> {};
#ifndef _WIN32
TEST_P(SQRVVZeroDim, ReturnsZeroNoHang) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
// Minimal trained vector per codec family (d=0 loops never read it,
// but the uniform constructors index [0] and [1]).
std::vector<float> trained;
if (class_of(p.qtype) == QClass::UniformApprox) {
trained = {0.0f, 1.0f};
}
const size_t d = 0;
pid_t pid = fork();
ASSERT_GE(pid, 0) << "fork failed";
if (pid == 0) {
// child: run the kernel under a hard deadline; _exit a status code
alarm(20);
DcPair dc = [&] {
DcPair dp;
dp.scalar_dc.reset(
faiss::scalar_quantizer::sq_select_distance_computer<
faiss::SIMDLevel::NONE>(
p.metric, p.qtype, d, trained));
{
ScopedSIMDLevel _(faiss::SIMDLevel::RISCV_RVV);
dp.rvv_dc.reset(
faiss::scalar_quantizer::sq_select_distance_computer<
faiss::SIMDLevel::RISCV_RVV>(
p.metric, p.qtype, d, trained));
}
return dp;
}();
if (!dc.scalar_dc || !dc.rvv_dc)
_exit(3);
float q_dummy = 0.0f;
dc.scalar_dc->set_query(&q_dummy);
dc.rvv_dc->set_query(&q_dummy);
uint8_t code_dummy = 0;
float ref = dc.scalar_dc->query_to_code(&code_dummy);
float tst = dc.rvv_dc->query_to_code(&code_dummy);
if (!std::isfinite(tst) || !std::isfinite(ref))
_exit(4);
if (tst != 0.0f)
_exit(1); // must be exactly 0 for d=0
if (std::abs(ref - tst) > 1e-6f)
_exit(2); // parity broken
_exit(0);
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status) && WTERMSIG(status) == SIGALRM) {
FAIL() << "qtype=" << int(p.qtype) << " " << metric_name(p.metric)
<< ": d=0 kernel HUNG (vsetvl(0)==0 loop) — killed after 20s";
} else if (WIFEXITED(status)) {
int rc = WEXITSTATUS(status);
switch (rc) {
case 0:
GTEST_SUCCEED();
break;
case 1:
FAIL() << "d=0 RVV distance != 0 (qtype=" << int(p.qtype) << " "
<< metric_name(p.metric) << ")";
break;
case 2:
FAIL() << "d=0 RVV != scalar parity (qtype=" << int(p.qtype)
<< " " << metric_name(p.metric) << ")";
break;
case 3:
FAIL() << "distance computer is null (qtype=" << int(p.qtype)
<< " " << metric_name(p.metric) << ")";
break;
case 4:
FAIL() << "d=0 non-finite result (qtype=" << int(p.qtype) << " "
<< metric_name(p.metric) << ")";
break;
default:
FAIL() << "child exit " << rc;
}
} else {
FAIL() << "child abnormal exit";
}
}
#endif // !_WIN32
#ifdef _WIN32
TEST_P(SQRVVZeroDim, ReturnsZeroNoHang) {
GTEST_SKIP() << "fork-based hang isolation requires POSIX";
}
#endif
INSTANTIATE_TEST_SUITE_P(
D6,
SQRVVZeroDim,
::testing::Values(
ZeroDimParams{SQ::QT_4bit, METRIC_L2, "4bit_L2"},
ZeroDimParams{SQ::QT_4bit, METRIC_INNER_PRODUCT, "4bit_IP"},
ZeroDimParams{
SQ::QT_4bit_uniform,
METRIC_L2,
"4bit_uniform_L2"},
ZeroDimParams{
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
"4bit_uniform_IP"},
ZeroDimParams{SQ::QT_6bit, METRIC_L2, "6bit_L2"},
ZeroDimParams{SQ::QT_6bit, METRIC_INNER_PRODUCT, "6bit_IP"},
ZeroDimParams{SQ::QT_8bit, METRIC_L2, "8bit_L2"},
ZeroDimParams{SQ::QT_8bit, METRIC_INNER_PRODUCT, "8bit_IP"},
ZeroDimParams{
SQ::QT_8bit_uniform,
METRIC_L2,
"8bit_uniform_L2"},
ZeroDimParams{
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
"8bit_uniform_IP"},
ZeroDimParams{SQ::QT_8bit_direct, METRIC_L2, "8bit_direct_L2"},
ZeroDimParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
"8bit_direct_IP"},
ZeroDimParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
"8bit_direct_signed_L2"},
ZeroDimParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
"8bit_direct_signed_IP"},
ZeroDimParams{SQ::QT_bf16, METRIC_L2, "bf16_L2"},
ZeroDimParams{SQ::QT_bf16, METRIC_INNER_PRODUCT, "bf16_IP"},
ZeroDimParams{SQ::QT_fp16, METRIC_L2, "fp16_L2"},
ZeroDimParams{SQ::QT_fp16, METRIC_INNER_PRODUCT, "fp16_IP"}),
[](const ::testing::TestParamInfo<ZeroDimParams>& info) {
return std::string(info.param.name);
});
// ===========================================================================
// D7 — Special queries.
// FloatDomain + UniformApprox: exact float-domain kernels — clamp
// boundaries, far-out-of-range, and huge-but-finite magnitudes must
// stay in parity with the scalar reference (1e-4 rel).
// Direct: integer-grid in-contract queries — exact.
// ===========================================================================
struct SpecialQParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
size_t d;
float tol;
const char* name;
};
class SQRVVSpecialQueries : public ::testing::TestWithParam<SpecialQParams> {};
TEST_P(SQRVVSpecialQueries, EdgeQueriesParity) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t n_train = 64;
const size_t n_db = 4;
const QClass qclass = class_of(p.qtype);
const bool direct = (qclass == QClass::Direct);
const int dlo = (p.qtype == SQ::QT_8bit_direct_signed) ? -128 : 0;
const int dhi = (p.qtype == SQ::QT_8bit_direct_signed) ? 127 : 255;
std::vector<float> xt = direct
? random_int_floats(n_train, p.d, dlo, dhi, 42)
: random_int_floats(n_train, p.d, -40, 200, 42);
std::vector<float> xb =
direct ? xt : random_int_floats(n_db, p.d, -40, 200, 43);
SQ sq(p.d, p.qtype);
sq.train(n_train, xt.data());
std::vector<uint8_t> codes(sq.code_size * n_db, 0);
sq.compute_codes(xb.data(), codes.data(), n_db);
DcPair dc = make_dc_pair(
p.qtype, p.metric, p.d, sq.trained, codes.data(), sq.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
float vmin = 0, vdiff = 0;
bool has_range = false;
trained_range(p.qtype, p.d, sq.trained, vmin, vdiff, has_range);
// Query set per class
std::vector<std::vector<float>> queries;
queries.emplace_back(p.d, 0.0f); // all-zero
if (direct) {
queries.emplace_back(p.d, float(dlo));
queries.emplace_back(p.d, float(dhi));
queries.emplace_back(p.d, float(dlo) + 1.0f);
queries.emplace_back(p.d, float(dhi) - 1.0f);
{
std::vector<float> q(p.d);
for (size_t i = 0; i < p.d; i++)
q[i] = (i % 2 == 0) ? float(dlo) : float(dhi);
queries.push_back(std::move(q));
}
} else if (qclass == QClass::UniformApprox) {
// Uniform codecs are now float-domain (exact affine), so any query
// stays in parity; in-range systematic values are a conservative
// subset that still exercises the affine reconstruction path.
if (has_range) {
queries.emplace_back(p.d, vmin);
queries.emplace_back(p.d, vmin + vdiff);
queries.emplace_back(p.d, vmin + 0.5f * vdiff);
{
std::vector<float> q(p.d);
for (size_t i = 0; i < p.d; i++)
q[i] = (i % 2 == 0) ? vmin : vmin + vdiff;
queries.push_back(std::move(q));
}
}
} else {
// FloatDomain: exact kernels — full-range + far-out + huge finite
queries.emplace_back(p.d, 1.0f);
queries.emplace_back(p.d, -1.0f);
queries.emplace_back(p.d, 1e4f);
queries.emplace_back(p.d, -1e4f);
queries.emplace_back(p.d, 1e18f); // huge but finite in f32
if (has_range) {
queries.emplace_back(p.d, vmin);
queries.emplace_back(p.d, vmin + vdiff);
queries.emplace_back(p.d, vmin - 1000.0f);
queries.emplace_back(p.d, vmin + vdiff + 1000.0f);
}
}
// Relative tolerance: 1e-4 for all float-domain / exact kernels
// (uniform L2/IP are now float-domain, same precision class as the
// non-uniform codecs).
const float rel_tol = 1e-4f;
for (size_t qi = 0; qi < queries.size(); qi++) {
SCOPED_TRACE(
::testing::Message()
<< p.name << " query=" << qi << " d=" << p.d);
dc.scalar_dc->set_query(queries[qi].data());
dc.rvv_dc->set_query(queries[qi].data());
for (size_t i = 0; i < n_db; i++) {
const uint8_t* code = codes.data() + i * sq.code_size;
float ref = dc.scalar_dc->query_to_code(code);
float tst = dc.rvv_dc->query_to_code(code);
ASSERT_TRUE(std::isfinite(ref)) << "scalar non-finite";
ASSERT_TRUE(std::isfinite(tst))
<< "qtype=" << int(p.qtype) << " non-finite";
EXPECT_NEAR(ref, tst, p.tol + std::abs(ref) * rel_tol);
}
}
}
/*
* d=17 crosses VLMAX for the e8m1 kernels AND exercises tails, keeping
* runtime modest across all 9 qtypes x 2 metrics.
*/
INSTANTIATE_TEST_SUITE_P(
D7,
SQRVVSpecialQueries,
::testing::Values(
SpecialQParams{SQ::QT_4bit, METRIC_L2, 17, 1e-3f, "4bit"},
SpecialQParams{
SQ::QT_4bit,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"4bit_IP"},
SpecialQParams{
SQ::QT_4bit_uniform,
METRIC_L2,
17,
1e-3f,
"4bit_uniform"},
SpecialQParams{
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"4bit_uniform_IP"},
SpecialQParams{SQ::QT_6bit, METRIC_L2, 17, 1e-3f, "6bit"},
SpecialQParams{
SQ::QT_6bit,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"6bit_IP"},
SpecialQParams{SQ::QT_8bit, METRIC_L2, 17, 1e-3f, "8bit"},
SpecialQParams{
SQ::QT_8bit,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"8bit_IP"},
SpecialQParams{
SQ::QT_8bit_uniform,
METRIC_L2,
17,
1e-3f,
"8bit_uniform"},
SpecialQParams{
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"8bit_uniform_IP"},
SpecialQParams{
SQ::QT_8bit_direct,
METRIC_L2,
17,
1e-3f,
"8bit_direct"},
SpecialQParams{
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"8bit_direct_IP"},
SpecialQParams{
SQ::QT_8bit_direct_signed,
METRIC_L2,
17,
1e-3f,
"8bit_direct_signed"},
SpecialQParams{
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"8bit_direct_signed_IP"},
SpecialQParams{SQ::QT_bf16, METRIC_L2, 17, 1e-3f, "bf16"},
SpecialQParams{
SQ::QT_bf16,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"bf16_IP"},
SpecialQParams{SQ::QT_fp16, METRIC_L2, 17, 1e-3f, "fp16"},
SpecialQParams{
SQ::QT_fp16,
METRIC_INNER_PRODUCT,
17,
1e-3f,
"fp16_IP"}));
// ===========================================================================
// D8 — Very small / odd dimensions. The review asks for "odd and very small
// dimensions" in addition to VLMAX boundaries. d < 8 exercises the
// packing tail: 4-bit odd-d padding nibble, 6-bit ng==0 (d<4) pure
// scalar tail vs ng>=1 with a d%4 scalar tail, and the single-lane /
// tail-only vector path for every codec.
// ===========================================================================
struct SmallDimParams {
SQ::QuantizerType qtype;
faiss::MetricType metric;
size_t d;
const char* name;
};
class SQRVVSmallDim : public ::testing::TestWithParam<SmallDimParams> {};
TEST_P(SQRVVSmallDim, SmallDimensionParity) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t n_train = 64;
const size_t n_db = 4;
const bool direct = is_direct(p.qtype);
const int dlo = (p.qtype == SQ::QT_8bit_direct_signed) ? -128 : 0;
const int dhi = (p.qtype == SQ::QT_8bit_direct_signed) ? 127 : 255;
std::vector<float> xb = direct
? random_int_floats(n_train, p.d, dlo, dhi, 42)
: make_random_vectors(n_train, p.d, 42);
std::vector<float> xq = direct ? random_int_floats(1, p.d, dlo, dhi, 99)
: make_random_vectors(1, p.d, 99);
if (p.metric == METRIC_INNER_PRODUCT && !direct) {
faiss::fvec_renorm_L2(p.d, n_train, xb.data());
faiss::fvec_renorm_L2(p.d, 1, xq.data());
}
SQ sq(p.d, p.qtype);
sq.train(n_train, xb.data());
std::vector<uint8_t> codes(sq.code_size * n_db, 0);
sq.compute_codes(xb.data(), codes.data(), n_db);
DcPair dc = make_dc_pair(
p.qtype, p.metric, p.d, sq.trained, codes.data(), sq.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
dc.scalar_dc->set_query(xq.data());
dc.rvv_dc->set_query(xq.data());
SCOPED_TRACE(
::testing::Message()
<< p.name << " d=" << p.d << " " << metric_name(p.metric));
// All kernels are float-domain after the review fix -> reassociation
// precision only; direct codecs are additionally integer-exact here.
const float tol = 1e-3f;
for (size_t i = 0; i < n_db; i++) {
const uint8_t* code = codes.data() + i * sq.code_size;
float ref = dc.scalar_dc->query_to_code(code);
float tst = dc.rvv_dc->query_to_code(code);
ASSERT_TRUE(std::isfinite(ref) && std::isfinite(tst)) << p.name;
EXPECT_NEAR(ref, tst, tol + std::abs(ref) * 1e-4f)
<< p.name << " query_to_code code=" << i;
}
{
std::vector<uint8_t> zero_code(sq.code_size, 0);
float ref = dc.scalar_dc->query_to_code(zero_code.data());
float tst = dc.rvv_dc->query_to_code(zero_code.data());
EXPECT_NEAR(ref, tst, tol + std::abs(ref) * 1e-4f)
<< p.name << " zero code";
}
for (size_t i = 0; i + 1 < n_db; i += 2) {
float ref = dc.scalar_dc->symmetric_dis(i, i + 1);
float tst = dc.rvv_dc->symmetric_dis(i, i + 1);
EXPECT_NEAR(ref, tst, tol + std::abs(ref) * 1e-4f)
<< p.name << " symmetric_dis(" << i << "," << (i + 1) << ")";
}
{
const uint8_t *c0 = codes.data(), *c1 = codes.data() + sq.code_size,
*c2 = codes.data() + 2 * sq.code_size,
*c3 = codes.data() + 3 * sq.code_size;
float r[4], t[4];
dc.scalar_dc->query_to_codes_batch_4(
c0, c1, c2, c3, r[0], r[1], r[2], r[3]);
dc.rvv_dc->query_to_codes_batch_4(
c0, c1, c2, c3, t[0], t[1], t[2], t[3]);
for (int j = 0; j < 4; j++)
EXPECT_NEAR(r[j], t[j], tol + std::abs(r[j]) * 1e-4f)
<< p.name << " batch_4[" << j << "]";
}
}
// 9 qtypes x {L2, IP} x {1, 2, 3, 5, 7}
#define SMALL_DIM_CASES(QT, METRIC, TAG) \
SmallDimParams{QT, METRIC, 1, TAG "_d1"}, \
SmallDimParams{QT, METRIC, 2, TAG "_d2"}, \
SmallDimParams{QT, METRIC, 3, TAG "_d3"}, \
SmallDimParams{QT, METRIC, 5, TAG "_d5"}, \
SmallDimParams{QT, METRIC, 7, TAG "_d7"}
INSTANTIATE_TEST_SUITE_P(
D8,
SQRVVSmallDim,
::testing::Values(
SMALL_DIM_CASES(SQ::QT_4bit, METRIC_L2, "4bit_L2"),
SMALL_DIM_CASES(SQ::QT_4bit, METRIC_INNER_PRODUCT, "4bit_IP"),
SMALL_DIM_CASES(
SQ::QT_4bit_uniform,
METRIC_L2,
"4bit_uniform_L2"),
SMALL_DIM_CASES(
SQ::QT_4bit_uniform,
METRIC_INNER_PRODUCT,
"4bit_uniform_IP"),
SMALL_DIM_CASES(SQ::QT_6bit, METRIC_L2, "6bit_L2"),
SMALL_DIM_CASES(SQ::QT_6bit, METRIC_INNER_PRODUCT, "6bit_IP"),
SMALL_DIM_CASES(SQ::QT_8bit, METRIC_L2, "8bit_L2"),
SMALL_DIM_CASES(SQ::QT_8bit, METRIC_INNER_PRODUCT, "8bit_IP"),
SMALL_DIM_CASES(
SQ::QT_8bit_uniform,
METRIC_L2,
"8bit_uniform_L2"),
SMALL_DIM_CASES(
SQ::QT_8bit_uniform,
METRIC_INNER_PRODUCT,
"8bit_uniform_IP"),
SMALL_DIM_CASES(
SQ::QT_8bit_direct,
METRIC_L2,
"8bit_direct_L2"),
SMALL_DIM_CASES(
SQ::QT_8bit_direct,
METRIC_INNER_PRODUCT,
"8bit_direct_IP"),
SMALL_DIM_CASES(
SQ::QT_8bit_direct_signed,
METRIC_L2,
"8bit_direct_signed_L2"),
SMALL_DIM_CASES(
SQ::QT_8bit_direct_signed,
METRIC_INNER_PRODUCT,
"8bit_direct_signed_IP"),
SMALL_DIM_CASES(SQ::QT_bf16, METRIC_L2, "bf16_L2"),
SMALL_DIM_CASES(SQ::QT_bf16, METRIC_INNER_PRODUCT, "bf16_IP"),
SMALL_DIM_CASES(SQ::QT_fp16, METRIC_L2, "fp16_L2"),
SMALL_DIM_CASES(SQ::QT_fp16, METRIC_INNER_PRODUCT, "fp16_IP")),
[](const ::testing::TestParamInfo<SmallDimParams>& info) {
return std::string(info.param.name);
});
// ===========================================================================
// D9 — q == recon(code). For every qtype under L2, decode a code back to its
// reconstructed vector and use that as the query; the L2 distance to
// that same code must be ~0 (the code reconstructs exactly to the
// query). This pins the general semantic baseline the review asks for
// ("queries equal to reconstructed code values"), beyond the uniform-L2
// counterexample already covered by D1.
// ===========================================================================
struct ReconParams {
SQ::QuantizerType qtype;
size_t d;
const char* name;
};
class SQRVVReconQuery : public ::testing::TestWithParam<ReconParams> {};
TEST_P(SQRVVReconQuery, QueryEqualsReconstructedValue) {
const auto& p = GetParam();
if (!faiss::SIMDConfig::is_simd_level_available(
faiss::SIMDLevel::RISCV_RVV)) {
GTEST_SKIP() << "RISCV_RVV not available";
}
const size_t n_train = 64;
const bool direct = is_direct(p.qtype);
const int dlo = (p.qtype == SQ::QT_8bit_direct_signed) ? -128 : 0;
const int dhi = (p.qtype == SQ::QT_8bit_direct_signed) ? 127 : 255;
std::vector<float> xb = direct
? random_int_floats(n_train, p.d, dlo, dhi, 42)
: make_random_vectors(n_train, p.d, 42);
SQ sq(p.d, p.qtype);
sq.train(n_train, xb.data());
// Encode one in-contract vector, then decode it back to recon(code).
std::vector<float> xsrc = direct ? random_int_floats(1, p.d, dlo, dhi, 7)
: make_random_vectors(1, p.d, 7);
std::vector<uint8_t> code(sq.code_size);
sq.compute_codes(xsrc.data(), code.data(), 1);
std::vector<float> q(p.d);
sq.decode(code.data(), q.data(), 1); // q == recon(code)
DcPair dc = make_dc_pair(
p.qtype, METRIC_L2, p.d, sq.trained, code.data(), sq.code_size);
ASSERT_TRUE(dc.scalar_dc && dc.rvv_dc);
dc.scalar_dc->set_query(q.data());
dc.rvv_dc->set_query(q.data());
float ref = dc.scalar_dc->query_to_code(code.data());
float tst = dc.rvv_dc->query_to_code(code.data());
ASSERT_TRUE(std::isfinite(ref) && std::isfinite(tst)) << p.name;
EXPECT_NEAR(ref, 0.0f, 1e-3f)
<< p.name << " scalar reference nonzero for own recon";
EXPECT_NEAR(tst, 0.0f, 1e-3f)
<< p.name << " RVV distance to own recon = " << tst;
}
INSTANTIATE_TEST_SUITE_P(
D9,
SQRVVReconQuery,
::testing::Values(
ReconParams{SQ::QT_4bit, 8, "4bit_d8"},
ReconParams{SQ::QT_4bit, 17, "4bit_d17"},
ReconParams{SQ::QT_4bit_uniform, 8, "4bit_uniform_d8"},
ReconParams{SQ::QT_4bit_uniform, 17, "4bit_uniform_d17"},
ReconParams{SQ::QT_6bit, 8, "6bit_d8"},
ReconParams{SQ::QT_6bit, 17, "6bit_d17"},
ReconParams{SQ::QT_8bit, 8, "8bit_d8"},
ReconParams{SQ::QT_8bit, 17, "8bit_d17"},
ReconParams{SQ::QT_8bit_uniform, 8, "8bit_uniform_d8"},
ReconParams{SQ::QT_8bit_uniform, 17, "8bit_uniform_d17"},
ReconParams{SQ::QT_8bit_direct, 8, "8bit_direct_d8"},
ReconParams{SQ::QT_8bit_direct, 17, "8bit_direct_d17"},
ReconParams{
SQ::QT_8bit_direct_signed,
8,
"8bit_direct_signed_d8"},
ReconParams{
SQ::QT_8bit_direct_signed,
17,
"8bit_direct_signed_d17"},
ReconParams{SQ::QT_bf16, 8, "bf16_d8"},
ReconParams{SQ::QT_bf16, 17, "bf16_d17"},
ReconParams{SQ::QT_fp16, 8, "fp16_d8"},
ReconParams{SQ::QT_fp16, 17, "fp16_d17"}),
[](const ::testing::TestParamInfo<ReconParams>& info) {
return std::string(info.param.name);
});
} // namespace