mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-11 22:50:00 +00:00
Convert approx_topk and residual_quantizer_encode_steps to SIMDLevel dynamic dispatch (#4923)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4923 Convert HeapWithBuckets and the accum_and_*_tab functions from compile-time #ifdef __AVX2__ / __aarch64__ guards to SIMDLevel-templatized dynamic dispatch, consistent with the rest of FAISS's DD infrastructure (with_simd_level, simdlib, per-ISA .cpp files). Migration plan: P2234192040 Key changes: - New approx_topk.h: merges mode.h + heap_with_buckets.h. HeapWithBuckets<C,NB,N> dispatches via with_simd_level_256bit; SIMDLevel::NONE uses generic.h scalar fallback. SIMD primary template declares bs_addn without defining it. - New simdlib256-inl.h: out-of-line definition of HeapWithBucketsCMaxFloat::bs_addn using simdlib types (simd8float32_tpl/simd8uint32_tpl). Only included by per-ISA .cpp files. - New rq_beam_search_tab.h / rq_beam_search_tab-inl.h: declarations and definitions of accum_and_store/add/finalize_tab templatized on SIMDLevel, extracted from residual_quantizer_encode_steps.cpp. - New avx2.cpp / neon.cpp: explicit template instantiations per ISA. - residual_quantizer_encode_steps.cpp: HANDLE_APPROX / ACCUM_AND_*_TAB macros replaced with templatized lambdas + with_simd_level_256bit dispatch. Accumulation logic decomposed into compute_cent_distances_baseline and compute_cent_distances_simd<SL>. - Deleted: avx2-inl.h, mode.h, heap_with_buckets.h (merged into approx_topk.h). - Moved: approx_topk directory from utils/ to impl/. Reviewed By: algoriddle Differential Revision: D96360821 fbshipit-source-id: a1facb34b8af065e0a1430897cc05fbe763d3e26
This commit is contained in:
committed by
meta-codesync[bot]
parent
58a57e6bca
commit
b1feeb79f8
@@ -12,6 +12,7 @@ set(FAISS_SIMD_AVX2_SRC
|
||||
impl/fast_scan/impl-avx2.cpp
|
||||
impl/pq_code_distance/pq_code_distance-avx2.cpp
|
||||
impl/scalar_quantizer/sq-avx2.cpp
|
||||
impl/approx_topk/avx2.cpp
|
||||
utils/simd_impl/distances_avx2.cpp
|
||||
)
|
||||
set(FAISS_SIMD_AVX512_SRC
|
||||
@@ -23,6 +24,7 @@ set(FAISS_SIMD_AVX512_SRC
|
||||
set(FAISS_SIMD_NEON_SRC
|
||||
impl/fast_scan/impl-neon.cpp
|
||||
impl/scalar_quantizer/sq-neon.cpp
|
||||
impl/approx_topk/neon.cpp
|
||||
utils/simd_impl/distances_aarch64.cpp
|
||||
)
|
||||
set(FAISS_SIMD_SVE_SRC
|
||||
@@ -315,10 +317,11 @@ set(FAISS_HEADERS
|
||||
utils/distances_fused/avx512.h
|
||||
utils/distances_fused/distances_fused.h
|
||||
utils/distances_fused/simdlib_based.h
|
||||
utils/approx_topk/approx_topk.h
|
||||
utils/approx_topk/avx2-inl.h
|
||||
utils/approx_topk/generic.h
|
||||
utils/approx_topk/mode.h
|
||||
impl/approx_topk/approx_topk.h
|
||||
impl/approx_topk/generic.h
|
||||
impl/approx_topk/rq_beam_search_tab.h
|
||||
impl/approx_topk/rq_beam_search_tab-inl.h
|
||||
impl/approx_topk/simdlib256-inl.h
|
||||
utils/approx_topk_hamming/approx_topk_hamming.h
|
||||
utils/transpose/transpose-avx2-inl.h
|
||||
utils/transpose/transpose-avx512-inl.h
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
#include <faiss/IndexBinary.h>
|
||||
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/maybe_owned_vector.h>
|
||||
#include <faiss/utils/approx_topk/mode.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <faiss/utils/distances.h>
|
||||
#include <faiss/utils/utils.h>
|
||||
|
||||
#include <faiss/utils/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
// this is needed for prefetching
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include <faiss/Clustering.h>
|
||||
#include <faiss/impl/AdditiveQuantizer.h>
|
||||
|
||||
#include <faiss/utils/approx_topk/mode.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Approximate top-k search using bucketed heaps with SIMD acceleration.
|
||||
//
|
||||
// HeapWithBuckets<C, NBUCKETS, N> splits n elements into NBUCKETS buckets,
|
||||
// tracks the top-N per bucket using SIMD, then merges into a regular heap.
|
||||
// This trades a small accuracy loss for significant speedup.
|
||||
//
|
||||
// The core idea is the following.
|
||||
// Say we need to find beam_size indices with the minimal distance
|
||||
// values. It is done via heap (priority_queue) using the following
|
||||
// pseudocode:
|
||||
//
|
||||
// def baseline():
|
||||
// distances = np.empty([beam_size * n], dtype=float)
|
||||
// indices = np.empty([beam_size * n], dtype=int)
|
||||
//
|
||||
// heap = Heap(max_heap_size=beam_size)
|
||||
//
|
||||
// for i in range(0, beam_size * n):
|
||||
// heap.push(distances[i], indices[i])
|
||||
//
|
||||
// Basically, this is what heap_addn() function from utils/Heap.h does.
|
||||
//
|
||||
// The following scheme can be used for approximate beam search.
|
||||
// Say, we need to find elements with min distance.
|
||||
// Basically, we split n elements of every beam into NBUCKETS buckets
|
||||
// and track the index with the minimal distance for every bucket.
|
||||
// This can be effectively SIMD-ed and significantly lowers the number
|
||||
// of operations, but yields approximate results for beam_size >= 2.
|
||||
//
|
||||
// def approximate_v1():
|
||||
// distances = np.empty([beam_size * n], dtype=float)
|
||||
// indices = np.empty([beam_size * n], dtype=int)
|
||||
//
|
||||
// heap = Heap(max_heap_size=beam_size)
|
||||
//
|
||||
// for beam in range(0, beam_size):
|
||||
// # The value of 32 is just an example.
|
||||
// # The value may be varied: the larger the value is,
|
||||
// # the slower and the more precise vs baseline beam search is
|
||||
// NBUCKETS = 32
|
||||
//
|
||||
// local_min_distances = [HUGE_VALF] * NBUCKETS
|
||||
// local_min_indices = [0] * NBUCKETS
|
||||
//
|
||||
// for i in range(0, n / NBUCKETS):
|
||||
// for j in range(0, NBUCKETS):
|
||||
// idx = beam * n + i * NBUCKETS + j
|
||||
// if distances[idx] < local_min_distances[j]:
|
||||
// local_min_distances[j] = distances[idx]
|
||||
// local_min_indices[j] = indices[idx]
|
||||
//
|
||||
// for j in range(0, NBUCKETS):
|
||||
// heap.push(local_min_distances[j], local_min_indices[j])
|
||||
//
|
||||
// The accuracy can be improved by tracking min-2 elements for every
|
||||
// bucket. Such a min-2 implementation with NBUCKETS buckets provides
|
||||
// better accuracy than top-1 implementation with 2 * NBUCKETS buckets.
|
||||
// Min-3 is also doable. One can use min-N approach, but I'm not sure
|
||||
// whether min-4 and above are practical, because of the lack of SIMD
|
||||
// registers (unless AVX-512 version is used).
|
||||
//
|
||||
// C++ template for top-N implementation is provided. The code
|
||||
// assumes that indices[idx] == idx. One can write a code that lifts
|
||||
// such an assumption easily.
|
||||
//
|
||||
// Currently, the code that tracks elements with min distances is implemented
|
||||
// (Max Heap). Min Heap option can be added easily.
|
||||
//
|
||||
// Dispatch:
|
||||
// AVX2 / ARM_NEON → HeapWithBucketsCMaxFloat (simdlib256-inl.h)
|
||||
// NONE (scalar) → HeapWithBucketsGenericCMaxFloat (generic.h)
|
||||
//
|
||||
// The SIMD definitions live in simdlib256-inl.h (only included by per-ISA
|
||||
// .cpp files). Common TUs see only declarations here, so no extern template
|
||||
// suppression is needed.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <faiss/impl/approx_topk/generic.h>
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
#include <faiss/utils/simd_levels.h>
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ApproxTopK_mode_t
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Represents the mode of use of approximate top-k computations
|
||||
/// that allows to trade accuracy vs speed. So, every options
|
||||
/// besides EXACT_TOPK increases the speed.
|
||||
///
|
||||
/// B represents the number of buckets.
|
||||
/// D is the number of min-k elements to track within every bucket.
|
||||
///
|
||||
/// Default option is EXACT_TOPK.
|
||||
/// APPROX_TOPK_BUCKETS_B16_D2 is worth starting from, if you'd like
|
||||
/// to experiment a bit.
|
||||
///
|
||||
/// It seems that only the limited number of combinations are
|
||||
/// meaningful, because of the limited supply of SIMD registers.
|
||||
/// Also, certain combinations, such as B32_D1 and B16_D1, were concluded
|
||||
/// to be not very precise in benchmarks, so they were not introduced.
|
||||
|
||||
enum ApproxTopK_mode_t : int {
|
||||
EXACT_TOPK = 0,
|
||||
APPROX_TOPK_BUCKETS_B32_D2 = 1,
|
||||
APPROX_TOPK_BUCKETS_B8_D3 = 2,
|
||||
APPROX_TOPK_BUCKETS_B16_D2 = 3,
|
||||
APPROX_TOPK_BUCKETS_B8_D2 = 4,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HeapWithBuckets dispatch
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace faiss {
|
||||
|
||||
// Primary template: declared, bs_addn NOT defined here.
|
||||
// The out-of-line definition lives in simdlib256-inl.h, which is only
|
||||
// included by the per-ISA .cpp files (avx2.cpp, neon.cpp).
|
||||
template <uint32_t NBUCKETS, uint32_t N, SIMDLevel SL>
|
||||
struct HeapWithBucketsCMaxFloat {
|
||||
static_assert(
|
||||
(NBUCKETS) > 0 && ((NBUCKETS % 8) == 0),
|
||||
"Number of buckets needs to be 8, 16, 24, ...");
|
||||
|
||||
static void addn(
|
||||
const uint32_t n,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
bs_addn(1, n, distances, k, bh_val, bh_ids);
|
||||
}
|
||||
|
||||
// Declared but not defined — resolved at link time from avx2.cpp/neon.cpp.
|
||||
static void bs_addn(
|
||||
const uint32_t beam_size,
|
||||
const uint32_t n_per_beam,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids);
|
||||
};
|
||||
|
||||
// NONE specialization: delegates to the scalar generic implementation.
|
||||
template <uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBucketsCMaxFloat<NBUCKETS, N, SIMDLevel::NONE> {
|
||||
static void addn(
|
||||
const uint32_t n,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
bs_addn(1, n, distances, k, bh_val, bh_ids);
|
||||
}
|
||||
|
||||
static void bs_addn(
|
||||
const uint32_t beam_size,
|
||||
const uint32_t n_per_beam,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
HeapWithBucketsGenericCMaxFloat<NBUCKETS, N>::bs_addn(
|
||||
beam_size, n_per_beam, distances, k, bh_val, bh_ids);
|
||||
}
|
||||
};
|
||||
|
||||
// Primary template — not implemented for arbitrary comparators.
|
||||
template <typename C, uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets {
|
||||
static_assert(
|
||||
sizeof(C) == 0,
|
||||
"HeapWithBuckets: unsupported comparator type");
|
||||
};
|
||||
|
||||
// Partial specialization for CMax<float, int> that dispatches
|
||||
// via with_simd_level_256bit.
|
||||
template <uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets<CMax<float, int>, NBUCKETS, N> {
|
||||
static void addn(
|
||||
const uint32_t n,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
bs_addn(1, n, distances, k, bh_val, bh_ids);
|
||||
}
|
||||
|
||||
static void bs_addn(
|
||||
const uint32_t beam_size,
|
||||
const uint32_t n_per_beam,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
with_simd_level_256bit([&]<SIMDLevel SL>() {
|
||||
HeapWithBucketsCMaxFloat<NBUCKETS, N, SL>::bs_addn(
|
||||
beam_size, n_per_beam, distances, k, bh_val, bh_ids);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Explicit template instantiations of HeapWithBucketsCMaxFloat and
|
||||
// accum_and_*_tab for SIMDLevel::AVX2.
|
||||
|
||||
#ifdef COMPILE_SIMD_AVX2
|
||||
|
||||
#include <faiss/impl/approx_topk/rq_beam_search_tab-inl.h>
|
||||
#include <faiss/impl/approx_topk/simdlib256-inl.h>
|
||||
#include <faiss/impl/simdlib/simdlib_avx2.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
template struct HeapWithBucketsCMaxFloat<8, 3, SIMDLevel::AVX2>;
|
||||
template struct HeapWithBucketsCMaxFloat<8, 2, SIMDLevel::AVX2>;
|
||||
template struct HeapWithBucketsCMaxFloat<16, 2, SIMDLevel::AVX2>;
|
||||
template struct HeapWithBucketsCMaxFloat<16, 1, SIMDLevel::AVX2>;
|
||||
template struct HeapWithBucketsCMaxFloat<32, 2, SIMDLevel::AVX2>;
|
||||
|
||||
#define INSTANTIATE_ACCUM_TAB(M) \
|
||||
template void accum_and_store_tab<M, 4, SIMDLevel::AVX2>( \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
float* __restrict); \
|
||||
template void accum_and_add_tab<M, 4, SIMDLevel::AVX2>( \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
float* __restrict); \
|
||||
template void accum_and_finalize_tab<M, 4, SIMDLevel::AVX2>( \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const float* __restrict, \
|
||||
float* __restrict);
|
||||
|
||||
INSTANTIATE_ACCUM_TAB(1)
|
||||
INSTANTIATE_ACCUM_TAB(2)
|
||||
INSTANTIATE_ACCUM_TAB(3)
|
||||
INSTANTIATE_ACCUM_TAB(4)
|
||||
INSTANTIATE_ACCUM_TAB(5)
|
||||
INSTANTIATE_ACCUM_TAB(6)
|
||||
INSTANTIATE_ACCUM_TAB(7)
|
||||
INSTANTIATE_ACCUM_TAB(8)
|
||||
|
||||
#undef INSTANTIATE_ACCUM_TAB
|
||||
|
||||
} // namespace faiss
|
||||
|
||||
#endif // COMPILE_SIMD_AVX2
|
||||
@@ -16,16 +16,12 @@
|
||||
|
||||
namespace faiss {
|
||||
|
||||
// This is the implementation of the idea and it is very slow,
|
||||
// because a compiler is unable to vectorize it properly.
|
||||
|
||||
template <typename C, uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets {
|
||||
// this case was not implemented yet.
|
||||
};
|
||||
// Scalar (generic) implementation of HeapWithBuckets.
|
||||
// This is correct but slow because a compiler is unable to
|
||||
// vectorize it properly. Used as the SIMDLevel::NONE fallback.
|
||||
|
||||
template <uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets<CMax<float, int>, NBUCKETS, N> {
|
||||
struct HeapWithBucketsGenericCMaxFloat {
|
||||
static void addn(
|
||||
// number of elements
|
||||
const uint32_t n,
|
||||
@@ -135,4 +131,15 @@ struct HeapWithBuckets<CMax<float, int>, NBUCKETS, N> {
|
||||
}
|
||||
};
|
||||
|
||||
// Legacy name kept for backward compatibility (used when
|
||||
// approx_topk.h is not included).
|
||||
template <typename C, uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBucketsGeneric {
|
||||
// not implemented for arbitrary C
|
||||
};
|
||||
|
||||
template <uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBucketsGeneric<CMax<float, int>, NBUCKETS, N>
|
||||
: HeapWithBucketsGenericCMaxFloat<NBUCKETS, N> {};
|
||||
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Explicit template instantiations of HeapWithBucketsCMaxFloat and
|
||||
// accum_and_*_tab for SIMDLevel::ARM_NEON.
|
||||
|
||||
#ifdef COMPILE_SIMD_ARM_NEON
|
||||
|
||||
#include <faiss/impl/approx_topk/rq_beam_search_tab-inl.h>
|
||||
#include <faiss/impl/approx_topk/simdlib256-inl.h>
|
||||
#include <faiss/impl/simdlib/simdlib_neon.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
template struct HeapWithBucketsCMaxFloat<8, 3, SIMDLevel::ARM_NEON>;
|
||||
template struct HeapWithBucketsCMaxFloat<8, 2, SIMDLevel::ARM_NEON>;
|
||||
template struct HeapWithBucketsCMaxFloat<16, 2, SIMDLevel::ARM_NEON>;
|
||||
template struct HeapWithBucketsCMaxFloat<16, 1, SIMDLevel::ARM_NEON>;
|
||||
template struct HeapWithBucketsCMaxFloat<32, 2, SIMDLevel::ARM_NEON>;
|
||||
|
||||
#define INSTANTIATE_ACCUM_TAB(M) \
|
||||
template void accum_and_store_tab<M, 4, SIMDLevel::ARM_NEON>( \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
float* __restrict); \
|
||||
template void accum_and_add_tab<M, 4, SIMDLevel::ARM_NEON>( \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
float* __restrict); \
|
||||
template void accum_and_finalize_tab<M, 4, SIMDLevel::ARM_NEON>( \
|
||||
const float* __restrict, \
|
||||
const uint64_t* __restrict, \
|
||||
const int32_t* __restrict, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
size_t, \
|
||||
const float* __restrict, \
|
||||
const float* __restrict, \
|
||||
float* __restrict);
|
||||
|
||||
INSTANTIATE_ACCUM_TAB(1)
|
||||
INSTANTIATE_ACCUM_TAB(2)
|
||||
INSTANTIATE_ACCUM_TAB(3)
|
||||
INSTANTIATE_ACCUM_TAB(4)
|
||||
INSTANTIATE_ACCUM_TAB(5)
|
||||
INSTANTIATE_ACCUM_TAB(6)
|
||||
INSTANTIATE_ACCUM_TAB(7)
|
||||
INSTANTIATE_ACCUM_TAB(8)
|
||||
|
||||
#undef INSTANTIATE_ACCUM_TAB
|
||||
|
||||
} // namespace faiss
|
||||
|
||||
#endif // COMPILE_SIMD_ARM_NEON
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Definitions of the SIMDLevel-templatized accum_and_*_tab functions.
|
||||
// Only included by per-ISA .cpp files (avx2.cpp, neon.cpp).
|
||||
// Do NOT include this from common translation units.
|
||||
//
|
||||
// Common TUs include rq_beam_search_tab.h (declarations only).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include <faiss/impl/approx_topk/rq_beam_search_tab.h>
|
||||
#include <faiss/impl/simdlib/simdlib.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_store_tab(
|
||||
const size_t m_offset,
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
float* const __restrict output) {
|
||||
using simd_float = simd8float32_tpl<SL>;
|
||||
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * m_offset + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd_float regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] = simd_float(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd_float(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik].storeu(output + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
output[kk] = reg;
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_add_tab(
|
||||
const size_t m_offset,
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
float* const __restrict output) {
|
||||
using simd_float = simd8float32_tpl<SL>;
|
||||
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * m_offset + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd_float regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] = simd_float(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd_float(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
simd_float existing(output + kk + ik * 8);
|
||||
existing += regs[ik];
|
||||
existing.storeu(output + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
output[kk] += reg;
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_finalize_tab(
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
const float* const __restrict distances_i,
|
||||
const float* const __restrict cd_common,
|
||||
float* const __restrict output) {
|
||||
using simd_float = simd8float32_tpl<SL>;
|
||||
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * M + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd_float regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] = simd_float(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd_float(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
simd_float two(2.0f);
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
simd_float common_v(cd_common + kk + ik * 8);
|
||||
common_v = fmadd(two, regs[ik], common_v);
|
||||
common_v += simd_float(distances_i[b]);
|
||||
common_v.storeu(output + b * K + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
output[b * K + kk] = distances_i[b] + cd_common[kk] + 2 * reg;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// @file rq_beam_search_tab.h
|
||||
/// @brief Declarations for SIMDLevel-templatized codebook accumulation
|
||||
/// functions.
|
||||
///
|
||||
/// These functions accumulate codebook cross-norm tables for beam search
|
||||
/// encoding in the Residual Quantizer. They compute the distance
|
||||
/// contributions from previously encoded codebooks using SIMD-accelerated
|
||||
/// register accumulation.
|
||||
///
|
||||
/// Definitions are in rq_beam_search_tab-inl.h (only included by per-ISA
|
||||
/// .cpp files). The common TU only sees these declarations, so no extern
|
||||
/// template suppression is needed — the linker resolves to the explicit
|
||||
/// instantiations in avx2.cpp / neon.cpp.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include <faiss/utils/simd_levels.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
/// Accumulate cross-norms for M codebooks and store the result.
|
||||
///
|
||||
/// Loads M codebook rows (selected by codes_i) and sums them using
|
||||
/// NK×8-wide SIMD chunks, writing the result to output. Used to
|
||||
/// initialize the temporary buffer in the m≥8 path.
|
||||
///
|
||||
/// @tparam M number of codebook rows to accumulate
|
||||
/// @tparam NK number of 8-float SIMD chunks per loop iteration
|
||||
/// @tparam SL SIMD level (AVX2, ARM_NEON, etc.)
|
||||
/// @param m_offset stride between beam entries in codes_i
|
||||
/// @param codebook_cross_norms cross-norm table, shape (total_codes, ldc)
|
||||
/// @param codebook_offsets per-codebook offset into cross-norm table
|
||||
/// @param codes_i code indices for the current query
|
||||
/// @param b beam index
|
||||
/// @param ldc leading dimension of cross-norm table (≥ K)
|
||||
/// @param K number of centroids in the current codebook
|
||||
/// @param output output buffer, size K (overwritten)
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_store_tab(
|
||||
size_t m_offset,
|
||||
const float* __restrict codebook_cross_norms,
|
||||
const uint64_t* __restrict codebook_offsets,
|
||||
const int32_t* __restrict codes_i,
|
||||
size_t b,
|
||||
size_t ldc,
|
||||
size_t K,
|
||||
float* __restrict output);
|
||||
|
||||
/// Accumulate cross-norms for M codebooks and add to existing output.
|
||||
///
|
||||
/// Like accum_and_store_tab, but adds the accumulated result to the
|
||||
/// existing values in output (output[k] += sum). Used for subsequent
|
||||
/// chunks of 8 codebooks in the m≥8 path.
|
||||
///
|
||||
/// @tparam M number of codebook rows to accumulate
|
||||
/// @tparam NK number of 8-float SIMD chunks per loop iteration
|
||||
/// @tparam SL SIMD level (AVX2, ARM_NEON, etc.)
|
||||
/// @param m_offset stride between beam entries in codes_i
|
||||
/// @param codebook_cross_norms cross-norm table
|
||||
/// @param codebook_offsets per-codebook offset
|
||||
/// @param codes_i code indices
|
||||
/// @param b beam index
|
||||
/// @param ldc leading dimension of cross-norm table
|
||||
/// @param K number of centroids
|
||||
/// @param output output buffer, size K (accumulated into)
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_add_tab(
|
||||
size_t m_offset,
|
||||
const float* __restrict codebook_cross_norms,
|
||||
const uint64_t* __restrict codebook_offsets,
|
||||
const int32_t* __restrict codes_i,
|
||||
size_t b,
|
||||
size_t ldc,
|
||||
size_t K,
|
||||
float* __restrict output);
|
||||
|
||||
/// Accumulate cross-norms for M codebooks and finalize distances.
|
||||
///
|
||||
/// Accumulates M codebook rows, then computes the final centroid distance:
|
||||
/// output[b*K + k] = distances_i[b] + cd_common[k] + 2 * sum[k]
|
||||
/// Used for m=1..7 where the entire accumulation fits in registers.
|
||||
///
|
||||
/// @tparam M number of codebook rows to accumulate (equals m)
|
||||
/// @tparam NK number of 8-float SIMD chunks per loop iteration
|
||||
/// @tparam SL SIMD level (AVX2, ARM_NEON, etc.)
|
||||
/// @param codebook_cross_norms cross-norm table
|
||||
/// @param codebook_offsets per-codebook offset
|
||||
/// @param codes_i code indices (stride is M)
|
||||
/// @param b beam index
|
||||
/// @param ldc leading dimension of cross-norm table
|
||||
/// @param K number of centroids
|
||||
/// @param distances_i per-beam input distances, size beam_size
|
||||
/// @param cd_common common distance term, size K
|
||||
/// @param output output centroid distances (b*K offset)
|
||||
template <size_t M, size_t NK, SIMDLevel SL>
|
||||
void accum_and_finalize_tab(
|
||||
const float* __restrict codebook_cross_norms,
|
||||
const uint64_t* __restrict codebook_offsets,
|
||||
const int32_t* __restrict codes_i,
|
||||
size_t b,
|
||||
size_t ldc,
|
||||
size_t K,
|
||||
const float* __restrict distances_i,
|
||||
const float* __restrict cd_common,
|
||||
float* __restrict output);
|
||||
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Out-of-line definition of HeapWithBucketsCMaxFloat::bs_addn using
|
||||
// simdlib types. Only included by per-ISA .cpp files (avx2.cpp, neon.cpp).
|
||||
// Do NOT include this from common translation units.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/simdlib/simdlib.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
#include <faiss/utils/simd_levels.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
// Element-wise max of two simd8float32 vectors, implemented via
|
||||
// cmplt_min_max_fast (which computes both min and max).
|
||||
template <SIMDLevel SL>
|
||||
inline simd8float32_tpl<SL> simd8float32_max(
|
||||
simd8float32_tpl<SL> a,
|
||||
simd8float32_tpl<SL> b) {
|
||||
simd8float32_tpl<SL> min_val, max_val;
|
||||
simd8uint32_tpl<SL> dummy(0u), dmin, dmax;
|
||||
cmplt_min_max_fast(a, dummy, b, dummy, min_val, dmin, max_val, dmax);
|
||||
return max_val;
|
||||
}
|
||||
|
||||
template <uint32_t NBUCKETS, uint32_t N, SIMDLevel SL>
|
||||
void HeapWithBucketsCMaxFloat<NBUCKETS, N, SL>::bs_addn(
|
||||
const uint32_t beam_size,
|
||||
const uint32_t n_per_beam,
|
||||
const float* const __restrict distances,
|
||||
const uint32_t k,
|
||||
float* const __restrict bh_val,
|
||||
int32_t* const __restrict bh_ids) {
|
||||
using C = CMax<float, int>;
|
||||
using simd_float = simd8float32_tpl<SL>;
|
||||
using simd_uint = simd8uint32_tpl<SL>;
|
||||
|
||||
for (uint32_t beam_index = 0; beam_index < beam_size; beam_index++) {
|
||||
simd_float min_distances_i[NBUCKETS / 8][N];
|
||||
simd_uint min_indices_i[NBUCKETS / 8][N];
|
||||
|
||||
for (uint32_t j = 0; j < NBUCKETS / 8; j++) {
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
min_distances_i[j][p] =
|
||||
simd_float(std::numeric_limits<float>::max());
|
||||
min_indices_i[j][p] = simd_uint(0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u);
|
||||
}
|
||||
}
|
||||
|
||||
simd_uint current_indices(0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u);
|
||||
simd_uint indices_delta(NBUCKETS);
|
||||
|
||||
const uint32_t nb = (n_per_beam / NBUCKETS) * NBUCKETS;
|
||||
|
||||
// put the data into buckets
|
||||
for (uint32_t ip = 0; ip < nb; ip += NBUCKETS) {
|
||||
for (uint32_t j = 0; j < NBUCKETS / 8; j++) {
|
||||
const simd_float distances_reg(
|
||||
distances + j * 8 + ip + n_per_beam * beam_index);
|
||||
|
||||
simd_float distance_candidate = distances_reg;
|
||||
simd_uint indices_candidate = current_indices;
|
||||
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
// Use cmplt_min_max_fast for comparison, min values,
|
||||
// min indices, and max indices.
|
||||
simd_float min_d_new, max_d_unused;
|
||||
simd_uint min_idx_new, max_idx_new;
|
||||
cmplt_min_max_fast(
|
||||
distance_candidate,
|
||||
indices_candidate,
|
||||
min_distances_i[j][p],
|
||||
min_indices_i[j][p],
|
||||
min_d_new,
|
||||
min_idx_new,
|
||||
max_d_unused,
|
||||
max_idx_new);
|
||||
|
||||
// The max distance uses distances_reg (the original
|
||||
// input), NOT distance_candidate. This is a deliberate
|
||||
// approximation that breaks the data dependency chain.
|
||||
simd_float max_d_new = simd8float32_max<SL>(
|
||||
min_distances_i[j][p], distances_reg);
|
||||
|
||||
distance_candidate = max_d_new;
|
||||
indices_candidate = max_idx_new;
|
||||
|
||||
min_distances_i[j][p] = min_d_new;
|
||||
min_indices_i[j][p] = min_idx_new;
|
||||
}
|
||||
}
|
||||
|
||||
current_indices = current_indices + indices_delta;
|
||||
}
|
||||
|
||||
// fix the indices
|
||||
for (uint32_t j = 0; j < NBUCKETS / 8; j++) {
|
||||
const simd_uint offset(n_per_beam * beam_index + j * 8);
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
min_indices_i[j][p] = min_indices_i[j][p] + offset;
|
||||
}
|
||||
}
|
||||
|
||||
// merge every bucket into the regular heap
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
for (uint32_t j = 0; j < NBUCKETS / 8; j++) {
|
||||
uint32_t min_indices_scalar[8];
|
||||
float min_distances_scalar[8];
|
||||
|
||||
min_indices_i[j][p].storeu(min_indices_scalar);
|
||||
min_distances_i[j][p].storeu(min_distances_scalar);
|
||||
|
||||
for (size_t j8 = 0; j8 < 8; j8++) {
|
||||
const auto value = min_distances_scalar[j8];
|
||||
const auto index =
|
||||
static_cast<int32_t>(min_indices_scalar[j8]);
|
||||
if (C::cmp2(bh_val[0], value, bh_ids[0], index)) {
|
||||
heap_replace_top<C>(k, bh_val, bh_ids, value, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process leftovers
|
||||
for (uint32_t ip = nb; ip < n_per_beam; ip++) {
|
||||
const int32_t index = ip + n_per_beam * beam_index;
|
||||
const float value = distances[index];
|
||||
|
||||
if (C::cmp(bh_val[0], value)) {
|
||||
heap_replace_top<C>(k, bh_val, bh_ids, value, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace faiss
|
||||
@@ -11,12 +11,12 @@
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/impl/ResidualQuantizer.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
#include <faiss/impl/simdlib/simdlib_dispatch.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
#include <faiss/utils/distances.h>
|
||||
#include <faiss/utils/utils.h>
|
||||
|
||||
#include <faiss/utils/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/approx_topk/rq_beam_search_tab.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
@@ -39,190 +39,6 @@ int sgemm_(
|
||||
|
||||
namespace faiss {
|
||||
|
||||
/********************************************************************
|
||||
* Basic routines
|
||||
********************************************************************/
|
||||
|
||||
namespace {
|
||||
|
||||
template <size_t M, size_t NK>
|
||||
void accum_and_store_tab(
|
||||
const size_t m_offset,
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
float* const __restrict output) {
|
||||
// load pointers into registers
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * m_offset + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
// do accumulation in registers using SIMD.
|
||||
// It is possible that compiler may be smart enough so that
|
||||
// this manual SIMD unrolling might be unneeded.
|
||||
#if defined(__AVX2__) || defined(__aarch64__)
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
// process in chunks of size (8 * NK) floats
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd8float32 regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik].loadu(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd8float32(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
// write the result
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik].storeu(output + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const size_t K8 = 0;
|
||||
#endif
|
||||
|
||||
// process leftovers
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
output[kk] = reg;
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t M, size_t NK>
|
||||
void accum_and_add_tab(
|
||||
const size_t m_offset,
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
float* const __restrict output) {
|
||||
// load pointers into registers
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * m_offset + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
// do accumulation in registers using SIMD.
|
||||
// It is possible that compiler may be smart enough so that
|
||||
// this manual SIMD unrolling might be unneeded.
|
||||
#if defined(__AVX2__) || defined(__aarch64__)
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
// process in chunks of size (8 * NK) floats
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd8float32 regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik].loadu(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd8float32(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
// write the result
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
simd8float32 existing(output + kk + ik * 8);
|
||||
existing += regs[ik];
|
||||
existing.storeu(output + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const size_t K8 = 0;
|
||||
#endif
|
||||
|
||||
// process leftovers
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
output[kk] += reg;
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t M, size_t NK>
|
||||
void accum_and_finalize_tab(
|
||||
const float* const __restrict codebook_cross_norms,
|
||||
const uint64_t* const __restrict codebook_offsets,
|
||||
const int32_t* const __restrict codes_i,
|
||||
const size_t b,
|
||||
const size_t ldc,
|
||||
const size_t K,
|
||||
const float* const __restrict distances_i,
|
||||
const float* const __restrict cd_common,
|
||||
float* const __restrict output) {
|
||||
// load pointers into registers
|
||||
const float* cbs[M];
|
||||
for (size_t ij = 0; ij < M; ij++) {
|
||||
const size_t code = static_cast<size_t>(codes_i[b * M + ij]);
|
||||
cbs[ij] = &codebook_cross_norms[(codebook_offsets[ij] + code) * ldc];
|
||||
}
|
||||
|
||||
// do accumulation in registers using SIMD.
|
||||
// It is possible that compiler may be smart enough so that
|
||||
// this manual SIMD unrolling might be unneeded.
|
||||
#if defined(__AVX2__) || defined(__aarch64__)
|
||||
const size_t K8 = (K / (8 * NK)) * (8 * NK);
|
||||
|
||||
// process in chunks of size (8 * NK) floats
|
||||
for (size_t kk = 0; kk < K8; kk += 8 * NK) {
|
||||
simd8float32 regs[NK];
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik].loadu(cbs[0] + kk + ik * 8);
|
||||
}
|
||||
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
regs[ik] += simd8float32(cbs[ij] + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
|
||||
simd8float32 two(2.0f);
|
||||
for (size_t ik = 0; ik < NK; ik++) {
|
||||
// cent_distances[b * K + k] = distances_i[b] + cd_common[k]
|
||||
// + 2 * dp[k];
|
||||
|
||||
simd8float32 common_v(cd_common + kk + ik * 8);
|
||||
common_v = fmadd(two, regs[ik], common_v);
|
||||
|
||||
common_v += simd8float32(distances_i[b]);
|
||||
common_v.storeu(output + b * K + kk + ik * 8);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const size_t K8 = 0;
|
||||
#endif
|
||||
|
||||
// process leftovers
|
||||
for (size_t kk = K8; kk < K; kk++) {
|
||||
float reg = cbs[0][kk];
|
||||
for (size_t ij = 1; ij < M; ij++) {
|
||||
reg += cbs[ij][kk];
|
||||
}
|
||||
|
||||
output[b * K + kk] = distances_i[b] + cd_common[kk] + 2 * reg;
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
/********************************************************************
|
||||
* Single encoding step
|
||||
********************************************************************/
|
||||
@@ -331,22 +147,28 @@ void beam_search_encode_step(
|
||||
}
|
||||
std::vector<int> perm(new_beam_size, -1);
|
||||
|
||||
#define HANDLE_APPROX(NB, BD) \
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B##NB##_D##BD: \
|
||||
HeapWithBuckets<C, NB, BD>::bs_addn( \
|
||||
beam_size, \
|
||||
K, \
|
||||
cent_distances_i, \
|
||||
new_beam_size, \
|
||||
new_distances_i, \
|
||||
perm.data()); \
|
||||
break;
|
||||
|
||||
auto approx = [&]<uint32_t NB, uint32_t ND>() {
|
||||
HeapWithBuckets<C, NB, ND>::bs_addn(
|
||||
beam_size,
|
||||
K,
|
||||
cent_distances_i,
|
||||
new_beam_size,
|
||||
new_distances_i,
|
||||
perm.data());
|
||||
};
|
||||
switch (approx_topk_mode) {
|
||||
HANDLE_APPROX(8, 3)
|
||||
HANDLE_APPROX(8, 2)
|
||||
HANDLE_APPROX(16, 2)
|
||||
HANDLE_APPROX(32, 2)
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B8_D3:
|
||||
approx.template operator()<8, 3>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B8_D2:
|
||||
approx.template operator()<8, 2>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B16_D2:
|
||||
approx.template operator()<16, 2>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B32_D2:
|
||||
approx.template operator()<32, 2>();
|
||||
break;
|
||||
default:
|
||||
heap_addn<C>(
|
||||
new_beam_size,
|
||||
@@ -358,8 +180,6 @@ void beam_search_encode_step(
|
||||
}
|
||||
heap_reorder<C>(new_beam_size, new_distances_i, perm.data());
|
||||
|
||||
#undef HANDLE_APPROX
|
||||
|
||||
for (int j = 0; j < new_beam_size; j++) {
|
||||
int js = perm[j] / K;
|
||||
int ls = perm[j] % K;
|
||||
@@ -380,6 +200,166 @@ void beam_search_encode_step(
|
||||
}
|
||||
|
||||
// exposed in the faiss namespace
|
||||
|
||||
namespace {
|
||||
|
||||
// Baseline (scalar) implementation for computing cent_distances.
|
||||
// Accumulates codebook cross-norms via fvec_add into a temporary buffer.
|
||||
// Its primary flaw is that it writes too much to the temporary buffer dp.
|
||||
// This code is kept because it is easy to understand what the optimized
|
||||
// SIMD version (compute_cent_distances_simd) optimizes exactly.
|
||||
void compute_cent_distances_baseline(
|
||||
size_t K,
|
||||
size_t beam_size,
|
||||
const float* codebook_cross_norms,
|
||||
size_t ldc,
|
||||
const uint64_t* codebook_offsets,
|
||||
size_t m,
|
||||
const int32_t* codes_i,
|
||||
const float* distances_i,
|
||||
const float* cd_common,
|
||||
float* cent_distances) {
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
std::vector<float> dp(K);
|
||||
for (size_t m1 = 0; m1 < m; m1++) {
|
||||
size_t c = codes_i[b * m + m1];
|
||||
const float* cb =
|
||||
&codebook_cross_norms[(codebook_offsets[m1] + c) * ldc];
|
||||
fvec_add(K, cb, dp.data(), dp.data());
|
||||
}
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] =
|
||||
distances_i[b] + cd_common[k] + 2 * dp[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SIMD-optimized implementation for computing cent_distances.
|
||||
// Uses accum_and_finalize_tab / accum_and_store_tab / accum_and_add_tab
|
||||
// to accumulate codebook cross-norms in SIMD registers.
|
||||
template <SIMDLevel SL>
|
||||
void compute_cent_distances_simd(
|
||||
size_t K,
|
||||
size_t beam_size,
|
||||
const float* codebook_cross_norms,
|
||||
size_t ldc,
|
||||
const uint64_t* codebook_offsets,
|
||||
size_t m,
|
||||
const int32_t* codes_i,
|
||||
const float* distances_i,
|
||||
const float* cd_common,
|
||||
float* cent_distances) {
|
||||
auto do_finalize = [&]<size_t NK>() {
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
accum_and_finalize_tab<NK, 4, SL>(
|
||||
codebook_cross_norms,
|
||||
codebook_offsets,
|
||||
codes_i,
|
||||
b,
|
||||
ldc,
|
||||
K,
|
||||
distances_i,
|
||||
cd_common,
|
||||
cent_distances);
|
||||
}
|
||||
};
|
||||
|
||||
switch (m) {
|
||||
case 0:
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] = distances_i[b] + cd_common[k];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
do_finalize.template operator()<1>();
|
||||
break;
|
||||
case 2:
|
||||
do_finalize.template operator()<2>();
|
||||
break;
|
||||
case 3:
|
||||
do_finalize.template operator()<3>();
|
||||
break;
|
||||
case 4:
|
||||
do_finalize.template operator()<4>();
|
||||
break;
|
||||
case 5:
|
||||
do_finalize.template operator()<5>();
|
||||
break;
|
||||
case 6:
|
||||
do_finalize.template operator()<6>();
|
||||
break;
|
||||
case 7:
|
||||
do_finalize.template operator()<7>();
|
||||
break;
|
||||
default: {
|
||||
// m >= 8: accumulate in chunks of 8 into a temporary buffer.
|
||||
std::vector<float> dp(K);
|
||||
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
accum_and_store_tab<8, 4, SL>(
|
||||
m,
|
||||
codebook_cross_norms,
|
||||
codebook_offsets,
|
||||
codes_i,
|
||||
b,
|
||||
ldc,
|
||||
K,
|
||||
dp.data());
|
||||
|
||||
for (size_t im = 8; im < ((m + 7) / 8) * 8; im += 8) {
|
||||
size_t m_left = std::min(m - im, size_t(8));
|
||||
auto do_add = [&]<size_t NK2>() {
|
||||
accum_and_add_tab<NK2, 4, SL>(
|
||||
m,
|
||||
codebook_cross_norms,
|
||||
codebook_offsets + im,
|
||||
codes_i + im,
|
||||
b,
|
||||
ldc,
|
||||
K,
|
||||
dp.data());
|
||||
};
|
||||
switch (m_left) {
|
||||
case 1:
|
||||
do_add.template operator()<1>();
|
||||
break;
|
||||
case 2:
|
||||
do_add.template operator()<2>();
|
||||
break;
|
||||
case 3:
|
||||
do_add.template operator()<3>();
|
||||
break;
|
||||
case 4:
|
||||
do_add.template operator()<4>();
|
||||
break;
|
||||
case 5:
|
||||
do_add.template operator()<5>();
|
||||
break;
|
||||
case 6:
|
||||
do_add.template operator()<6>();
|
||||
break;
|
||||
case 7:
|
||||
do_add.template operator()<7>();
|
||||
break;
|
||||
case 8:
|
||||
do_add.template operator()<8>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] =
|
||||
distances_i[b] + cd_common[k] + 2 * dp[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void beam_search_encode_step_tab(
|
||||
size_t K,
|
||||
size_t n,
|
||||
@@ -413,145 +393,33 @@ void beam_search_encode_step_tab(
|
||||
cd_common[k] = cent_norms_i[k] - 2 * query_cp_i[k];
|
||||
}
|
||||
|
||||
bool use_baseline_implementation = false;
|
||||
|
||||
// This is the baseline implementation. Its primary flaw
|
||||
// that it writes way too many info to the temporary buffer
|
||||
// called dp.
|
||||
//
|
||||
// This baseline code is kept intentionally because it is easy to
|
||||
// understand what an optimized version optimizes exactly.
|
||||
//
|
||||
if (use_baseline_implementation) {
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
std::vector<float> dp(K);
|
||||
|
||||
for (size_t m1 = 0; m1 < m; m1++) {
|
||||
size_t c = codes_i[b * m + m1];
|
||||
const float* cb =
|
||||
&codebook_cross_norms
|
||||
[(codebook_offsets[m1] + c) * ldc];
|
||||
fvec_add(K, cb, dp.data(), dp.data());
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] =
|
||||
distances_i[b] + cd_common[k] + 2 * dp[k];
|
||||
}
|
||||
with_simd_level_256bit([&]<SIMDLevel SL>() {
|
||||
if constexpr (SL == SIMDLevel::NONE) {
|
||||
compute_cent_distances_baseline(
|
||||
K,
|
||||
beam_size,
|
||||
codebook_cross_norms,
|
||||
ldc,
|
||||
codebook_offsets,
|
||||
m,
|
||||
codes_i,
|
||||
distances_i,
|
||||
cd_common.data(),
|
||||
cent_distances.data());
|
||||
} else {
|
||||
compute_cent_distances_simd<SL>(
|
||||
K,
|
||||
beam_size,
|
||||
codebook_cross_norms,
|
||||
ldc,
|
||||
codebook_offsets,
|
||||
m,
|
||||
codes_i,
|
||||
distances_i,
|
||||
cd_common.data(),
|
||||
cent_distances.data());
|
||||
}
|
||||
|
||||
} else {
|
||||
// An optimized implementation that avoids using a temporary buffer
|
||||
// and does the accumulation in registers.
|
||||
|
||||
// Compute a sum of NK AQ codes.
|
||||
#define ACCUM_AND_FINALIZE_TAB(NK) \
|
||||
case NK: \
|
||||
for (size_t b = 0; b < beam_size; b++) { \
|
||||
accum_and_finalize_tab<NK, 4>( \
|
||||
codebook_cross_norms, \
|
||||
codebook_offsets, \
|
||||
codes_i, \
|
||||
b, \
|
||||
ldc, \
|
||||
K, \
|
||||
distances_i, \
|
||||
cd_common.data(), \
|
||||
cent_distances.data()); \
|
||||
} \
|
||||
break;
|
||||
|
||||
// this version contains many switch-case scenarios, but
|
||||
// they won't affect branch predictor.
|
||||
switch (m) {
|
||||
case 0:
|
||||
// trivial case
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] =
|
||||
distances_i[b] + cd_common[k];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
ACCUM_AND_FINALIZE_TAB(1)
|
||||
ACCUM_AND_FINALIZE_TAB(2)
|
||||
ACCUM_AND_FINALIZE_TAB(3)
|
||||
ACCUM_AND_FINALIZE_TAB(4)
|
||||
ACCUM_AND_FINALIZE_TAB(5)
|
||||
ACCUM_AND_FINALIZE_TAB(6)
|
||||
ACCUM_AND_FINALIZE_TAB(7)
|
||||
|
||||
default: {
|
||||
// m >= 8 case.
|
||||
|
||||
// A temporary buffer has to be used due to the lack of
|
||||
// registers. But we'll try to accumulate up to 8 AQ codes
|
||||
// in registers and issue a single write operation to the
|
||||
// buffer, while the baseline does no accumulation. So, the
|
||||
// number of write operations to the temporary buffer is
|
||||
// reduced 8x.
|
||||
|
||||
// allocate a temporary buffer
|
||||
std::vector<float> dp(K);
|
||||
|
||||
for (size_t b = 0; b < beam_size; b++) {
|
||||
// Initialize it. Compute a sum of first 8 AQ codes
|
||||
// because m >= 8 .
|
||||
accum_and_store_tab<8, 4>(
|
||||
m,
|
||||
codebook_cross_norms,
|
||||
codebook_offsets,
|
||||
codes_i,
|
||||
b,
|
||||
ldc,
|
||||
K,
|
||||
dp.data());
|
||||
|
||||
#define ACCUM_AND_ADD_TAB(NK) \
|
||||
case NK: \
|
||||
accum_and_add_tab<NK, 4>( \
|
||||
m, \
|
||||
codebook_cross_norms, \
|
||||
codebook_offsets + im, \
|
||||
codes_i + im, \
|
||||
b, \
|
||||
ldc, \
|
||||
K, \
|
||||
dp.data()); \
|
||||
break;
|
||||
|
||||
// accumulate up to 8 additional AQ codes into
|
||||
// a temporary buffer
|
||||
for (size_t im = 8; im < ((m + 7) / 8) * 8; im += 8) {
|
||||
size_t m_left = m - im;
|
||||
if (m_left > 8) {
|
||||
m_left = 8;
|
||||
}
|
||||
|
||||
switch (m_left) {
|
||||
ACCUM_AND_ADD_TAB(1)
|
||||
ACCUM_AND_ADD_TAB(2)
|
||||
ACCUM_AND_ADD_TAB(3)
|
||||
ACCUM_AND_ADD_TAB(4)
|
||||
ACCUM_AND_ADD_TAB(5)
|
||||
ACCUM_AND_ADD_TAB(6)
|
||||
ACCUM_AND_ADD_TAB(7)
|
||||
ACCUM_AND_ADD_TAB(8)
|
||||
}
|
||||
}
|
||||
|
||||
// done. finalize the result
|
||||
for (size_t k = 0; k < K; k++) {
|
||||
cent_distances[b * K + k] =
|
||||
distances_i[b] + cd_common[k] + 2 * dp[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the optimized implementation ends here
|
||||
}
|
||||
});
|
||||
using C = CMax<float, int>;
|
||||
int32_t* new_codes_i = new_codes + i * (m + 1) * new_beam_size;
|
||||
float* new_distances_i = new_distances + i * new_beam_size;
|
||||
@@ -564,22 +432,28 @@ void beam_search_encode_step_tab(
|
||||
}
|
||||
std::vector<int> perm(new_beam_size, -1);
|
||||
|
||||
#define HANDLE_APPROX(NB, BD) \
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B##NB##_D##BD: \
|
||||
HeapWithBuckets<C, NB, BD>::bs_addn( \
|
||||
beam_size, \
|
||||
K, \
|
||||
cent_distances_i, \
|
||||
new_beam_size, \
|
||||
new_distances_i, \
|
||||
perm.data()); \
|
||||
break;
|
||||
|
||||
auto approx = [&]<uint32_t NB, uint32_t ND>() {
|
||||
HeapWithBuckets<C, NB, ND>::bs_addn(
|
||||
beam_size,
|
||||
K,
|
||||
cent_distances_i,
|
||||
new_beam_size,
|
||||
new_distances_i,
|
||||
perm.data());
|
||||
};
|
||||
switch (approx_topk_mode) {
|
||||
HANDLE_APPROX(8, 3)
|
||||
HANDLE_APPROX(8, 2)
|
||||
HANDLE_APPROX(16, 2)
|
||||
HANDLE_APPROX(32, 2)
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B8_D3:
|
||||
approx.template operator()<8, 3>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B8_D2:
|
||||
approx.template operator()<8, 2>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B16_D2:
|
||||
approx.template operator()<16, 2>();
|
||||
break;
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B32_D2:
|
||||
approx.template operator()<32, 2>();
|
||||
break;
|
||||
default:
|
||||
heap_addn<C>(
|
||||
new_beam_size,
|
||||
@@ -593,8 +467,6 @@ void beam_search_encode_step_tab(
|
||||
|
||||
heap_reorder<C>(new_beam_size, new_distances_i, perm.data());
|
||||
|
||||
#undef HANDLE_APPROX
|
||||
|
||||
for (int j = 0; j < new_beam_size; j++) {
|
||||
int js = perm[j] / K;
|
||||
int ls = perm[j] % K;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <faiss/Index.h>
|
||||
#include <faiss/utils/approx_topk/mode.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
|
||||
@@ -691,7 +691,7 @@ void gpu_sync_all_devices()
|
||||
%template(IndexBinaryIDMap2) faiss::IndexIDMap2Template<faiss::IndexBinary>;
|
||||
|
||||
|
||||
%include <faiss/utils/approx_topk/mode.h>
|
||||
%include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
#ifdef FAISS_ENABLE_SVS
|
||||
%ignore faiss::to_svs_storage_kind;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This file contains an implementation of approximate top-k search
|
||||
// using heap. It was initially created for a beam search.
|
||||
//
|
||||
// The core idea is the following.
|
||||
// Say we need to find beam_size indices with the minimal distance
|
||||
// values. It is done via heap (priority_queue) using the following
|
||||
// pseudocode:
|
||||
//
|
||||
// def baseline():
|
||||
// distances = np.empty([beam_size * n], dtype=float)
|
||||
// indices = np.empty([beam_size * n], dtype=int)
|
||||
//
|
||||
// heap = Heap(max_heap_size=beam_size)
|
||||
//
|
||||
// for i in range(0, beam_size * n):
|
||||
// heap.push(distances[i], indices[i])
|
||||
//
|
||||
// Basically, this is what heap_addn() function from utils/Heap.h does.
|
||||
//
|
||||
// The following scheme can be used for approximate beam search.
|
||||
// Say, we need to find elements with min distance.
|
||||
// Basically, we split n elements of every beam into NBUCKETS buckets
|
||||
// and track the index with the minimal distance for every bucket.
|
||||
// This can be effectively SIMD-ed and significantly lowers the number
|
||||
// of operations, but yields approximate results for beam_size >= 2.
|
||||
//
|
||||
// def approximate_v1():
|
||||
// distances = np.empty([beam_size * n], dtype=float)
|
||||
// indices = np.empty([beam_size * n], dtype=int)
|
||||
//
|
||||
// heap = Heap(max_heap_size=beam_size)
|
||||
//
|
||||
// for beam in range(0, beam_size):
|
||||
// # The value of 32 is just an example.
|
||||
// # The value may be varied: the larger the value is,
|
||||
// # the slower and the more precise vs baseline beam search is
|
||||
// NBUCKETS = 32
|
||||
//
|
||||
// local_min_distances = [HUGE_VALF] * NBUCKETS
|
||||
// local_min_indices = [0] * NBUCKETS
|
||||
//
|
||||
// for i in range(0, n / NBUCKETS):
|
||||
// for j in range(0, NBUCKETS):
|
||||
// idx = beam * n + i * NBUCKETS + j
|
||||
// if distances[idx] < local_min_distances[j]:
|
||||
// local_min_distances[j] = distances[idx]
|
||||
// local_min_indices[j] = indices[idx]
|
||||
//
|
||||
// for j in range(0, NBUCKETS):
|
||||
// heap.push(local_min_distances[j], local_min_indices[j])
|
||||
//
|
||||
// The accuracy can be improved by tracking min-2 elements for every
|
||||
// bucket. Such a min-2 implementation with NBUCKETS buckets provides
|
||||
// better accuracy than top-1 implementation with 2 * NBUCKETS buckets.
|
||||
// Min-3 is also doable. One can use min-N approach, but I'm not sure
|
||||
// whether min-4 and above are practical, because of the lack of SIMD
|
||||
// registers (unless AVX-512 version is used).
|
||||
//
|
||||
// C++ template for top-N implementation is provided. The code
|
||||
// assumes that indices[idx] == idx. One can write a code that lifts
|
||||
// such an assumption easily.
|
||||
//
|
||||
// Currently, the code that tracks elements with min distances is implemented
|
||||
// (Max Heap). Min Heap option can be added easily.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
|
||||
// the list of available modes is in the following file
|
||||
#include <faiss/utils/approx_topk/mode.h>
|
||||
|
||||
#ifdef __AVX2__
|
||||
#include <faiss/utils/approx_topk/avx2-inl.h>
|
||||
#else
|
||||
#include <faiss/utils/approx_topk/generic.h>
|
||||
#endif
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
template <typename C, uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets {
|
||||
// this case was not implemented yet.
|
||||
};
|
||||
|
||||
template <uint32_t NBUCKETS, uint32_t N>
|
||||
struct HeapWithBuckets<CMax<float, int>, NBUCKETS, N> {
|
||||
static constexpr uint32_t NBUCKETS_8 = NBUCKETS / 8;
|
||||
static_assert(
|
||||
(NBUCKETS) > 0 && ((NBUCKETS % 8) == 0),
|
||||
"Number of buckets needs to be 8, 16, 24, ...");
|
||||
|
||||
static void addn(
|
||||
// number of elements
|
||||
const uint32_t n,
|
||||
// distances. It is assumed to have n elements.
|
||||
const float* const __restrict distances,
|
||||
// number of best elements to keep
|
||||
const uint32_t k,
|
||||
// output distances
|
||||
float* const __restrict bh_val,
|
||||
// output indices, each being within [0, n) range
|
||||
int32_t* const __restrict bh_ids) {
|
||||
// forward a call to bs_addn with 1 beam
|
||||
bs_addn(1, n, distances, k, bh_val, bh_ids);
|
||||
}
|
||||
|
||||
static void bs_addn(
|
||||
// beam_size parameter of Beam Search algorithm
|
||||
const uint32_t beam_size,
|
||||
// number of elements per beam
|
||||
const uint32_t n_per_beam,
|
||||
// distances. It is assumed to have (n_per_beam * beam_size)
|
||||
// elements.
|
||||
const float* const __restrict distances,
|
||||
// number of best elements to keep
|
||||
const uint32_t k,
|
||||
// output distances
|
||||
float* const __restrict bh_val,
|
||||
// output indices, each being within [0, n_per_beam * beam_size)
|
||||
// range
|
||||
int32_t* const __restrict bh_ids) {
|
||||
// // Basically, the function runs beam_size iterations.
|
||||
// // Every iteration NBUCKETS * N elements are added to a regular heap.
|
||||
// // So, maximum number of added elements is beam_size * NBUCKETS * N.
|
||||
// // This number is expected to be less or equal than k.
|
||||
// FAISS_THROW_IF_NOT_FMT(
|
||||
// beam_size * NBUCKETS * N >= k,
|
||||
// "Cannot pick %d elements, only %d. "
|
||||
// "Check the function and template arguments values.",
|
||||
// k,
|
||||
// beam_size * NBUCKETS * N);
|
||||
|
||||
using C = CMax<float, int>;
|
||||
|
||||
// main loop
|
||||
for (uint32_t beam_index = 0; beam_index < beam_size; beam_index++) {
|
||||
__m256 min_distances_i[NBUCKETS_8][N];
|
||||
__m256i min_indices_i[NBUCKETS_8][N];
|
||||
|
||||
for (uint32_t j = 0; j < NBUCKETS_8; j++) {
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
min_distances_i[j][p] =
|
||||
_mm256_set1_ps(std::numeric_limits<float>::max());
|
||||
min_indices_i[j][p] =
|
||||
_mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
|
||||
}
|
||||
}
|
||||
|
||||
__m256i current_indices = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
|
||||
__m256i indices_delta = _mm256_set1_epi32(NBUCKETS);
|
||||
|
||||
const uint32_t nb = (n_per_beam / NBUCKETS) * NBUCKETS;
|
||||
|
||||
// put the data into buckets
|
||||
for (uint32_t ip = 0; ip < nb; ip += NBUCKETS) {
|
||||
for (uint32_t j = 0; j < NBUCKETS_8; j++) {
|
||||
const __m256 distances_reg = _mm256_loadu_ps(
|
||||
distances + j * 8 + ip + n_per_beam * beam_index);
|
||||
|
||||
// loop. Compiler should get rid of unneeded ops
|
||||
__m256 distance_candidate = distances_reg;
|
||||
__m256i indices_candidate = current_indices;
|
||||
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
const __m256 comparison = _mm256_cmp_ps(
|
||||
min_distances_i[j][p],
|
||||
distance_candidate,
|
||||
_CMP_LE_OS);
|
||||
|
||||
// // blend seems to be slower than min
|
||||
// const __m256 min_distances_new = _mm256_blendv_ps(
|
||||
// distance_candidate,
|
||||
// min_distances_i[j][p],
|
||||
// comparison);
|
||||
const __m256 min_distances_new = _mm256_min_ps(
|
||||
distance_candidate, min_distances_i[j][p]);
|
||||
const __m256i min_indices_new =
|
||||
_mm256_castps_si256(_mm256_blendv_ps(
|
||||
_mm256_castsi256_ps(indices_candidate),
|
||||
_mm256_castsi256_ps(
|
||||
min_indices_i[j][p]),
|
||||
comparison));
|
||||
|
||||
// // blend seems to be slower than min
|
||||
// const __m256 max_distances_new = _mm256_blendv_ps(
|
||||
// min_distances_i[j][p],
|
||||
// distance_candidate,
|
||||
// comparison);
|
||||
const __m256 max_distances_new = _mm256_max_ps(
|
||||
min_distances_i[j][p], distances_reg);
|
||||
const __m256i max_indices_new =
|
||||
_mm256_castps_si256(_mm256_blendv_ps(
|
||||
_mm256_castsi256_ps(
|
||||
min_indices_i[j][p]),
|
||||
_mm256_castsi256_ps(indices_candidate),
|
||||
comparison));
|
||||
|
||||
distance_candidate = max_distances_new;
|
||||
indices_candidate = max_indices_new;
|
||||
|
||||
min_distances_i[j][p] = min_distances_new;
|
||||
min_indices_i[j][p] = min_indices_new;
|
||||
}
|
||||
}
|
||||
|
||||
current_indices =
|
||||
_mm256_add_epi32(current_indices, indices_delta);
|
||||
}
|
||||
|
||||
// fix the indices
|
||||
for (uint32_t j = 0; j < NBUCKETS_8; j++) {
|
||||
const __m256i offset =
|
||||
_mm256_set1_epi32(n_per_beam * beam_index + j * 8);
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
min_indices_i[j][p] =
|
||||
_mm256_add_epi32(min_indices_i[j][p], offset);
|
||||
}
|
||||
}
|
||||
|
||||
// merge every bucket into the regular heap
|
||||
for (uint32_t p = 0; p < N; p++) {
|
||||
for (uint32_t j = 0; j < NBUCKETS_8; j++) {
|
||||
int32_t min_indices_scalar[8];
|
||||
float min_distances_scalar[8];
|
||||
|
||||
_mm256_storeu_si256(
|
||||
(__m256i*)min_indices_scalar, min_indices_i[j][p]);
|
||||
_mm256_storeu_ps(
|
||||
min_distances_scalar, min_distances_i[j][p]);
|
||||
|
||||
// this exact way is needed to maintain the order as if the
|
||||
// input elements were pushed to the heap sequentially
|
||||
for (size_t j8 = 0; j8 < 8; j8++) {
|
||||
const auto value = min_distances_scalar[j8];
|
||||
const auto index = min_indices_scalar[j8];
|
||||
if (C::cmp2(bh_val[0], value, bh_ids[0], index)) {
|
||||
heap_replace_top<C>(
|
||||
k, bh_val, bh_ids, value, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process leftovers
|
||||
for (uint32_t ip = nb; ip < n_per_beam; ip++) {
|
||||
const int32_t index = ip + n_per_beam * beam_index;
|
||||
const float value = distances[index];
|
||||
|
||||
if (C::cmp(bh_val[0], value)) {
|
||||
heap_replace_top<C>(k, bh_val, bh_ids, value, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace faiss
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/// Represents the mode of use of approximate top-k computations
|
||||
/// that allows to trade accuracy vs speed. So, every options
|
||||
/// besides EXACT_TOPK increases the speed.
|
||||
///
|
||||
/// B represents the number of buckets.
|
||||
/// D is the number of min-k elements to track within every bucket.
|
||||
///
|
||||
/// Default option is EXACT_TOPK.
|
||||
/// APPROX_TOPK_BUCKETS_B16_D2 is worth starting from, if you'd like
|
||||
/// to experiment a bit.
|
||||
///
|
||||
/// It seems that only the limited number of combinations are
|
||||
/// meaningful, because of the limited supply of SIMD registers.
|
||||
/// Also, certain combinations, such as B32_D1 and B16_D1, were concluded
|
||||
/// to be not very precise in benchmarks, so they were not introduced.
|
||||
///
|
||||
/// TODO: Consider d-ary SIMD heap.
|
||||
|
||||
enum ApproxTopK_mode_t : int {
|
||||
EXACT_TOPK = 0,
|
||||
APPROX_TOPK_BUCKETS_B32_D2 = 1,
|
||||
APPROX_TOPK_BUCKETS_B8_D3 = 2,
|
||||
APPROX_TOPK_BUCKETS_B16_D2 = 3,
|
||||
APPROX_TOPK_BUCKETS_B8_D2 = 4,
|
||||
};
|
||||
@@ -34,7 +34,7 @@
|
||||
// Low-level Hamming distance computations and hamdis_t.
|
||||
#include <faiss/utils/hamming_distance/hamdis-inl.h>
|
||||
|
||||
#include <faiss/utils/approx_topk/mode.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <faiss/utils/approx_topk/approx_topk.h>
|
||||
#include <faiss/impl/approx_topk/approx_topk.h>
|
||||
|
||||
#include <faiss/impl/FaissException.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
|
||||
Reference in New Issue
Block a user