Faiss GPU检索短板与cuVS CAGRA组合方案实践

发布时间:2026/10/15 17:28:12
Faiss GPU检索短板与cuVS CAGRA组合方案实践
1. Faiss 的 GPU 检索为什么总差一口气做向量检索的团队绕不开 Faiss。这句话说了好几年目前依然是事实生态成熟、接口稳定、社区案例多、网上随便一搜就是一堆踩坑记录。但一旦你想把检索整个搬到 GPU 上提速问题就没有想象中那么简单了。我最近在一个百万级向量的召回项目里重新梳理了 Faiss 的 GPU 路径又对比了 cuVS 的 CAGRA 索引才把之前一直模糊的那条线彻底摸清楚。Faiss GPU 的能力覆盖其实很有限暴力检索IndexFlatL2有IVFFlat、IVFPQ 有但业内应用最广的 HNSW 在 GPU 上一直缺席。在目前主流的 Faiss 版本里HNSW 的构建和搜索都只有 CPU 实现。这意味着什么意味着如果你走的是图索引路线、又想用 GPU 加速官方路径基本是堵死的。很多团队的实际状态就是线上召回用 CPU 扛 HNSWGPU 只负责跑排序模型。显存资源天天等着模型调度CPU 却在召回这块累得半死两头都别扭。为什么会这样业内一般归因于两个层面。第一个是算法结构问题。IVF 这类索引把向量空间切成规整的桶查询时先定位桶再扫桶内数据这种粗筛 精扫的模式天然适合 GPU 并行。而 HNSW 的搜索是链式指针追踪先在一层找入口再往下层走每一步的邻居都是上一步的结果决定的。这种串行依赖和随机访存在 GPU 上会大量浪费线程改写出高性能 kernel 的难度极高。第二个是工程层面的问题。Faiss 的 GPU 资源管理是一套独立体系——显存池、拷贝流、页面锁定内存都要自己配真要压到生产环境调优成本一点也不低。这里就是 cuVS 切入的位置。它底层基于 RAFT 的 CUDA 原语把 CAGRA 这种专门为 GPU 设计的图索引做了出来建图和搜索都能全程跑在 GPU 上。与其说 cuVS 是 Faiss 的替代品不如说它是补 Faiss 短板的那块拼图Faiss 的生态照用cuVS 负责把 GPU 端的召回效率真正提上来。本文后面会从原理、组合方式、实测调参和工程坑四个角度把这条路线讲透。2. cuVS 的解法CAGRA 把图检索搬上 GPU2.1 CAGRA 为什么能在 GPU 上跑出高性能CAGRA 的全称是 CUsGrAph-based ANN名字里就点出了它的核心思想把图索引改造成适合 GPU 执行的形态。传统 HNSW 的问题在于每个节点的邻居数量不确定搜索路径高度不规则CAGRA 的做法是两阶段——先建图再剪枝。建图阶段默认使用 NN-descent 算法在 GPU 上通过多轮迭代逼近近似 KNN 图。这个阶段可以设置中间图度数intermediate_graph_degree让图先建得密一些保证候选邻居质量。随后进入剪枝阶段把每个节点的邻居统一剪到一个固定值 graph_degree同时尽量保留高质量邻居。正是因为出度固定、数据排布规整GPU 的访存效率才能拉满。搜索阶段走的是 beam search 路线从若干候选节点出发以 search_width 为宽度每跳并行访问固定数量的邻居。你可以把它理解成多路并行的贪心搜索每一条路都独立推进最后汇合打分。这种设计把图搜索里最耗时的随机指针追踪变成了一组可以并行执行的规整访存GPU 的 tensor core 和显存带宽都被充分利用起来。我实测下来单条查询延迟能压到 1 毫秒以内这在传统 CPU 图索引上很难做到。2.2 CAGRA 之外的 cuVS 全貌cuVS 不是只有 CAGRA 这一个索引。它同时提供 IVFFlat、IVFPQ、暴力搜索等经典实现统一 API并且支持 GPU 和 CPU 两种运行后端。也就是说你可以在 GPU 上完成最耗时的建图然后切换到 CPU 上用同一套接口做线上搜索这对部署阶段非常友好。除此之外cuVS 还支持多种精度存储FP32、FP16、INT8 量化都可以选显存不够的时候可以降精度换容量。索引本身也有序列化接口比如 HDF5 格式可以落盘方便跨机器复用。这些能力让它不完全是一个GPU 专用工具而是一个有完整工程链路的检索库。那 cuVS 和 Faiss 到底什么关系我的理解是它们不是谁取代谁的关系。Faiss 是全家桶评估指标、聚类、量化、序列化、索引工厂都很齐全CPU 端的工程经验非常丰富cuVS 则把 GPU 召回这个最核心的环节做得更彻底。实际项目里最常见的用法是两者配合各干各最擅长的事——这也是下一节要展开的重点。3. 三种组合方案与可运行代码3.1 方案 AcuVS 召回 Faiss 精排这是我在项目里最常用的结构。CAGRA 负责从全库里快速捞回一个较大的候选集Faiss 再用精确 L2 距离在这个候选集内排序。图索引再准也是近似精排能纠正最后一公里的误差把召回率从中等偏上拉到接近暴力检索的水平而耗时几乎可以忽略。代码大概是这个套路cuVS 的 Python 接口在 24.x 版本间有细节变动以你实际安装版本为准import numpy as np import cupy as cp import faiss from cuvs.neighbors import cagra d 128 n 1_000_000 k 100 topk_rerank 400 # 从CAGRA取400个候选再精排到100 data np.random.random((n, d)).astype(np.float32) dataset cp.asarray(data) index_params cagra.IndexParams( metricsqeuclidean, graph_degree64, build_algonn_descent, ) index cagra.build(index_params, dataset) queries np.random.random((10_000, d)).astype(np.float32) search_params cagra.SearchParams( itopk_sizetopk_rerank * 2, search_width4, ) _, neighbours cagra.search( search_params, index, cp.asarray(queries), ktopk_rerank ) # Faiss精排对每条query在候选向量里做精确最近邻 cand_ids neighbours.get() final_d np.empty((queries.shape[0], k), dtypenp.float32) final_i np.empty((queries.shape[0], k), dtypenp.int64) for qid in range(queries.shape[0]): cand_vecs data[cand_ids[qid]] D, I faiss.knn(queries[qid:qid 1], cand_vecs, k) final_d[qid] D[0] final_i[qid] cand_ids[qid][I[0]]这里有两个细节要提醒。第一CAGRA 搜索的 k 必须明显大于最终精排的 k留出足够的修剪空间否则精排就是在矮子里面拔高个。第二faiss.knn 的候选集只有几百条开销完全可以忽略但别把它用在全库上——那就变成暴力检索了失去了 CAGRA 的意义。我一般取 final_k 的 3 到 5 倍作为 CAGRA 的召回量效果最稳。3.2 方案 BcuVS 建图、CPU 端推理如果你的线上服务是纯 CPU 实例但离线环境有 GPU 可以用来建索引那么可以用 cuVS 在 GPU 上完成最耗时的图构建再落到 CPU 上检索。严格来说CAGRA 的索引格式和 Faiss 的 HNSW 格式不能直接互换目前可行的路径有两条。第一条是直接用 cuVS 的 CPU 搜索接口。构建端在 GPU 上完成 CAGRA序列化后传到部署机部署机用 cuVS 的 CPU 后端加载并搜索接口保持一致只是吞吐会低一些。第二条是手动做格式转换把 CAGRA 的邻接表导出再用邻接表去构建 Faiss 的 IndexHNSW。这个转换不难但要清楚一点——CAGRA 的图是剪过枝的、有向的重建出来的 HNSW 召回率会比原图略低需要在转换后重新评估指标。3.3 方案 C用 Faiss 做基准给 cuVS 做体检任何新引入的检索库都必须过基准测试。我的习惯是用 Faiss 的 IndexFlatL2 在 GPU 上跑暴力检索拿 ground truth然后计算 cuVS 的 Recallk。这样两个库在同一个数据、同一个查询集、同一个指标口径下对比结果才可信。gt_index faiss.IndexFlatL2(d) gt_index.add(data) _, gt_ids gt_index.search(queries, k) # 与cuVS/精排结果对比 recall 0.0 for qid in range(queries.shape[0]): recall len(set(gt_ids[qid]) set(final_i[qid])) / k print(Recall100:, recall / queries.shape[0])超过一千万条数据后暴力检索做 ground truth 会明显变慢。我的做法是把验证查询集缩小到五万条以内或者用抽样 人工核对的方式做近似验证。总之数字只有放在统一口径下才有意义这是所有对比工作的前提。4. 实测表现与调参过程记录4.1 一组能说明趋势的对照数据先声明这不是严谨的基准测试只是我在一套模拟环境里的相对记录。环境大致是CUDA 12.3单张 24GB 显存左右的专业级 GPU数据 100 万条 128 维 float32查询 1 万条。跑出来的结果如下方案建索引耗时索引显存搜索吞吐(QPS)平均延迟(ms)Recall10Faiss GPU IVFPQ (nlist4096, m32)约65秒约780MB约5.2万约1.90.861cuVS CAGRA (graph_degree64)约18秒约1.1GB约12.8万约0.780.942cuVS CAGRA Faiss精排约18秒约1.1GB约9.6万约1.050.983Faiss GPU 暴力检索不适用约512MB约2.1万约4.81.000几个值得细看的点。CAGRA 的建图明显快于 IVFPQ 的 train add原因是 NN-descent 在 GPU 上并行度很高而 IVFPQ 的聚类、量化、编码是串行环节多的流水线。吞吐方面CAGRA 比暴力检索有数量级优势对比 IVFPQ 也有接近翻倍的领先。精排方案牺牲了一部分吞吐换来了接近 1.0 的召回这正是很多线上场景最看重的指标。显存方面CAGRA 因为要额外存图结构确实比压缩后的 IVFPQ 高但 100 万条数据也才 1GB 出头完全可控。4.2 影响结果的四个参数排序按影响程度从大到小排第一是 search_width搜索宽度。它直接决定 beam search 每层考察的路径数越大召回越好延迟也越高。我的经验是从 4 起步依次试 2、4、8 三个档位观察 Recallk 的边际收益找到那个再往上加就不划算了的拐点。第二是 itopk_size这是搜索时的内部缓冲区大小。它必须大于最终返回的 k我通常设成目标 k 的 2 到 4 倍。设得不够召回会莫名其妙掉下去而且很难排查设得过大又白白增加中间排序的开销。第三是 graph_degree构建期的图质量参数。默认 64 比较均衡数据噪声大或者维度特别高时可以升到 96但建图时间和显存都会跟着涨。第四才是精度选择。float16 存储能省一半显存、速度也有提升。我在这批数据上测试转 float16 只掉了约 0.5 个百分点的召回因为数据分布比较均匀。但如果你的向量是长尾分布、或者存在明显的尺度差异建议先用 float32别为了省显存去赌。批量大小也要单独提一句CAGRA 单条查询延迟不高但吞吐依赖批量。我测下来 1024 到 4096 的批量最稳512 以下 GPU 占用明显不足。线上如果是单条查询、强延迟要求可以考虑加一层小批量缓冲把并发请求合并成一批再查效果立竿见影。5. 工程化路上容易踩的坑5.1 显存比你以为的多CAGRA 的显存占用由三部分组成原始向量、图结构、搜索中间缓冲区。图结构这块最容易漏算100 万节点、graph_degree 64每个节点存 64 个 int32 邻居就是 100万×64×4B大约 256MB。NN-descent 建图时还有中间图和高频访问的临时结构峰值甚至可能到最终索引的两三倍。所以数据规模到两千万条以上时我建议先拿十分之一的抽样试建一次把峰值显存测出来再决定精度档位和是否需要分片。5.2 两个库共驻的 CUDA 冲突同一个进程里同时用 Faiss GPU 和 cuVS最容易遇到的是显存池互相打架。Faiss 默认有自己的显存分配机制cuVS 底层走 RAFT 的 pooling 分配器两边各自预留显存就凭空多出一截。我的做法是Faiss 侧改成更保守的分配策略cuVS 侧把内存池的初始大小调到能覆盖 CAGRA 峰值。另外这两个库各自持有 CUDA context不要在多个线程里交叉创建、销毁索引否则会碰到奇怪的 cudaErrorMemoryAllocation只能重启进程恢复。5.3 评估口径不一致导致的误导这是最隐蔽的坑。Faiss 的 recall 计算默认是每个 query 返回的 k 个结果里有多少个出现在 ground truth 里但很多团队在对比时一个库用 Recall10、另一个用 Recall100数字看起来差距巨大其实是口径不一致。另外IVFPQ 返回的是量化后的距离CAGRA 返回的是原始向量的距离两者的绝对值不能直接对比。统一方法就是都换算成召回率并且固定同一份 ground truth别拿各库文档里的典型值做比较。还有一点建索引的随机种子和数据顺序都会影响结果对比前把数据 shuffle 成同一份多跑三次取中位数。5.4 版本对齐是个隐藏炸弹cuVS 的 Python 包和 Faiss 的 GPU 包各自捆绑了 CUDA 相关依赖如果安装时没注意版本很容易出现底层库冲突表现就是进程启动时报错、或者运行中随机崩溃。我的建议是在干净的虚拟环境里安装固定 CUDA 主版本faiss-gpu 和 cuvs 都选同一 CUDA 版本的 wheel。千万别在已有复杂依赖的项目里直接升级某一个库先建个隔离环境验证再考虑迁移。6. 我的选型建议最后说点实在的。如果你的数据量在几十万以内Faiss GPU 的暴力检索就够了完全没必要引入 cuVS如果数据在百万以上、GPU 资源又充足CAGRA 是我目前试过的方案里性价比最高的。具体到组合方式线上强延迟、高 QPS 的场景用纯 CAGRA召回要求苛刻的场景用 CAGRA Faiss 精排纯 CPU 部署的场景则考虑 cuVS 建图后做格式转换或直接用其 CPU 接口。工具选型没有标准答案关键在于把召回率、延迟、显存三个变量放在同一套评估体系里做决定。我个人的体会是与其等一个完美的统一框架不如先把两套库在各自最擅长的位置用好——Faiss 管稳健的生态和精排兜底cuVS 管 GPU 上的高吞吐召回两边的工程收益往往比一次伤筋动骨的框架迁移来得更快。