Skip to content

Faiss 源码结构与常用 API 导读

本文配合 Faiss 项目调研 阅读:前一篇回答选型问题,这一篇回答“源码放在哪里、对象怎样协作、API 怎样使用”。

源码基线固定为 Faiss v1.12.0,不表示它是最新版本。以下路径均相对于该版本仓库根目录,调用链为省略异常处理、并行分支等细节后的阅读路线。核对日期:2026-09-17。

1. 先看整体分层

可以把 Faiss 分为接口、索引组织、算法组件和计算内核四层:

Python / C++ / C 调用者
        │
        ├─ Python: faiss/python/ → SWIG → C++
        ├─ C++:    faiss/Index.h 与具体索引头文件
        └─ C:      c_api/ → C++
        │
        ▼
IndexFlat / IndexIVF / IndexHNSW / 组合索引
        │
        ├─ impl/      量化、图算法、距离访问接口、结果处理
        ├─ invlists/  IVF 的列表存储和 ID 定位
        ├─ utils/     CPU 距离、堆、SIMD 等基础设施
        └─ gpu/       GPU 索引、资源管理、设备端实现

这张图表达职责分层,不是严格的类继承树。尤其要分清:IndexIVFPQ 是可检索的索引,ProductQuantizer 是量化组件;IndexHNSW 对外提供索引接口,HNSW 保存并操作图结构。源码目录

2. 仓库顶层目录:先读哪些

路径 职责 阅读时机
faiss/ C++ 核心、Python 包装、GPU 实现 主线
tutorial/ Python/C++ 最小示例 第一步,先理解输入输出
tests/ 正确性测试、索引行为与边界条件 用来核实一个 API 能做什么
demos/ 更完整的演示程序 理解实际数据上的使用流程
benchs/ 各类算法与性能实验脚本 研究参数、精度和效率
perf_tests/ 独立性能测试代码 定位性能回归与测量方式
c_api/ C 接口封装 从 C 或其他语言接入
contrib/ Python 辅助工具,如与 PyTorch 的衔接 主接口满足不了需求时
cmake/CMakeLists.txt 构建选项、依赖和目标定义 编译、定制优化或移植
conda/ 包构建相关文件 理解发布环境
misc/ 零散辅助脚本 按需查阅
INSTALL.mdCHANGELOG.md 安装要求和版本变化 切换环境或版本前

以上是职责摘要,完整文件列表见 v1.12.0 仓库。学习时建议先跑 tutorial,再追一个 search 调用,不必从头遍历所有文件。

3. faiss/ 下的主要文件

3.1 公共契约与基础索引

文件 作用
Index.h / Index.cpp 浮点向量索引的基类、状态与默认行为
MetricType.h 距离度量枚举,如 L2、内积
IndexFlatCodes.* 固定长度编码数组的存储和基础操作
IndexFlat.* 原始浮点向量的精确穷举检索
IndexPQ.* 使用 PQ 编码的索引
IndexScalarQuantizer.* 标量量化索引
IndexBinary.hIndexBinaryFlat.* 独立的二进制向量索引体系

不要被 IndexFlatCodes 的名字误导:这里的 code 是固定长度的字节表示,不一定压缩。IndexFlat 也继承它,使用原始浮点数据对应的字节存储。IndexFlatCodes.hIndexFlat.h

3.2 候选组织与索引组合

文件 作用
IndexIVF.* IVF 公共流程:训练粗量化器、分桶、查询选桶、调度扫描器
IndexIVFFlat.* 桶内保存原始向量,直接计算距离
IndexIVFPQ.* 桶内保存 PQ 编码,执行编码和距离估计
IndexHNSW.* 将图结构和底层向量存储组合为索引
IndexPreTransform.* 在索引前串接 PCA、OPQ 等变换
IndexIDMap.* 内部位置与外部 ID 的映射
IndexRefine.* 候选召回后进一步计算距离和排序
IndexShards.* 聚合不同数据分片的检索结果
IndexReplicas.* 管理同一份数据的多个副本,并分配查询
index_factory.* 解析字符串,创建组合索引
AutoTune.* 参数设置、候选参数空间与评估辅助
Clustering.*VectorTransform.* 聚类和向量变换
index_io.hclone_index.* 索引读写声明与复制入口

Shard 和 Replica 是库内组合能力;由它们不能直接推导出跨机器 RPC、故障恢复或副本一致性已经实现。索引源码

3.3 impl/、invlists/、utils/ 如何区分

目录与典型文件 负责什么 适合追踪的问题
impl/ProductQuantizer.* PQ 训练、编码、解码、距离表 PQ 码本如何使用
impl/ScalarQuantizer.* 标量量化 各量化类型如何编码
impl/HNSW.* 图层级、邻居与图搜索 efSearch 如何影响探索
impl/DistanceComputer.h 按 ID 访问向量距离的抽象 图搜索如何接不同存储
impl/IDSelector.* 判断一个 ID 是否被选中 过滤和删除如何指定对象
impl/ResultHandler.h 检索结果收集接口 候选怎样进入结果集
impl/index_read.cppindex_write.cpp 各索引的序列化分支 保存了哪些字段
invlists/InvertedLists.* IVF 列表存储抽象与内存实现 一个桶里如何放 codes 和 IDs
invlists/DirectMap.* 从 ID 定位到列表号和列表偏移 IVF 如何 reconstruct 或更新
invlists/OnDiskInvertedLists.* 文件支持的倒排列表实现 大索引的磁盘存储路径
utils/distances.* CPU 距离与近邻计算入口 Flat 的性能瓶颈
utils/Heap.*simdlib* 堆操作、SIMD 抽象等 top-k 和向量化实现

例如,研究 IVFPQ 时,先在 IndexIVFPQ 找到索引流程,再进入 ProductQuantizer 看编码算法,最后按实际调用定位距离表及 SIMD 内核。impl/ 并不是一个统一的后端,它包含多种被上层复用的组件。implinvlistsutils

4. 类关系:继承与组合要一起看

常用类的简化继承关系如下:

Index
├─ IndexFlatCodes
│  ├─ IndexFlat
│  │  ├─ IndexFlatL2
│  │  └─ IndexFlatIP
│  ├─ IndexPQ
│  └─ IndexScalarQuantizer
├─ IndexIVF
│  ├─ IndexIVFFlat
│  └─ IndexIVFPQ
├─ IndexHNSW
│  └─ IndexHNSWFlat
├─ IndexPreTransform
└─ IndexIDMapTemplate<Index>
   └─ IndexIDMap2Template<Index>

同时存在几条重要的组合关系:

  • IndexIVF.quantizer 指向粗量化索引,用于将向量分配到桶;invlists 保存桶内数据。
  • IndexIVFPQ.pq 保存 PQ 组件;默认残差编码路径先减去粗聚类中心,再量化残差。
  • IndexHNSW.hnsw 管理图,storage 管理向量或编码;图并不等于向量存储。
  • IndexPreTransform.chain 保存变换链,index 指向后续索引。
  • IndexIDMap2 在 ID 映射基础上维护反向映射,便于按业务 ID 重建向量。

这些关系可分别在 IndexIVF.hIndexIVFPQ.hIndexHNSW.hIndexPreTransform.hIndexIDMap.h 中核对。

5. 一次 Python search 怎样进入 C++

用户通常写:

D, I = index.search(xq, k)

但 C++ 接口需要查询数量、输入指针和预分配的输出:

virtual void search(
    idx_t n, const float* x, idx_t k,
    float* distances, idx_t* labels,
    const SearchParameters* params = nullptr) const = 0;

衔接工作主要位于 faiss/python/

文件 职责
__init__.py 导入绑定、安装包装方法、维护部分对象引用关系
loader.py 选择可加载的编译模块及指令集变体
swigfaiss.swig SWIG 绑定定义,连接 Python 与 C++
class_wrappers.py 将数组式 API 转换成底层指针式调用
array_conversions.py NumPy 与 C++ 容器的转换辅助
extra_wrappers.py Kmeans 等便利封装
gpu_wrappers.py Python GPU 转换及多 GPU 辅助函数

以默认 float32 路径为例,replacement_search 读取 x.shape、转成连续数组、检查维度,为 D/I 分配空间,再调用 search_csearch_c 是保留下来的底层绑定方法,不是另一套检索算法。随后通过 C++ 虚函数分派进入具体索引。

Python index.search(xq, k)
 → class_wrappers.py: replacement_search
 → search_c(n, swig_ptr(xq), k, swig_ptr(D), swig_ptr(I), params)
 → SWIG 绑定
 → 具体 C++ Index::search 实现

所以研究算法应继续进入 C++;研究 dtype、维度错误或 Python 接口差异,则先看包装层。class_wrappers.pyPython 目录

6. 三条值得先追的检索调用链

6.1 Flat:最短的基线

IndexFlat::search                     faiss/IndexFlat.cpp
 → 判断 metric_type
 → knn_L2sqr / knn_inner_product      faiss/utils/distances.cpp
 → 距离计算与结果维护
 → 写入 distances / labels

读取输入来自 get_xb()。根据数据形状及条件,底层可能走不同计算分支,不应认为每次搜索都调用同一个 BLAS 或 SIMD 内核。建议先用 Flat 理解内存布局、距离与 top-k,再看近似索引。IndexFlat.cppdistances.cpp

6.2 IVF:先选桶,再扫描

IndexIVF::search                      faiss/IndexIVF.cpp
 → 从请求参数或索引成员读取 nprobe
 → quantizer->search                 找到候选桶
 → invlists->prefetch_lists          提示预取
 → search_preassigned               处理已选好的桶
    → get_InvertedListScanner       由具体索引创建扫描器
    → 扫描桶内 codes / IDs
    → scan_codes                    比较候选、更新 top-k

IndexIVFFlatIndexIVFPQ 共享选桶框架,但扫描器解释 codes 的方式不同:前者使用原始浮点向量,后者使用 PQ 编码。search_preassigned 是已有粗分桶结果时的底层入口,普通用户通常调用 search 即可。IndexIVF.cppIndexIVFFlat.cppIndexIVFPQ.cpp

训练与添加还要单独追:

train: 训练粗量化器 → 需要时计算残差 → train_encoder
add:   分配桶 → 具体 add_core → 编码/存储 → 更新 ID 定位信息

这解释了为什么只调用 train 不会让 ntotal 增加,也解释了为什么 IVFPQ 的训练成本不能只看粗聚类。

6.3 HNSW:图搜索与距离存储分离

IndexHNSW::search                    faiss/IndexHNSW.cpp
 → hnsw_search
 → 从 storage 获取 DistanceComputer
 → 为查询设置距离计算对象
 → HNSW::search                     faiss/impl/HNSW.cpp
 → 维护访问标记、候选和结果

efConstruction 影响建图阶段的搜索,efSearch 影响查询阶段的探索。修改查询参数不会重建已有图。理解 DistanceComputer 后,再看不同 HNSW 存储变体会更容易。IndexHNSW.cppHNSW.cpp

7. 常用 API 速查

以下为 Python 常见形式,具体支持情况以索引类型为准。

API / 属性 功能 常见误区
index.d 输入维度 数据最后一维必须匹配
index.ntotal 已入库向量数 训练样本数不计入其中
index.is_trained 是否完成所需训练 Flat 初始即为 True
index.train(xt) 学习索引参数 不能代替 add
index.add(xb) 按顺序 ID 添加 不等于按业务主键 upsert
index.add_with_ids(xb, ids) 指定 ID 添加 Flat 原生不支持
D, I = index.search(xq, k) top-k 检索 距离先返回,ID 后返回;缺失 ID 为 -1
index.assign(xq, k) 只返回近邻 ID 与 Kmeans.assign 的返回形式不同
lims, D, I = index.range_search(xq, r) 阈值检索 每个查询结果数不同;并非所有索引支持
index.reconstruct(id) 取回或近似恢复向量 压缩索引可能只能近似恢复
index.reconstruct_n(i0, ni) 批量恢复连续编号 不是任意业务 ID 列表
index.remove_ids(ids) 删除指定 ID 支持性和 ID 变化因索引而异
index.reset() 清空入库数据 不应当作统一的“回到未训练状态”操作
faiss.index_factory(d, spec) 字符串构建组合索引 组合后参数可能在内部子索引上
faiss.ParameterSpace().set_index_parameter(...) 设置支持的索引参数 只适用于识别的参数与索引
faiss.write_index / read_index 文件持久化 GPU 索引先转回 CPU
faiss.serialize_index / deserialize_index 内存序列化 与业务元数据一起管理版本
faiss.clone_index 克隆支持的索引 会增加资源占用
faiss.normalize_L2(x) 原地逐行归一化 会修改输入数组
faiss.omp_set_num_threads(n) 设置 OpenMP 线程数 不是所有线程池的统一开关

接口定义与包装细节见 Index.hclass_wrappers.pyindex_io.h

8. 可运行的 CPU 示例

以下 Python 代码块按顺序执行,共享第一个代码块的数据。数据是随机生成的,只用于展示 API 和验证行为,不用于性能结论。

可在独立环境安装 faiss-cpu==1.12.0 和 NumPy;不要加入本站的 MkDocs 依赖。本次验证使用 Python 3.12、Faiss CPU 1.12.0,GPU 与后面的 C++ 示例不在 CPU 验证范围内。

8.1 数据约定和精确检索

常规浮点接口使用二维连续 float32 数组:xb.shape=(nb,d)xq.shape=(nq,d)。业务 ID 使用 int64。输出 D/I 的形状都是 (nq,k)

import numpy as np
import faiss

faiss.omp_set_num_threads(2)
rng = np.random.default_rng(42)
d, k = 32, 5
xt = np.ascontiguousarray(rng.normal(size=(4096, d)), dtype=np.float32)
xb = np.ascontiguousarray(rng.normal(size=(2000, d)), dtype=np.float32)
xq = xb[:8].copy()

flat = faiss.IndexFlatL2(d)
assert flat.is_trained
flat.add(xb)
D, I = flat.search(xq, k)
assert flat.ntotal == len(xb)
assert I.shape == (8, k)
assert np.array_equal(I[:, 0], np.arange(8))
assert np.allclose(D[:, 0], 0, atol=1e-5)
assert np.allclose(flat.reconstruct(0), xb[0])
print("Flat 首条查询:", I[0], D[0])

查询直接使用前八条入库向量,所以第一近邻应是它自身;这是正确性检查,不是召回实验。如果返回 ID 为 -1,应先剔除,再拿结果索引业务数据,不能让 NumPy 将它解释为最后一行。

8.2 余弦检索

xb_cos, xq_cos = xb.copy(), xq.copy()
assert np.all(np.linalg.norm(xb_cos, axis=1) > 0)
assert np.all(np.linalg.norm(xq_cos, axis=1) > 0)
faiss.normalize_L2(xb_cos)
faiss.normalize_L2(xq_cos)
cosine = faiss.IndexFlatIP(d)
cosine.add(xb_cos)
scores, cosine_ids = cosine.search(xq_cos, k)
assert np.allclose(scores[:, 0], 1, atol=1e-5)

这里的 scores 是内积即余弦分数,越大越相似;不要沿用 L2 的“越小越好”。实际数据还应排除 NaN、Inf 和零向量。度量说明

8.3 IVF:train、add 与每次查询参数

quantizer = faiss.IndexFlatL2(d)
ivf = faiss.IndexIVFFlat(quantizer, d, 32, faiss.METRIC_L2)
assert not ivf.is_trained
ivf.train(xt)
assert ivf.is_trained and ivf.ntotal == 0
ivf.add(xb)
ivf.nprobe = 4
D_ivf, I_ivf = ivf.search(xq, k)

# 覆盖本次查询的 nprobe,不修改 ivf.nprobe。
params = faiss.SearchParametersIVF()
params.nprobe = 32
D_full, I_full = ivf.search(xq, k, params=params)
assert ivf.nprobe == 4
assert np.allclose(D_full, D, rtol=1e-4, atol=1e-4)

这个例子使用精确粗量化器,扫描全部 32 个桶且没有其他扫描上限,所以 IVFFlat 距离结果应与 Flat 一致(浮点容差和并列项排序需单独处理)。不能将这个结论照搬到 IVFPQ:扫描全部桶仍然存在 PQ 误差。IndexIVF.cpp

8.4 IVFPQ 与 index_factory

# d=32,切成 4 个子空间,每段 4 bits:每条 PQ 编码共 2 字节。
pq_index = faiss.index_factory(d, "IVF32,PQ4x4", faiss.METRIC_L2)
pq_index.train(xt)
pq_index.add(xb)
faiss.ParameterSpace().set_index_parameter(pq_index, "nprobe", 8)
D_pq, I_pq = pq_index.search(xq, k)
assert I_pq.shape == (8, k)
assert pq_index.code_size == 2

IVF32,PQ4x4 表示 32 个倒排桶、4 个 PQ 子空间、每段 4 bits。code_size 不包含每条 ID、码本及桶管理开销。PQ 维度通常要求能整除子空间数;压缩后不能要求自身距离严格为零。

另一个组合字符串 PCA16,IVF32,Flat 表示先降到 16 维,再建立 IVFFlat。实际返回对象可能是包装索引,直接写 index.nprobe = 8 可能只是在 Python 对象上增加一个无效属性;应使用 ParameterSpace 或取得真实子索引。index_factory.cppAutoTune.cpp

8.5 HNSW:建图参数与搜索参数

hnsw = faiss.IndexHNSWFlat(d, 16)
hnsw.hnsw.efConstruction = 80  # add 建图之前设置
hnsw.add(xb)
hnsw.hnsw.efSearch = 40
D_hnsw, I_hnsw = hnsw.search(xq, k)
assert I_hnsw.shape == (8, k)

这里的 16 是图连接参数,不是 PQ 子空间数。HNSWFlat 不需要训练码本,但 add 要构图,不能把 is_trained=True 理解成构建过程免费。该版本 HNSW 不提供常规 remove_ids 删除能力,套上 IDMap 也不会自动补齐底层删除算法。IndexHNSW.h

8.6 业务 ID、重建和删除

business_ids = np.arange(10000, 10000 + len(xb), dtype=np.int64)
mapped = faiss.IndexIDMap2(faiss.IndexFlatL2(d))
mapped.add_with_ids(xb, business_ids)
D_map, I_map = mapped.search(xq[:1], 1)
assert I_map[0, 0] == 10000
assert np.allclose(mapped.reconstruct(10000), xb[0])
removed = mapped.remove_ids(np.array([10000], dtype=np.int64))
assert removed == 1
assert mapped.ntotal == len(xb) - 1
assert mapped.search(xb[1:2], 1)[1][0, 0] == 10001

业务 ID 应由调用方保证唯一;Faiss 添加不是数据库式 upsert。裸 Flat 删除后顺序编号会移动,IDMap2 可以维持外部 ID 映射,但底层必须支持删除。IVF 的随机重建需要 ID 定位结构,下一例展示 Hashtable DirectMap。IndexIDMap.cppDirectMap.h

ivf_ids = faiss.IndexIVFFlat(faiss.IndexFlatL2(d), d, 32)
ivf_ids.set_direct_map_type(faiss.DirectMap.Hashtable)
ivf_ids.train(xt)
ivf_ids.add_with_ids(xb, business_ids)
assert np.allclose(ivf_ids.reconstruct(10007), xb[7])
assert ivf_ids.remove_ids(np.array([10007], dtype=np.int64)) == 1

DirectMap.Array 对 ID 有连续编号等约束,不能与任意业务 ID 的 Hashtable 方式混用;DirectMap 也会产生额外内存开销。

8.7 查询时按 ID 过滤

selector = faiss.IDSelectorRange(0, 100)  # 半开区间 [0, 100)
filtered_params = faiss.SearchParametersIVF()
filtered_params.nprobe = 32
filtered_params.sel = selector
D_filtered, I_filtered = ivf.search(xq, k, params=filtered_params)
valid = I_filtered[I_filtered >= 0]
assert np.all(valid < 100)

这里 ivf 使用默认顺序 ID,所以筛选的是前 100 条向量。过滤器作用在目标索引理解的 ID 空间,嵌套 IDMap 等封装时要重新核对语义。它不是 SQL 条件引擎:业务层需要先把元数据条件转成可用的 ID 选择方式。候选不足时仍可能返回 -1IDSelector.hIndexIVF.cpp

lims, range_D, range_I = flat.range_search(xq[:2], 1e-4)
assert len(lims) == 3
for q in range(2):
    start, end = int(lims[q]), int(lims[q + 1])
    assert q in range_I[start:end]
    print("range query", q, range_I[start:end])

L2 下筛选的是平方距离小于阈值的结果。第 q 个查询对应 [lims[q], lims[q+1]),不能按二维 top-k 数组读取。结果不应假定已按距离排序;内积范围检索的阈值方向也与 L2 不同。IndexFlat.cppPython 包装

8.9 PCA 与组合索引

pca_index = faiss.index_factory(d, "PCA16,IVF32,Flat")
pca_index.train(xt)
pca_index.add(xb)
faiss.ParameterSpace().set_index_parameter(pca_index, "nprobe", 4)
D_pca, I_pca = pca_index.search(xq, k)
assert pca_index.d == 32  # 对外仍然接收原始维度
assert I_pca.shape == (8, k)

IndexPreTransform 在训练、入库和查询时执行变换链。PCA 降维后,距离是在变换后的空间计算的,不能直接当作原始空间的精确 L2。OPQ 也是可组合的变换组件,但用途是配合量化优化表示,不能简单理解成 PCA 的同义词。IndexPreTransform.cppVectorTransform.h

8.10 保存、加载与克隆

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temp_dir:
    path = str(Path(temp_dir) / "example.faiss")
    faiss.write_index(flat, path)
    restored = faiss.read_index(path)
    D_restored, I_restored = restored.search(xq, k)
    assert np.array_equal(I_restored, I)
    assert np.allclose(D_restored, D)

serialized = faiss.serialize_index(flat)
restored_memory = faiss.deserialize_index(serialized)
cloned = faiss.clone_index(flat)
cloned.reset()
assert cloned.ntotal == 0 and flat.ntotal == len(xb)
assert restored_memory.ntotal == flat.ntotal

索引之外,业务应一起保存 embedding 模型版本、归一化配置、ID 映射和数据快照版本。只加载可信索引文件,读取接口不负责验证文件内容的安全性。GPU 索引需先转 CPU 再写盘。IO 文档

8.11 聚类与独立 PQ 编解码

Faiss 的组件也能脱离检索索引使用:

km = faiss.Kmeans(d, 16, niter=10, nredo=1, seed=42)
km.train(xt)
cluster_D, cluster_I = km.index.search(xb[:10], 1)
assert km.centroids.shape == (16, d)
assert cluster_I.shape == (10, 1)

pq = faiss.ProductQuantizer(d, 4, 4)
pq.train(xt)
codes = pq.compute_codes(xb[:10])
decoded = pq.decode(codes)
assert codes.shape == (10, 2)
assert decoded.shape == (10, d)
assert np.isfinite(decoded).all()
print("PQ reconstruction MSE:", np.mean((decoded - xb[:10]) ** 2))

Kmeans 是 C++ Clustering 的 Python 便利封装;ProductQuantizer 是编码器,不提供 Index.search 那样的完整向量库接口。解码结果是近似值。extra_wrappers.pyProductQuantizer.h

8.12 二进制索引是另一套输入约定

binary_data = np.array([[0, 0], [255, 255], [15, 240]], dtype=np.uint8)
binary = faiss.IndexBinaryFlat(16)  # 维度单位是 bit,每条占 2 字节
binary.add(binary_data)
binary_D, binary_I = binary.search(binary_data[:1], 3)
assert binary_I[0, 0] == 0 and binary_D[0, 0] == 0
assert binary_D[0, -1] == 16

这里比较 Hamming 距离,输入是打包后的 uint8 字节。不能将 float embedding 直接 astype(uint8) 就当成有意义的二进制特征;如何产生二进制表示是另一个问题。IndexBinaryFlat.h

9. C++ 接口对照

以下程序演示内存布局与输出分配;需要在已配置 Faiss 头文件及链接库的 C++ 工程中编译,本次未编译该示例。

#include <faiss/IndexFlat.h>
#include <cassert>
#include <vector>

int main() {
    constexpr int d = 2;
    constexpr faiss::idx_t nb = 3, nq = 1, k = 2;
    const float xb[] = {0.f, 0.f, 1.f, 0.f, 0.f, 2.f};
    const float xq[] = {0.1f, 0.f};
    faiss::IndexFlatL2 index(d);
    index.add(nb, xb);
    std::vector<float> distances(nq * k);
    std::vector<faiss::idx_t> labels(nq * k);
    index.search(nq, xq, k, distances.data(), labels.data());
    assert(labels[0] == 0);
    assert(labels[1] == 1);
}

C++ 默认不会替你检查 NumPy shape:指针后面的数据必须按行连续存放,并且数量、维度和分配空间正确。组合索引还要关注 own_fields、对象生命周期等字段;Python 包装层会维护部分引用,不能把同样假设直接用于手写 C++。Index.hPython 引用维护

10. GPU 源码与 API

10.1 目录分工

路径 职责
gpu/GpuIndex.* GPU 索引公共入口与主机/设备数据处理
gpu/GpuIndexFlat.*GpuIndexIVF* 具体 GPU 索引包装
gpu/StandardGpuResources.* 临时内存、流等资源管理
gpu/GpuCloner.*GpuClonerOptions.h CPU/GPU 转换与克隆选项
gpu/impl/FlatIndex.*IVFFlat.*IVFPQ.* 更底层的数据组织和计算调度
gpu/impl/Distance.*IVFFlatScan.*PQScan* 距离计算和倒排扫描实现
gpu/impl/Cuvs* cuVS 后端衔接
gpu/utils/ 设备工具、张量、选择操作等辅助
gpu/test/gpu/perf/ GPU 正确性与性能测试

普通 Flat GPU 路径可从 GpuIndexFlat::searchImpl_ 追到 data_->querygpu/impl/FlatIndex.cu。若启用不同后端,还要继续检查实际构建和分派路径。GPU 源码GpuIndexFlat.cu

10.2 转换示例

下面依赖 GPU 版 Faiss、兼容设备与运行时,未在本次 CPU 环境执行。继续使用第 8 节的数据。

# GPU_ONLY: 需要 GPU 版 Faiss 和兼容设备。
resources = faiss.StandardGpuResources()
cpu_index = faiss.IndexFlatL2(d)
gpu_index = faiss.index_cpu_to_gpu(resources, 0, cpu_index)
gpu_index.add(xb)
D_gpu, I_gpu = gpu_index.search(xq, k)
restored_cpu = faiss.index_gpu_to_cpu(gpu_index)
assert restored_cpu.ntotal == len(xb)

resources 应保持有效,避免每次请求反复创建资源对象。index_cpu_to_gpu 的第二个参数是设备号;CPU 与 GPU 对象不是自动同步的镜像。CPU NumPy 数组参与调用可能产生传输,应分别测量纯计算与端到端耗时。并非所有 CPU 索引都支持直接转换,不能假定 HNSW 等任意索引都能套用此例。GPU 使用说明

11. 按问题定位源码

遇到的问题 优先查看
Python 数组维度、dtype 或返回值不符合预期 python/class_wrappers.py
train 后没有数据 Index.h 中 train/add 的职责与具体索引状态
IVF 召回低 nprobe、粗量化训练、IndexIVF::search
IVFPQ 全扫描仍有误差 IndexIVFPQProductQuantizer 的量化路径
HNSW 构建慢或查询慢 efConstructionefSearchimpl/HNSW.cpp
ID 删除后映射错位 IndexFlatCodesIndexIDMapDirectMap
reconstruct 失败 索引是否支持、是否有 DirectMap、ID 空间是否正确
参数设置了却没效果 包装层、实际子索引类型、AutoTune.cpp
CPU 利用率异常 批大小、OpenMP/BLAS 配置、utils/distances.cpp
GPU 慢或显存不足 数据搬运、资源临时区、实际后端与扫描实现
索引无法保存/加载 impl/index_write.cppindex_read.cpp 与版本

这些是排查入口,不是对某个现象的唯一归因;应结合最小复现和 profiler 验证。

12. 推荐阅读顺序

  1. 运行 Flat 示例,搞清楚输入、输出、ID 和距离。
  2. 阅读 Index.h → IndexFlat.cpp → utils/distances.cpp,追通精确搜索。
  3. 阅读 IndexIVF.cpp → IndexIVFFlat.cpp → invlists/,理解选桶与扫描分工。
  4. 阅读 IndexIVFPQ.cpp → impl/ProductQuantizer.cpp,理解残差、编码和查表。
  5. 阅读 IndexHNSW.cpp → impl/HNSW.cpp → DistanceComputer.h,理解图与存储组合。
  6. 阅读 Python 包装、IDMap、PreTransform 和 IO,将算法接回工程接口。
  7. 有明确性能目标后再进入 GPU 和 SIMD 内核。

每一步建议记录:输入输出、拥有的数据结构、关键状态变化、调用链和可复现的小例子。这样形成的源码笔记比逐文件抄写函数名更便于后续修改和调试。