跳到主要内容

性能优化建议

本页整理了一些让 Faiss 跑得更快的实用技巧,尤其适用于速度优先的场景,同时也介绍了速度与准确率之间的折中方式。

速度与准确率的索引参数

以下内容简要介绍了在配置索引时值得关注的参数。

选择索引类型

首先应选择合适的索引类型:相似度检索用索引向量编码用索引

另请阅读关于加性量化器(Additive Quantizers)的介绍。

备注

目前基于 ProductAdditiveQuantizer 的索引家族还未完善文档和基准测试,有更新会在后续补充。

选择索引参数

通过faiss.index_factory()来实例化索引很常见,但这种方式会设定参数为较保守的默认值。实际上,Faiss 支持丰富的速度相关参数,许多组件涉及以下内容:

  • 编码阶段的迭代算法:迭代次数越多,召回率(recall)通常越高
    • HNSW: efConstruction
    • ResidualQuantizer: niter_codebook_refine
    • LocalSearchQuantizer: encode_ils_iters, icm_iters, train_iters, encode_ils_iters(用于向量编码器)
  • 启发式算法:在训练或检索时限制候选数量,候选越多,召回率越高
    • IndexBinaryIVF: nprobe, max_codes
    • IndexIVF: nprobe, max_codes
    • ResidualQuantizer: max_beam_size
    • HNSW: efSearch
  • 可选加速模式:如基于查找表(LUT)的近似,通常用准确率换取更快速度
    • IndexIVFPQ: do_polysemous_training, polysemous_ht, use_precomputed_table
    • IndexIVFPQFastScan: use_precomputed_table
    • IndexPQ: do_polysemous_training, polysemous_ht, search_type
    • ResidualQuantizer: use_beam_LUT, approx_topk_mode, train_type
    • LocalSearchQuantizer: update_codebooks_with_double
  • 特定情况的计算优化:例如大批量 IVF 检索
  • 分块处理的大容量内部缓冲区:详见后文
  • 随机数生成器:用于结果可复现
提示

默认参数一般表现不错,但强烈建议结合各参数说明针对自己的速度-准确率-内存(RAM)需求摸索最佳组合。

实践建议:研究已选索引及其子索引可用的全部参数。

K-means 聚类

K-means 聚类经常在 Faiss 内部使用。默认情况下,faiss/Clustering.h的 k-means 实现会执行 25 次迭代(niter 参数),每个聚类最多抽取 256 个样本点(max_points_per_centroid 参数)参与,每次抽样是随机选取的。例如,默认的 PQx12 训练大约比 PQx10 慢 4 倍,比 PQx8 慢 16 倍,因为参与样本数成比例增加。

基于 IVF 的索引在默认情况下使用 10 次 k-means 迭代(详见:faiss/IndexIVF.h, faiss/IndexBinaryIVF.h, faiss/gpu/GpuIndexIVF.cu)。

提示

实践建议:尝试调整 k-means 聚类的 nitermax_points_per_centroid 参数,以寻求训练速度和聚类质量的最佳平衡。

查询批处理

Faiss 针对批量(batch)样本优化,推荐尽量批量处理多个查询(而非逐条单独处理)。Faiss 内部会对批量元素进行高效并行,远优于外部手动并行。

提示

实践建议:如条件允许,尽量以批量方式处理请求。

PQ 训练模式:Train_shared

PQ(Product Quantizer,乘积量化器)支持 Train_shared 训练模式,可对全部子量化器(subquantizer)训练一次公共的码本(codebook),而非各自独立训练。这样对 10-bit 或 12-bit PQ 可明显提升训练速度,但会略微降低准确率。

faiss::IndexPQ indexPQ;
indexPQ.pq.train_type = faiss::ProductQuantizer::Train_shared;

faiss::IndexIVFPQ indexIVFPQ;
indexIVFPQ.pq.train_type = faiss::ProductQuantizer::Train_shared;
备注

Train_shared 适合对速度有更高要求但可牺牲一些检索精度的训练场景。

系统配置优化

以下内容针对 Faiss 运行环境的优化建议,如线程并发、内存配置等。

巨页(Huge memory pages)

绝大多数现代 CPU 架构和操作系统都支持巨页。Faiss 常常以随机方式访问大块数据(如码本),这会让 TLB(翻译后备缓冲区,Translation Lookaside Buffer)压力增大。实际经验显示,启用 2MB/1GB 巨页后某些向量编码场景的速度可提升高达 20%(在 x86-64 平台)。Faiss 本身没有专门的巨页分配机制,最简单的测试方式是用支持巨页的内存分配器(如 mimalloc)替代。

注意

暂无 ARM 平台上的相关实验;巨页资源稀缺且难管理,请谨慎使用。

提示

实践建议:尝试启用巨页(Huge memory pages)的内存分配器,看应用是否有提速效果。

NUMA 节点(非一致性内存访问)

Faiss 本身并不感知 NUMA(Non-Uniform Memory Access)架构,也不会主动协调内存分配以减少 NUMA 节点之间的内存通信压力。例如,实际观察过同样的基准测试在单个 NUMA 节点(12 核/24 线程)与 4 个 NUMA 节点下运行速度无明显差异。

提示

实践建议:根据实际硬件资源配置,合理规划应用所使用的 CPU 资源数目和方式。

Intel MKL 库(数学核心库)

Faiss 会尽量使用 Intel MKL(Math Kernel Library) 进行数值计算,MKL 底层依赖 OpenMP。根据官方文档,线程数与物理 CPU 核心数一致时性能最优(见相关讨论),这与我们的测试结果相符。默认情况下 MKL 会开启与超线程数一致的线程(即核心数*2)。

提示

实践建议:除非明确只需单线程,否则建议设置 OMP 线程数等于物理核心数。例如在 12 核/24 线程机器上运行命令如下:

OMP_NUM_THREADS=12 my-application

内部缓冲区大小

如果输入数据集很大,Faiss 可能会将其拆分为多个块(chunk)分批处理。提升单块处理数据量有助于提升缓存效率和线性代数(BLAS)运算效率。但这会增加内存消耗和内存分配耗时。可调整的内部缓冲区包括(但不限于):

  • IndexBinaryFlat.query_batch_size
  • LocalSearchQuantizer.chunk_size
  • AdditiveQuantizer.max_mem_distances
  • faiss.multi_index_quantizer_search_bs
  • faiss.index2layer_sa_encode_bs
  • faiss.product_quantizer_compute_codes_bs
  • faiss.index_ivfpq_add_core_o_bs
  • faiss.precomputed_table_max_bytes
  • faiss.hamming_batch_size
  • faiss.rowwise_minmax_sa_encode_bs
  • faiss.rowwise_minmax_sa_decode_bs
  • faiss.distance_compute_blas_threshold
  • faiss.distance_compute_blas_query_bs
  • faiss.distance_compute_blas_database_bs
  • faiss.distance_compute_min_k_reservoir
备注

部分内部缓冲区暂未开放配置接口,仅可通过源代码修改。

AVX-512 指令集支持

Faiss 核心实现大量利用了 AVX2 或 ARM NEON 指令集优化,目前除了一个 AVX-512 专用 kernel 需手动开启外,尚未广泛支持 AVX-512:

  • Intel MKL 如可用,则相关 GEMM(矩阵乘法)操作已能自动用 AVX-512 提速。
  • AVX-512 有时会导致 CPU 降频,反而性能低于 AVX2。
  • 开发 AVX-512 kernel 对代码维护和编译带来额外成本。

当前(2023 年 1 月):

  • 新一代 Intel CPU 已基本解决 AVX-512 降频问题;
  • AMD Zen4 已支持 AVX-512;
  • bfloat16 也在 AVX-512 指令集中支持,可用于部分加性量化场景,但具体还需补充开发和基准测试。

可通过 Intel MKL 提供的相关配置方法手动控制指令集使用。

目前唯一由 Faiss 提供的 AVX-512 kernel 是融合内核,可同时完成 L2 距离计算和最近邻查找。该内核只在维度较小(1~16)时为 PQ 训练或 k-means 聚类带来少量额外加速,需手动开启。

PQ 转置码本表(Transposed centroid table for Product Quantizer)

(自 Faiss 1.7.3 起)

开启后在 CPU 上用于 IndexPQIndexIVFPQ 时,可用少量额外内存换取 pq.search() 检索速度提升。详见官方 Wiki 说明

示例代码如下:

# 训练一个 IVF PQ CPU 索引
...

# 创建转置码本表
index.pq.sync_transposed_centroids()

# 执行检索
while app_is_running:
D, I = index.search(xq, k)

向量编码器加速

部分向量编码器已引入 C++ 高性能解码模板提升解码速度。

提示

为了加速编码训练场景,应避免使用 Polysemous codes 多义码的索引。例如推荐用 PQ5x12np 而不是 PQ5x12

实现层级的优化

实现层面还存在代码体积与运行性能之间的权衡空间。

C++ 模板优化

Faiss 以 C++ 实现,并在多处利用模板(template)分发不同维度下的高效实现。例如下述函数位于faiss/utils/distances_simd.cpp,用于计算一个 d 维向量 x 与 ny 个 d 维向量 y 的内积:

void fvec_inner_products_ny(
float* dis,
const float* x,
const float* y,
size_t d,
size_t ny) {
#define DISPATCH(dval) \
case dval: \
fvec_op_ny_D##dval<ElementOpIP >(dis, x, y, ny); \
return;

switch (d) {
DISPATCH(1)
DISPATCH(2)
DISPATCH(4)
DISPATCH(8)
DISPATCH(12)
default:
fvec_inner_products_ny_ref(dis, x, y, d, ny);
return;
}
#undef DISPATCH
}

可以看到,对于维度 1, 2, 4, 8, 12 的情况分别调用专用内核,其余则调用通用计算和编译器自动优化。这样提升了常用小维度的执行速度。


【内容未完待续】

自定义的专用计算核心(Custom specialized kernels)通常比通用代码拥有更好的性能,但这会导致程序的二进制文件体积增加。

提示

如果你有适合自己业务需求的自定义计算核心,欢迎自由添加,并向我们提交 Pull Request!

CUDA 模板

由于 GPU 运算的严格约束,Faiss 中的 CUDA 代码比 C++ 代码更加频繁地使用了模板(template)。这样做是为了充分发挥显卡计算能力,提高并行效率。

反馈与建议

我们非常欢迎关于 Faiss 执行速度的任何反馈意见!

important

最有价值的反馈是可以复现的独立 Python 脚本,或是小型的 C++ 程序。这些代码应能明确展示 Faiss 在实际使用中的性能瓶颈位置(hot-spot)。

如果你发现了性能改进的线索,欢迎与我们分享,以便共同提升 Faiss 的表现。