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.md、CHANGELOG.md |
安装要求和版本变化 | 切换环境或版本前 |
以上是职责摘要,完整文件列表见 v1.12.0 仓库。学习时建议先跑 tutorial,再追一个 search 调用,不必从头遍历所有文件。
3. faiss/ 下的主要文件
3.1 公共契约与基础索引
| 文件 | 作用 |
|---|---|
Index.h / Index.cpp |
浮点向量索引的基类、状态与默认行为 |
MetricType.h |
距离度量枚举,如 L2、内积 |
IndexFlatCodes.* |
固定长度编码数组的存储和基础操作 |
IndexFlat.* |
原始浮点向量的精确穷举检索 |
IndexPQ.* |
使用 PQ 编码的索引 |
IndexScalarQuantizer.* |
标量量化索引 |
IndexBinary.h、IndexBinaryFlat.* 等 |
独立的二进制向量索引体系 |
不要被 IndexFlatCodes 的名字误导:这里的 code 是固定长度的字节表示,不一定压缩。IndexFlat 也继承它,使用原始浮点数据对应的字节存储。IndexFlatCodes.h、IndexFlat.h
3.2 候选组织与索引组合
| 文件 | 作用 |
|---|---|
IndexIVF.* |
IVF 公共流程:训练粗量化器、分桶、查询选桶、调度扫描器 |
IndexIVFFlat.* |
桶内保存原始向量,直接计算距离 |
IndexIVFPQ.* |
桶内保存 PQ 编码,执行编码和距离估计 |
IndexHNSW.* |
将图结构和底层向量存储组合为索引 |
IndexPreTransform.* |
在索引前串接 PCA、OPQ 等变换 |
IndexIDMap.* |
内部位置与外部 ID 的映射 |
IndexRefine.* |
候选召回后进一步计算距离和排序 |
IndexShards.* |
聚合不同数据分片的检索结果 |
IndexReplicas.* |
管理同一份数据的多个副本,并分配查询 |
index_factory.* |
解析字符串,创建组合索引 |
AutoTune.* |
参数设置、候选参数空间与评估辅助 |
Clustering.*、VectorTransform.* |
聚类和向量变换 |
index_io.h、clone_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.cpp、index_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/ 并不是一个统一的后端,它包含多种被上层复用的组件。impl、invlists、utils
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.h、IndexIVFPQ.h、IndexHNSW.h、IndexPreTransform.h、IndexIDMap.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_c。search_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.py、Python 目录
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.cpp、distances.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
IndexIVFFlat 与 IndexIVFPQ 共享选桶框架,但扫描器解释 codes 的方式不同:前者使用原始浮点向量,后者使用 PQ 编码。search_preassigned 是已有粗分桶结果时的底层入口,普通用户通常调用 search 即可。IndexIVF.cpp、IndexIVFFlat.cpp、IndexIVFPQ.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.cpp、HNSW.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.h、class_wrappers.py、index_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.cpp、AutoTune.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.cpp、DirectMap.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 选择方式。候选不足时仍可能返回 -1。IDSelector.h、IndexIVF.cpp
8.8 range_search:结果是变长的
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.cpp、Python 包装
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.cpp、VectorTransform.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.py、ProductQuantizer.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.h、Python 引用维护
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_->query 与 gpu/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 全扫描仍有误差 | IndexIVFPQ 与 ProductQuantizer 的量化路径 |
| HNSW 构建慢或查询慢 | efConstruction、efSearch、impl/HNSW.cpp |
| ID 删除后映射错位 | IndexFlatCodes、IndexIDMap、DirectMap |
| reconstruct 失败 | 索引是否支持、是否有 DirectMap、ID 空间是否正确 |
| 参数设置了却没效果 | 包装层、实际子索引类型、AutoTune.cpp |
| CPU 利用率异常 | 批大小、OpenMP/BLAS 配置、utils/distances.cpp |
| GPU 慢或显存不足 | 数据搬运、资源临时区、实际后端与扫描实现 |
| 索引无法保存/加载 | impl/index_write.cpp、index_read.cpp 与版本 |
这些是排查入口,不是对某个现象的唯一归因;应结合最小复现和 profiler 验证。
12. 推荐阅读顺序
- 运行 Flat 示例,搞清楚输入、输出、ID 和距离。
- 阅读
Index.h → IndexFlat.cpp → utils/distances.cpp,追通精确搜索。 - 阅读
IndexIVF.cpp → IndexIVFFlat.cpp → invlists/,理解选桶与扫描分工。 - 阅读
IndexIVFPQ.cpp → impl/ProductQuantizer.cpp,理解残差、编码和查表。 - 阅读
IndexHNSW.cpp → impl/HNSW.cpp → DistanceComputer.h,理解图与存储组合。 - 阅读 Python 包装、IDMap、PreTransform 和 IO,将算法接回工程接口。
- 有明确性能目标后再进入 GPU 和 SIMD 内核。
每一步建议记录:输入输出、拥有的数据结构、关键状态变化、调用链和可复现的小例子。这样形成的源码笔记比逐文件抄写函数名更便于后续修改和调试。