当前位置: 首页 > news >正文

PipeANN:基于SSD的十亿级向量检索系统设计与实战

1. 项目概述:PipeANN,一个为SSD而生的向量检索系统

如果你正在处理十亿级别的向量数据,并且对检索延迟和内存消耗感到头疼,那么PipeANN这个名字你应该记住。这是一个来自学术界的开源项目,但它解决的问题非常实际:如何在有限的硬件资源(尤其是内存)下,实现接近内存级性能的超大规模向量近似最近邻搜索。

简单来说,PipeANN是一个完全运行在SSD上的图向量索引库。它的核心目标很明确:用SSD的价格,逼近内存索引的性能。传统的图索引如HNSW虽然快,但动辄需要数百GB甚至TB级别的内存来容纳十亿向量,成本高昂。而早期的磁盘索引方案如DiskANN,虽然解决了内存问题,但检索延迟(通常在几毫秒到几十毫秒)对于在线服务来说依然不够理想。PipeANN的出现,正是在这个夹缝中找到了一个绝佳的平衡点。

我花了一些时间深入研究它的代码和论文,发现它的设计哲学非常“系统”。它不是简单地对现有算法做优化,而是从底层重新思考了图搜索算法与SSD硬件特性(尤其是其异步I/O和并行能力)的匹配问题。其宣称的指标相当惊人:在十亿向量规模下,实现亚毫秒级延迟和超过2万QPS的吞吐量,同时内存占用仅为同规模内存索引的十分之一。这听起来有点“鱼与熊掌兼得”的味道,但背后的技术拆解下来,逻辑是自洽且精巧的。

接下来,我会带你深入PipeANN的内部,拆解它的核心设计、手把手演示如何从零构建和运行一个十亿级别的索引,并分享在实际测试中可能遇到的“坑”和应对技巧。无论你是正在为向量数据库选型的架构师,还是对高性能存储系统感兴趣的研究者,这篇文章都能给你带来一些直接的参考。

2. 核心设计思路:为什么是“管道”?

PipeANN性能提升的关键,在于其名字中的“Pipe”——管道。要理解这一点,我们需要先看看传统磁盘图搜索(如DiskANN)为什么慢。

2.1 传统磁盘图搜索的瓶颈:同步I/O与访存停顿

在内存图索引(如HNSW)中,进行近邻搜索时,算法会维护一个候选节点列表(通常是一个优先队列),并不断地从列表中取出距离查询点最近的节点进行扩展(访问其邻居节点)。这个过程是计算密集内存访问密集的,但速度极快,因为所有数据都在DRAM中。

当图索引被放在磁盘上时,问题来了。每次需要访问一个节点的向量数据及其邻居列表时,都必须发起一次磁盘I/O。传统的实现(如DiskANN的vamana搜索)采用同步阻塞I/O:线程发出读请求后,必须等待数据从SSD返回,才能进行下一步的距离计算和队列更新。这意味着CPU在大部分时间都在“空转”,等待I/O。尽管SSD的延迟已经很低(几十微秒),但对于一个需要访问数十甚至上百个节点的搜索过程来说,累积的等待时间就非常可观了,通常达到几毫秒。

此外,由于搜索路径的不可预测性(图遍历的随机性),I/O访问模式也是随机的,无法充分利用SSD的顺序读写优势。

2.2 PipeANN的破局之道:异步I/O与预取流水线

PipeANN的核心创新“PipeSearch”算法,正是为了解决上述问题。它将一次搜索分解为多个可以重叠执行的阶段,形成一个处理流水线(Pipeline)。其核心思想是将I/O等待时间与计算时间重叠起来

  1. 阶段分离:算法将搜索过程明确分为几个阶段:发起I/O请求(issue)、等待I/O完成(wait)、计算距离(compute)、更新候选队列(update)。在传统方法中,这些阶段是串行的。
  2. 异步与预取:PipeSearch使用Linux的io_uring接口进行异步I/O。搜索线程在计算出需要访问的下一个节点后,立即异步发起该节点数据的读取请求,然后不等待结果返回,继续处理当前已加载到内存中的其他节点。当之前发起的I/O完成后,数据早已在内存中准备好,计算线程可以直接使用。
  3. 批量处理:算法会维护一个“进行中的I/O请求”列表。当有多个I/O请求完成时,可以批量取回数据进行计算,减少了系统调用的开销。

这个过程就像一条工厂流水线:当工位A(计算单元)在处理零件N时,工位B(I/O单元)已经在为零件N+1取料了。通过这种方式,CPU计算和SSD I/O得以高度并行,极大地隐藏了I/O延迟。

实操心得:io_uring是关键PipeANN的性能严重依赖于io_uring。在Ubuntu 20.04及以上版本的内核中,io_uring已经相当成熟。如果你的系统不支持或未启用io_uring,PipeANN会回退到libaio,性能会有显著下降。在部署生产环境前,务必确认内核版本并检查io_uring的支持情况。可以通过grep io_uring /proc/kallsyms | head -5来简单判断。

2.3 存储布局优化:对齐与打包

光有异步I/O还不够,数据在磁盘上的布局也必须配合。PipeANN对索引文件的存储格式做了精心设计:

  • 节点记录对齐:每个图节点(包含向量数据、邻居ID列表等)的存储记录被填充和对齐到SSD页面大小(通常是4KB)的整数倍。这确保了每次I/O读取的都是完整的、对齐的数据块,避免了读取放大(读取不需要的数据)或读取不足(需要多次I/O才能读全一个节点)。
  • 紧凑打包:在记录内部,数据被紧凑排列。为了支持过滤搜索,标签(Label)信息也被直接附加在节点记录末尾。这种布局使得一次read系统调用就能获取到一个节点进行搜索所需的全部信息,最大限度地减少了I/O次数。

这种硬件感知的存储设计,是PipeANN能达到高吞吐量的另一个基石。它减少了无效的数据传输,让每一次I/O都物尽其用。

3. 从零开始:构建与搜索十亿向量索引实战

理论讲完了,我们上手实操。这里我以SIFT1B数据集(10亿条128维向量)为例,演示如何使用PipeANN构建索引并进行搜索。这个过程对硬件有一定要求,建议在拥有大容量NVMe SSD和足够内存的服务器上进行。

3.1 环境准备与依赖安装

首先,确保你的系统满足要求。我推荐使用Ubuntu 22.04 LTS。

# 1. 安装系统依赖 sudo apt update sudo apt install -y make cmake g++ libaio-dev libgoogle-perftools-dev \ clang-format libmkl-full-dev libeigen3-dev # 2. 安装Python绑定依赖(如果需要) pip3 install "pybind11[global]"

注意事项:BLAS库的选择项目默认依赖Intel MKL (libmkl-full-dev) 进行高效的向量距离计算。如果你的机器是AMD CPU或者不想安装MKL,可以替换为OpenBLAS。安装OpenBLAS后,需要在编译时通过CMake参数指定BLAS库的路径。不过根据我的测试,MKL在Intel CPU上的性能通常有5-10%的优势。

3.2 获取与编译PipeANN

# 1. 克隆代码库 git clone https://github.com/thustorage/PipeANN.git cd PipeANN # 2. 编译第三方库 liburing cd third_party/liburing ./configure && make -j$(nproc) cd ../.. # 3. 编译PipeANN核心库 # 如果你想使用Python接口 python setup.py install # 或者,为了获得最佳性能,我强烈建议编译C++版本 bash ./build.sh

编译完成后,所有可执行文件(如build_disk_index,search_disk_index)都会位于build/tests/目录下。

3.3 数据集准备与格式转换

PipeANN使用的数据格式是简单的二进制格式。你需要将公开数据集(如SIFT1B的.bvecs格式)进行转换。

# 假设你已经下载了SIFT1B的基础数据文件 bigann_base.bvecs # 将其转换为PipeANN的.bin格式 ./build/tests/utils/vecs_to_bin uint8 bigann_base.bvecs sift1b.bin # 同样,转换查询集和Ground Truth文件 ./build/tests/utils/vecs_to_bin uint8 bigann_query.bvecs query.bin ./build/tests/utils/vecs_to_bin int32 idx_1000M.ibin gt_1b.bin # Ground truth通常是.ivecs或.ibin格式

这个.bin格式的布局是:文件开头是4字节的向量数量(int),接着是4字节的向量维度(int),然后是所有向量的原始数据按行优先顺序平铺。

踩坑记录:数据集版本与Ground Truth匹配不同来源的SIFT1B数据集可能有细微差别(如是否包含归一化)。务必确保你使用的查询集和Ground Truth文件与你的基础数据集版本完全匹配。一个常见的错误是用了A数据集的查询去查B数据集构建的索引,或者用错了Ground Truth,导致测出的召回率毫无意义。建议从Big-ANN-Benchmarks官网提供的统一链接下载全套数据。

3.4 构建十亿级磁盘索引

这是最耗时的一步。对于SIFT1B,我们需要约500GB的临时内存和近1TB的SSD空间。

# 在拥有大容量内存和SSD的机器上执行 # 参数解释: # uint8: 数据类型(SIFT是uint8) # sift1b.bin: 输入数据文件 # sift1b_index: 索引文件前缀(最终会生成 sift1b_index_disk.index 等) # 128: R参数(最大出度) # 200: L参数(构建时的候选池大小) # 32: PQ_bytes(乘积量化字节数,影响精度和索引大小) # 500: M_GB(构建过程最大内存使用,单位GB) # 112: 使用的线程数 # l2: 距离度量(L2距离) # pq: 邻居列表编码类型(乘积量化,支持更新) ./build/tests/build_disk_index uint8 sift1b.bin sift1b_index 128 200 32 500 112 l2 pq

这个过程可能会运行数小时到一整天,具体取决于你的CPU和SSD速度。控制台会输出进度信息。关键的参数是RLPQ_bytes

  • R (Max out-degree): 控制图的连通性。越大,图越稠密,搜索路径更短,但索引文件也更大,I/O量增加。对于十亿数据,128是一个经验值。
  • L (Candidate list size): 构建图时每一步考虑的候选节点数。越大,构建的图质量越高,但构建时间越长。
  • PQ_bytes: 这是PipeANN节省空间的秘诀。它将原始的128维uint8向量压缩到32字节。这会引入误差,但通过精心设计的检索算法,依然能保证高召回率。如果发现召回率不足,可以尝试增加到48或64。

3.5 构建内存入口图(可选但推荐)

为了加速搜索,PipeANN支持构建一个小的、常驻内存的“入口图”。这个图是原始大图的一个采样子图,用于快速找到搜索的起始点。

# 首先,从数据集中随机采样1%的点 ./build/tests/utils/gen_random_slice uint8 sift1b.bin sift1b_index_SAMPLE_RATE_0.01 0.01 # 这会生成两个文件:sift1b_index_SAMPLE_RATE_0.01_data.bin 和 sift1b_index_SAMPLE_RATE_0.01_ids.bin # 然后,基于这1%的采样点构建内存图 ./build/tests/build_memory_index uint8 sift1b_index_SAMPLE_RATE_0.01_data.bin \ sift1b_index_SAMPLE_RATE_0.01_ids.bin sift1b_index_mem.index 32 64 1.2 $(nproc) l2

这个内存索引很小(对于十亿数据,大约几百MB),但能显著减少搜索时需要访问的磁盘节点数,从而降低延迟。

3.6 执行搜索测试

现在,我们可以用查询集来测试索引的性能了。

# 参数解释: # uint8: 数据类型 # sift1b_index: 索引前缀 # 1: 搜索线程数(单线程测延迟) # 32: Beam width(搜索宽度,影响搜索质量) # query.bin: 查询集文件 # gt_1b.bin: Ground truth文件 # 10: 返回的最近邻数量(Top-K) # l2: 距离度量 # pq: 邻居列表编码类型 # 2: 搜索模式(2代表PipeANN的PipeSearch算法) # 10: 内存索引的搜索列表大小(L_mem),如果为0则不使用内存索引 # 10 20 30 40: 要测试的磁盘搜索列表大小(L_disk)序列 ./build/tests/search_disk_index uint8 sift1b_index 1 32 query.bin gt_1b.bin 10 l2 pq 2 10 10 20 30 40

运行后,你会看到类似下面的输出表格:

L I/O Width QPS Mean Lat P99 Lat Mean Hops Mean IOs Recall@10 ========================================================================================= 10 32 1952.03 490.99 3346.00 0.00 22.28 67.11 20 32 1717.53 547.84 1093.00 0.00 31.11 84.53 30 32 1538.67 608.31 1231.00 0.00 41.02 91.04 40 32 1420.46 655.24 1270.00 0.00 52.50 94.23

解读结果

  • L: 磁盘搜索列表大小。越大,搜索越精细,召回率越高,但延迟也增加。
  • QPS: 每秒查询数。单线程下能达到近2000 QPS,性能可观。
  • Mean Lat: 平均延迟(微秒)。L=40时约为0.66毫秒,达到了亚毫秒级的目标。
  • P99 Lat: 99分位延迟。这比平均延迟更重要,它反映了长尾延迟。L=40时约为1.27毫秒,表现稳定。
  • Recall@10: 召回率。L=40时达到94.23%,对于十亿规模来说是非常好的结果。
  • Mean IOs: 平均每次查询的I/O次数。这是PipeANN效率的直观体现,L=40时仅需约52次I/O。

你可以调整L的值,在延迟和召回率之间做权衡。对于大多数应用,L在30-50之间能取得很好的平衡。

4. 高级特性解析:动态更新与过滤搜索

PipeANN不仅仅是一个静态索引,它还支持动态更新(增删)和复杂的过滤搜索,这在实际系统中至关重要。

4.1 向量更新:OdinANN的直接插入算法

传统的磁盘图索引(如DiskANN)更新成本极高,通常需要重建整个图或大片区域。PipeANN集成了其姊妹篇OdinANN的算法,支持高效的直接插入和删除。

核心思想:当新向量插入时,算法会为其在图中找到一个合适的局部位置进行连接,并最小化对已有图结构的扰动。它通过一个巧妙的“代理节点”机制和局部图重建来实现,确保插入操作不会导致整体搜索性能的剧烈波动(论文中显示波动仅在1.07倍左右)。

启用更新功能:在编译时,你需要确保CMakeLists.txt没有定义-DREAD_ONLY_TESTS-DNO_MAPPING标志。然后使用对应的测试程序。

# 1. 为数据生成标签文件(建立向量ID到内部标签的映射) ./build/tests/utils/gen_tags uint8 data_200M.bin my_index_prefix # 2. 运行插入-搜索混合负载测试 # 此测试会模拟一边插入新向量,一边进行搜索查询的场景 ./build/tests/test_insert_search uint8 data_200M.bin 128 1000000 100 10 32 2 \ my_index_prefix query.bin /path/to/ground_truth_dir 0 10 4 32 10 20 30 40 50

重要警告:崩溃一致性PipeANN的更新操作不是崩溃一致的。这意味着如果在插入或删除过程中系统崩溃,索引文件可能会损坏。生产环境使用更新功能时,必须考虑这一点。论文中提到,可以通过定期执行final_merge操作来创建一个持久化的、一致的索引快照。在关键业务中,更稳妥的做法是在一个临时副本上执行更新,完成后原子性地替换线上索引。

4.2 过滤搜索:支持任意元数据过滤

在许多实际场景中,我们不仅需要找相似的向量,还需要满足某些元数据条件,例如“寻找与这张图片相似,且发布时间在2023年之后、来自欧洲用户的图片”。PipeANN通过LabelSelector抽象优雅地支持了这一点。

工作原理

  1. 标签附着:在构建索引时,每个向量可以关联一个或多个标签(例如,分类ID、数值范围、位图等)。这些标签数据被直接存储在磁盘上该向量节点的记录末尾。
  2. 选择器定义:定义一个Selector(选择器),它的核心是一个is_member(query_label, target_label, target_id)函数。对于每个在搜索路径上访问到的节点,都会调用此函数判断其标签是否满足查询条件。
  3. 搜索时过滤:在标准的图搜索过程中,只有通过Selector检查的节点,才会被放入候选队列并进行距离计算。不满足条件的节点会被直接跳过。

使用示例:假设我们有一个图片数据集,每个图片有多个分类标签(如“猫”、“户外”、“夜景”),我们想搜索“猫”的相似图片。

# 1. 构建带标签的索引 # 假设 labels.spmat 是一个稀疏矩阵格式的标签文件 ./build/tests/build_disk_index uint8 image_vectors.bin image_index 96 128 32 256 112 l2 pq spmat labels.spmat # 2. 执行过滤搜索 # 假设 query_labels.spmat 包含了每个查询对应的标签(例如,每个查询只想找“猫”) # subset 选择器表示:目标节点的标签必须是查询标签的子集 ./build/tests/search_disk_index_filtered uint8 image_index 16 32 query.bin gt.bin 10 l2 pq \ subset query_labels.spmat 0 0 20 50 100 200

这个功能非常强大,它将向量检索与结构化过滤在索引层深度融合,避免了先检索再过滤带来的性能损失和精度下降。

5. 性能调优与故障排查指南

在实际部署中,你可能会遇到性能未达预期或各种运行时错误。以下是我总结的一些关键调优点和排查步骤。

5.1 性能调优参数表

参数类别参数名影响调优建议
索引构建R(Max out-degree)图密度,影响搜索路径长度和索引大小。十亿级:128;百万级:64-96。越大召回越高,但IO增多。
L(Build candidate list)图构建质量,影响最终索引的搜索精度。通常设为R的1.5-2倍。十亿级:200。
PQ_bytes向量压缩率,影响索引大小和精度。默认32。召回率不足时增至48或64。内存紧张时可减至16。
搜索性能L_disk(Search list size)搜索精细度,平衡延迟与召回。最重要的参数。从20开始测试,逐步增加直到召回率满足要求。通常30-50是甜点区。
beam_width搜索宽度,影响搜索的贪婪程度。默认32即可。在复杂图或高召回要求下可增至64。
search_mode搜索算法。务必使用模式2 (PipeSearch)以获得最佳性能。
系统资源线程数 (threads)并行度。构建时可用满核心数。搜索时,QPS随线程数线性增长,但延迟可能略增。根据业务需求(高吞吐/低延迟)调整。
I/O引擎底层I/O接口。首选io_uring。确保内核>=5.1,并在编译时正确链接liburing
硬件相关SSD性能直接影响IOPS和延迟。使用高性能NVMe SSD(如Intel P5800X, Samsung PM9A3)。避免使用SATA SSD或HDD。
内存容量影响构建和缓存。构建十亿索引需~500GB。纯搜索需~40GB。确保有足够空闲内存,避免Swap。

5.2 常见问题与解决方案

问题1:构建索引时因内存不足被杀死(OOM Killer)。

  • 现象build_disk_index进程突然消失,dmesg显示Out of memory: Killed process ...
  • 原因:参数M_GB设置过大,超过了系统可用物理内存。
  • 解决
    1. 使用free -h确认可用内存。
    2. M_GB设置为略小于可用内存的值(例如,留出10-20GB给系统)。
    3. 如果数据集极大,考虑使用PiPNN构建算法(build_pipnn_index),它对内存峰值的要求可能不同。

问题2:搜索召回率(Recall)远低于论文报告值。

  • 现象:在相同L参数下,自己测试的召回率比论文中低很多。
  • 排查步骤
    1. 检查数据一致性:确认查询集、Ground Truth与基础数据集完全匹配。这是最常见的原因。
    2. 检查距离度量:确认构建索引 (build_disk_index) 和搜索 (search_disk_index) 时使用的metric参数一致(都是l2或都是ip)。
    3. 调整PQ_bytes:如果向量本身信息密度高(如DEEP的96维浮点数),32字节压缩可能损失过多信息。尝试将PQ_bytes增加到48或64重新构建索引。
    4. 增加L_disk:这是最直接的提升召回率的方法,但会增加延迟。逐步提高L_disk(如40, 60, 80),观察召回率变化曲线。
    5. 检查索引构建质量:确保构建时的RL参数足够大。对于十亿数据,R=128, L=200是推荐的起点。

问题3:搜索延迟(Latency)过高,达不到亚毫秒级。

  • 现象:平均延迟在几毫秒甚至更高。
  • 排查步骤
    1. 确认使用io_uring:运行搜索程序时,观察初始日志。如果看到Using libaio for I/O,说明io_uring未启用。重新检查编译环境和系统内核。
    2. 检查SSD状态:使用fioioping测试SSD的4KB随机读延迟。如果延迟高于100微秒,可能是SSD性能瓶颈或负载过高。
    3. 降低L_disk:这是降低延迟最有效的方法,但会牺牲召回率。需要在业务可接受的召回率下限内寻找最小的L_disk
    4. 检查系统负载:使用iostat -x 1查看%utilawait。如果SSD利用率持续接近100%,说明存在其他I/O密集型任务干扰。
    5. 使用内存入口图:确保在搜索命令中设置了mem_L参数(如10),这能有效减少初始搜索阶段的磁盘I/O。

问题4:编译错误,找不到liburingMKL

  • 解决
    • 对于liburing:确保已成功编译third_party/liburing,并且其头文件(liburing.h)和库文件(liburing.a.so)在标准搜索路径或CMake指定的路径中。
    • 对于MKL:如果不想安装庞大的Intel MKL,可以修改CMakeLists.txt,将find_package(MKL)注释掉,并链接OpenBLAS。需要提前安装libopenblas-dev

问题5:更新操作后,索引文件损坏或无法加载。

  • 原因:如前所述,更新操作非崩溃一致。
  • 解决
    1. 定期快照:在业务低峰期,调用索引的save方法(Python接口)或使用工具创建一致性快照。
    2. 写时复制:在独立的数据副本上进行更新操作。完成一批更新后,将旧索引原子性地替换为新索引。这需要额外的存储空间和更复杂的版本管理。
    3. 日志与恢复:对于关键系统,可以考虑在应用层记录更新操作日志。如果索引损坏,可以从上一个完好快照开始,重放日志进行恢复。PipeANN本身不提供此机制,需要自行实现。

经过以上步骤的调优和排查,你应该能让PipeANN在你的硬件和数据上发挥出接近其论文宣称的性能。这个系统的优势在于,它提供了一套从构建、搜索到更新的完整工具链,并且代码结构清晰,便于深入研究和定制化开发。对于需要处理超大规模向量数据,同时又对成本和延迟有严格要求的团队来说,PipeANN无疑是一个值得投入精力研究和使用的强大工具。

http://www.jsqmd.com/news/813774/

相关文章:

  • 江苏母线槽哪家好用,性价比高 - mypinpai
  • 淘宝淘金币自动化脚本终极指南:每天节省25分钟的高效解决方案
  • Claude code热门快捷指令清单
  • 月薪3000和年薪百万,差距凭什么这么大?行业“薪资金字塔”大揭秘!
  • Go语言错误重试机制深度解析:openclaw-nerve库实战指南
  • 2026年养森泡泡乐套装价格多少钱? - 工业品牌热点
  • SIP子系统:SoC设计效率与成本优化的关键技术
  • 通过Taotoken管理多个项目API Key与设置访问权限
  • 大语言模型结构化剪枝:Týr-the-Pruner方法解析
  • 喷墨设备怎么选?2026年UV喷码技术深度评测与选购指南
  • 收藏 | 从零开始学大模型:6个月完整开发路线图(附免费资源)
  • 6家头部企业抢人,薪资20-60K,AI行业
  • 软件测试的3条黄金赛道:选对少走3年弯路
  • 给 Agent 配一个浏览器:Cloudflare Browser Run 全面解析
  • 在线图片处理工具源码, 多功能编辑格式转换HTML单文件版
  • 写给刚入行的测试新人:别急着学自动化,先把这件事做好
  • 为 OpenClaw 配置 Taotoken 以驱动你的 AI 智能体工作流
  • 青少年抑郁焦虑干预平台怎么选?7大维度对比指南
  • 盖茨 Micro-V® 多楔带:洗衣机 干衣机行业的高效静音传动标杆
  • 基于Petals分布式网络的大语言模型聊天应用后端部署与API调用实战
  • 如何用开源视频字幕工具VideoSrt在3分钟内完成专业字幕制作
  • 别再熬夜改答辩 PPT 了!okbiye AI PPT,4 步搞定学术演示稿(附保姆级操作指南)
  • dial9:给 Tokio 装上“飞行记录仪“
  • Gemini CLI:命令行集成AI,提升开发效率与自动化实践
  • 3步解决macOS下DistroAV NDI插件安装配置难题
  • Snowflake Postgres、Lakebase、HorizonDB 登场,如何选“锁定”方案?
  • 破局者:国产恒温恒湿试验箱的硬核逆袭
  • ARM -- 电源管理整理(一)
  • 从“左撇子困境”看包容性设计:打破设计偏见,提升产品普适性
  • FastAPI多智能体开发:AI团队自动化后端工程实践