🚀 免费试用 Zilliz Cloud,完全托管的 Milvus,体验 10 倍的性能提升!立即试用>

milvus-logo
LFAI
  • Home
  • Blog
  • DiskANN,基于磁盘的 ANNS 解决方案,在十亿规模数据集上实现高召回率和高 QPS

DiskANN,基于磁盘的 ANNS 解决方案,在十亿规模数据集上实现高召回率和高 QPS

  • Engineering
September 24, 2021
Zilliz

李成明,Zilliz 研发工程师,毕业于东南大学,获计算机科学硕士学位。他目前主要研究高维数据的 ANNS 问题,包括基于图和基于量化的解决方案。

"DiskANN:单节点上的快速精确十亿点近邻搜索 "是2019年发表在NeurIPS上的一篇论文。论文介绍了一种最先进的方法,即使用一台仅有 64GB 内存和足够大固态硬盘的机器,在十亿规模数据集上执行索引构建和搜索。此外,它还满足了大规模数据集上 ANNS(近似近邻搜索)的三个要求:高召回率、低延迟和高密度(单机节点数)。该方法在十亿规模的数据集 SIFT-1B 上建立了基于图的索引,单机内存为 64GB,CPU 为 16 核,达到了 5000 QPS(每秒查询次数),召回率超过 95%@1,平均延迟低于 3ms。

作者

Suhas Jayaram Subramanya:微软印度研究院前雇员,CMU 博士研究生。主要研究方向为高性能计算和大规模数据的机器学习算法。

Devvrit:德克萨斯大学奥斯汀分校研究生研究助理。他的研究兴趣是理论计算机科学、机器学习和深度学习。

罗汉-卡德科迪Rohan Kadekodi):德克萨斯大学博士生。他的研究方向是系统和存储,主要包括持久存储、文件系统和 kV 存储。

拉维尚卡尔-克里沙斯瓦米(Ravishankar Krishaswamy):微软印度研究院首席研究员。CMU 博士。研究方向为基于图和聚类的近似算法。

Harsha Vardhan Simhadri:微软印度研究院首席研究员。CMU 博士。过去研究并行算法和运行时系统。现在,他的主要工作是开发新算法和编写编程模型。

动机

大多数主流 ANNS 算法都要在索引构建性能、搜索性能和召回率之间做出权衡。基于图的算法(如 HNSW 和 NSG)在搜索性能和召回率方面是目前最先进的方法。由于基于内存驻留图的索引方法占用内存过多,因此在内存资源有限的情况下,使用单机对大规模数据集进行索引和搜索相对困难。

许多应用都需要基于欧氏距离的 ANNS 在十亿规模的数据集上快速响应。以下是两种主要的解决方案:

  1. 反向索引 + 量化:将数据集聚成 M 个分区,并使用 PQ(乘积量化)等量化方案压缩数据集。由于数据压缩会造成精度损失,因此这种解决方案的召回率较低。增加 topk 有助于提高召回率,而 QPS 会相应下降。
  2. 分割和索引:将数据集分割成多个互不关联的碎片,并为每个碎片建立一个内存索引。当有查询请求时,将在每个分片的索引上进行搜索,搜索结果将在合并后返回。这种解决方案会导致数据集规模过度膨胀,从而需要更多的机器,因为单台机器的内存资源有限,导致 QPS 较低。

上述两种解决方案都受限于单台机器的内存限制。本文提出设计一种 SSD 驻留索引机制来解决这一问题。SSD 驻留索引的挑战在于减少随机磁盘访问次数和磁盘访问请求次数。

贡献

本文提出了一种名为 DiskANN 的 SSD 驻留 ANNS 方案,它能有效支持大规模数据集的搜索。该方案基于本文提出的基于图的算法:Vamana。本文的贡献包括

  1. DiskANN 可以在一台拥有 64GB 内存的机器上索引和搜索超过 100 维的十亿规模数据集,提供超过 95% 的召回率@1,延迟时间低于 5 毫秒。
  2. 为了最大限度地减少磁盘访问次数,我们提出了一种名为 Vamana 的基于图的新算法,其搜索半径小于 NSG 和 HNSW。
  3. Vamana 可以在内存中运行,其性能并不比 NSG 和 HNSW 慢。
  4. 在大型数据集的重叠分区上建立的较小的 Vamana 索引可以合并成一个图,而不会失去连接性。
  5. Vamana 可以与 PQ 等量化方案相结合。图结构和原始数据存储在磁盘上,而压缩数据保存在内存中。

Vamana

这种算法与 NSG[2][4] 的思想相似(不了解 NSG 的读者请参考参考文献 [2],不想看论文的读者可以参考参考文献 [4])。它们的主要区别在于修剪策略。准确地说,NSG 的微调策略中增加了一个开关 alpha。NSG 修剪策略的主要思想是,目标点的邻居选择要尽可能多样化。如果新邻居比目标点更接近目标点的邻居,我们就不需要将此点添加到邻居点集合中。换句话说,对于目标点的每个邻居,周围半径 dist(目标点,邻居点)范围内不能有其他邻居点。这种修剪策略能有效控制图的外度,而且相对激进。它减少了索引的内存占用,提高了搜索速度,但也降低了搜索精度。Vamana 的修剪策略是通过参数 alpha 自由控制修剪规模。其工作原理是将修剪条件中的 dist(邻接点、候选点)与参数 alpha(不小于 1)相乘。只有当 dist(目标点,某个候选点)大于放大的参考距离时,才会采用修剪策略,增加目标点邻近点之间相互排斥的容忍度。

Vamana 的索引过程相对简单:

  1. 初始化一个随机图形;
  2. 计算起点,起点类似于 NSG 的导航点。首先找到全局中心点,然后找到最靠近全局中心点的点作为导航点。Vamana 与 NSG 的区别在于,NSG 的输入已经是一个近邻图,因此用户只需直接在初始邻图上对中心点进行近似近邻搜索即可。然而,Vamana 初始化的是随机近邻图,因此用户无法直接在随机图上进行近似搜索。他们需要进行全局比较,以获得一个导航点作为后续迭代的起点。该点的目的是最小化平均搜索半径;
  3. 根据初始化的随机邻居图和步骤 2 中确定的搜索起点,对每个点执行近似近邻搜索,将搜索路径上的所有点作为候选邻居集,并使用 alpha = 1 执行修边策略。与 NSG 类似,选择从导航点开始的搜索路径上的点集作为候选邻居集会增加一些长边,并有效缩小搜索半径。
  4. 调整 alpha > 1(本文推荐 1.2)并重复步骤 3。步骤 3 基于随机近邻图,第一次迭代后图的质量较低。因此,需要再次迭代以提高图的质量,这对召回率非常重要。

本文比较了三种图索引,即 Vamana、NSG 和 HNSW。在索引和查询性能方面,Vamana 和 NSG 比较接近,都略微优于 HNSW。有关数据,请参阅下面的实验部分。

2.png 2.png

为了直观地展示 Vamana 索引的构建过程,论文提供了一张图,其中使用 200 个二维点模拟了两轮迭代。第一行使用 alpha = 1 来修剪边缘。可以看出,修剪策略相对激进,大量的边被修剪。在增加 alpha 值并放宽修剪条件后,明显又增加了大量的边。在最终图中,增加了不少长边。这可以有效缩小搜索半径。

磁盘ANN

一台只有 64GB 内存的个人电脑甚至无法容纳十亿条原始数据,更不用说在这些数据上建立索引了。我们面临着两个挑战:1.如何利用有限的内存资源为如此大规模的数据集建立索引?2.如果原始数据无法加载到内存中,搜索时如何计算距离?

论文提出了以下解决方案:

  1. 对于第一个难题:首先,使用 k-means 将数据划分为 k 个簇,然后将每个点分配到最近的 i 个簇。一般来说,i 的数量为 2 就足够了。为每个簇建立一个基于内存的 Vamana 索引,最后将 k 个 Vamana 索引合并为一个。
  2. 对于第二个挑战:在原始向量上建立索引,并查询压缩向量。在原始向量上建立索引可以确保图的质量,而压缩向量可以加载到内存中进行粗粒度搜索。虽然使用压缩向量进行搜索可能会导致精确度下降,但只要图的质量足够高,大方向就会是正确的。最终的距离结果将使用原始向量计算。

DiskANN 的索引布局与一般图形索引的布局类似。每个点的邻居集和原始向量数据被存储在一起。这样可以更好地利用数据的位置性。

如前所述,如果索引数据存储在固态硬盘上,则必须尽可能减少磁盘访问次数和磁盘读写请求,以确保较低的搜索延迟。因此,DiskANN 提出了两种优化策略:

  1. 缓存热点:缓存内存中从起点开始 C 跳以内的所有点。C 的值最好设置在 3 到 4 之间。
  2. 波束搜索:简单地说,就是预先加载邻居信息。搜索点 p 时,如果 p 的邻居点不在内存中,则需要从磁盘加载。由于少量 SSD 随机存取操作与 SSD 单扇区存取操作所需的时间差不多,因此一次可以加载 W 个未存取点的邻居信息。W 不能设置得过大或过小。W 过大会浪费计算资源和固态硬盘带宽,过小则会增加搜索延迟。

实验

实验包括三组:

基于内存的索引之间的比较:Vamana VS.NSG VS.HNSW

数据集:SIFT1M (128 维)、GIST1M (960 维)、DEEP1M (96 维) 和从 DEEP1B 随机抽样的 1M 数据集。

索引参数(所有数据集使用同一组参数):

HNSW:M = 128,efc = 512。

瓦马纳R=70,L=75,α=1.2。

NSG:R=60,L=70,C=500。

论文中没有提供搜索参数,这可能与索引参数一致。在参数选择方面,文中提到的 NSG 参数是基于 NSG GitHub 存储库中列出的参数,以选择性能更好的组。Vamana 和 NSG 比较接近,因此参数设置也比较接近。然而,文中并未给出 HNSW 参数选择的原因。我们认为,HNSW 的参数 M 设置得相对较大。如果基于图的索引的外度不设置在同一水平,可能会导致它们之间的比较缺乏说服力。

在上述索引参数下,Vamana、HNSW 和 NSG 的索引时间分别为 129 秒、219 秒和 480 秒。NSG 的索引时间包括使用 EFANN [3] 构建初始邻居图的时间。

Recall-QPS 曲线:

3.png 3.png

从图 3 可以看出,Vamana 在三个数据集上的表现都很出色,与 NSG 相似,略优于 HNSW。

搜索半径比较:

从图 2.c 可以看出,与 NSG 和 HNSW 相比,在相同召回率下,Vamana 的平均搜索路径最短。

一次性建立的索引与大型合并索引的比较

数据集SIFT1B

一次性建立的索引参数:L = 50,R = 128,alpha = 1.2。在 1800G DDR3 机器上运行 2 天后,内存峰值约为 1100G,平均出度为 113.9。

基于合并的索引程序:

  1. 使用 kmeans 在数据集上训练 40 个聚类;
  2. 每个点分布到最近的 2 个聚类中;
  3. 为每个聚类建立 L = 50、R = 64 和 alpha = 1.2 的 Vamana 索引;
  4. 合并每个聚类的索引。

该索引生成了一个 384GB 的索引,平均离度为 92.1。该索引在 64GB DDR4 机器上运行了 5 天。

对比结果如下(图 2a): 4.png 4.png

总之:

  1. 一次性构建的索引明显优于基于合并的索引;
  2. 基于合并的索引也很出色;
  3. 基于合并的索引方案也适用于 DEEP1B 数据集(图 2b)。

基于磁盘的索引:DiskANN VS.FAISS VS.IVF-OADC+G+P

IVFOADC+G+P 是参考文献 [5] 中提出的一种算法。

本文仅比较 DiskANN 和 IVFOADC+G+P,因为参考文献 [5] 已证明 IVFOADC+G+P 优于 FAISS。此外,FAISS 需要 GPU 资源,并非所有平台都支持。

IVF-OADC+G+P 似乎是 HNSW 和 IVF-PQ 的结合。它使用 HNSW 确定簇,并通过向目标簇添加一些剪枝策略来执行搜索。

结果如图 2a 所示。图中的 16 和 32 是代码集大小。数据集为 SIFT1B,由 OPQ 量化。

代码实现细节

DiskANN 的源代码在 https://github.com/microsoft/DiskANN 上开源。

2021 年 1 月,磁盘解决方案的源代码开源。

下面主要介绍索引过程和搜索过程。

建立索引

建立索引有 8 个参数:

data_type:选项包括 float/int8/uint8。

data_file.bin:原始数据二进制文件。文件中的前两个整数分别代表数据集向量总数 n 和向量维数 dim。最后 n 个dimsizeof(data_type) 字节是连续的向量数据。

index_prefix_path:输出文件的路径前缀。索引建立后,将生成多个与索引相关的文件。该参数是这些文件所在目录的公共前缀。

R:全局索引的最大出度。

L:Vamana 索引的参数 L,即候选集大小的上限。

B:查询时的内存阈值。它控制着 PQ 码本的大小(单位:GB)。

M:建立索引时的内存阈值。它决定片段的大小,单位为 GB。

T:线程数。

索引过程(入口函数:aux_utils.cpp::build_disk_index):

  1. 根据 index_prefix_path 生成各种输出文件名。
  2. 参数检查。
  3. 读取 data_file.bin 的元数据,得到 n 和 dim。根据 B 和 n 确定 PQ 的编码本子空间数 m。
  4. generate_pq_pivots:使用 p = 1500000/n 的采样率均匀采样 PQ 训练集的中心点,对 PQ 进行全局训练。
  5. generate_pq_data_from_pivots:生成全局 PQ 编码集,并分别保存中心点和编码集。
  6. build_merged_vamana_index:分割原始数据集,分段建立 Vamana 索引,最后将索引合并为一个索引。
  • partition_with_ram_budget:使用 kmeans 方法对数据集进行采样,将每个点分配到两个最近的簇。分割数据集,每个分割产生两个文件:一个数据文件和一个 ID 文件。ID 文件和数据文件相互对应,ID 文件中的每个 ID 对应数据文件中的一个向量。ID 是通过对原始数据的每个向量从 0 到 n-1 进行编号得到的。ID 相对重要,与合并有关。
    • 对训练集进行全局均匀采样,采样率为 1500000 / n;
    • 初始化 num_parts = 3:
      • 对步骤 i 中的训练集进行 num_parts-means++ 处理;
      • 使用 0.01 的采样率对测试集进行全局均匀采样,并将测试集划分为最近的 2 个聚类;
      • 计算每个簇中的点数,除以采样率,估算出每个簇中的点数;
      • 根据 Vamana 索引的大小,估算步骤 3 中最大簇所需的内存,如果不超过参数 M,则进入步骤 iii,否则 num_parts ++ 返回步骤 2;
    • 将原始数据集划分为 num_parts 组文件,每组文件包括碎片数据文件和碎片数据对应的 ID 文件。
  • 为步骤 a 中的所有分片分别创建 Vamana 索引并保存到磁盘;
  • merge_shards:将 num_parts 分片的 Vamana 索引合并为全局索引:
    • 将 num_parts 碎片的 ID 文件读入 idmap。该 idmap 相当于建立 fragment->id 的正向映射;
    • 根据 idmap 建立 id-> 片段的反向映射,知道每个向量在哪两个片段中;
    • 使用缓存为 1GB 的阅读器打开 num_parts 片段的 Vamana 索引,使用缓存为 1GB 的写入器打开输出文件,准备合并;
    • 将 Vamana 索引的 num_parts 导航点放入中心点文件,搜索时将使用该文件;
    • 按照 ID 由小到大开始合并,根据反向映射依次读取每个片段中每个原始向量的邻点集,进行删减、洗牌、截断,并写入输出文件。因为最初的分片是全局有序的,现在的合并也是有序的,所以最终刷新的索引中的 ID 和原始数据的 ID 是一一对应的。
    • 删除临时文件,包括片段文件、片段索引和片段 ID 文件。
  1. 创建磁盘布局:步骤 6 中生成的全局索引只有一个紧凑的邻接表。这一步是对齐索引。邻接表和原始数据存储在一起。搜索时,加载邻接表和读取原始向量一起进行精确的距离计算。还有一个 SECTOR 的概念,默认大小为 4096。每个 SECTOR 只包含 4096 / node_size 的向量信息,node_size = 单个向量大小 + 单个节点邻接表大小。

  2. 最后,进行一次 150000 / n 的全局均匀采样,保存并在搜索时用于热身。

搜索

有 10 个搜索参数:

  • index_type:选项包括 Float/int8/uint8,类似于建立索引时的第一个参数 data_type。
  • index_prefix_path:请参考索引参数 index_prefix_path。
  • num_nodes_to_cache(缓存节点数):缓存热点的数量。
  • num_threads:线程数:搜索线程数。
  • beamwidth:预载点数量上限。由系统决定是否设置为 0。
  • query_file.bin:查询集文件。
  • truthset.bin:结果集文件,"null "表示不提供结果集,由程序自行计算;
  • K: topk;
  • result_output_prefix:保存搜索结果的路径;
  • L*:搜索参数列表。可添加多个值。对于每个 L,在使用不同 L 进行搜索时将提供统计信息。

搜索过程

  1. 加载相关数据:加载查询集、PQ 中心点数据、编码本数据、搜索起点和其他数据,并读取索引元。
  2. 使用索引过程中采样的数据集进行缓存光束搜索,统计每个点的访问次数,并将访问频率最高的点加载到缓存中。
  3. 默认情况下有一个 WARMUP 操作符。与步骤 2 一样,该样本数据集也用于进行缓存光束搜索。
  4. 根据给定的参数数 L,每个 L 都将再次使用查询集进行缓存光束搜索,并输出召回率和 QPS 等统计数据。预热和统计热点数据的过程不计入查询时间。

关于缓存光束搜索:

  1. 从候选起点出发,查找离查询点最近的候选点。此处使用 PQ 距离,并将起点添加到搜索队列中。
  2. 开始搜索:
  • 在搜索队列中,未访问点的数量不超过 beam_width + 2。如果这些点在缓存中,则将它们添加到缓存命中队列中。如果未命中,则将其加入未命中队列。确保未命中队列的大小不超过 beam_width。
  • 向未命中队列中的点发送异步磁盘访问请求。
  • 对于缓存命中的点,使用原始数据和查询数据计算精确距离,将其加入结果队列,然后使用 PQ 计算未访问过的相邻点的距离,再将其加入搜索队列。搜索队列的长度受参数限制。
  • 在步骤 a 中处理缓存的未访问点,与步骤 c 类似。
  • 当搜索队列为空时,搜索结束,并返回结果队列 topk。

总结

虽然这是一篇相对较长的论文,但总体而言非常出色。论文和代码的思路都很清晰:通过 k-means 将一些重叠的桶划分开,然后再将桶划分开,建立一个映射索引,最后合并索引,这是一个比较新的思路。至于基于内存的图索引 Vamana,它本质上是一个随机初始化版本的 NSG,可以控制修剪粒度。在查询时,它充分利用了缓存 + 管道,掩盖了部分 io 时间,并提高了 QPS。然而,根据该论文,即使机器条件不特殊,训练时间也长达 5 天,可用性相对较低。未来肯定需要对训练进行优化。从代码的角度来看,其质量相对较高,可直接用于生产环境。

参考文献

  1. Suhas Jayaram Subramanya、Fnu Devvrit、Harsha Vardhan Simhadri、Ravishankar Krishnawamy、Rohan Kadekodi。DiskANN:单节点快速准确的十亿点近邻搜索。NeurIPS 2019。
  2. [傅聪、项超、王昌旭、蔡登。利用导航扩散图的快速近似近邻搜索。Doi: 10.14778/3303753.3303754.] PVLDB, 12(5):461 - 474, 2019.(http://www.vldb.org/pvldb/vol12/p461-fu.pdf)
  3. Cong Fu 和 Deng Cai.GitHub - ZJULearning/efanna:用于 ANN 搜索和 KNN 图构建的快速库。
  4. 搜索引擎:高维数据检索工业级解决方案

5. Dmitry Baranchuk, Artem Babenko, and Yury Malkov.重新审视十亿尺度近似近邻的倒置指数。

Try Managed Milvus for Free

Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

Get Started

Like the article? Spread the word

扩展阅读