文章目录
原文章: DiskANN: fast accurate billion-point nearest neighbor search on a single node
1. \textbf{1. } 1. 写在前面
1.1. \textbf{1.1. } 1.1. 预备知识与文中标记
1️⃣最邻近查询问题:
- 精确最邻近( k -NN k\text{-NN} k-NN):
- 含义:在数据库 P P P中,找到离查询点 q q q最近的 k k k个点
- 瓶颈:由于维度诅咒的存在, k -ANN k\text{-ANN} k-ANN无法突破线性暴力扫描,故引入近似最邻近查询
- 近似最邻近( k -ANN k\text{-ANN} k-ANN):
- 含义: k -NN k\text{-NN} k-NN的 Recall<100% \text{Recall<100\%} Recall<100%版本,需要在检索速度和 Recall \text{Recall} Recall中权衡
- 一些针对 k -ANN k\text{-ANN} k-ANN的索引
算法 优点 缺点 k-d Tree \text{k-d Tree} k-d Tree 索引空间小,低维下表现良好 维度大于 20 \text{20} 20时性能急剧下降 LSH \text{LSH} LSH 最坏情况下有理论保障 无法利用数据分布 图类( HNSW/NSG \small\text{HNSW/NSG} HNSW/NSG) 高维度下表现良好 索引(图)构建极其耗时 2️⃣其它预备知识
- 倒排索引( Inverted Index \text{Inverted Index} Inverted Index):用于快速全文检索的数据结构,示例如下
- 文档
Doc1: fat cat rat rat Doc2: fat cat Doc3: fat
- 构建的倒排索引
fat: Doc1 Doc2 Doc3 cat: Doc1 Doc2 rat: Doc1
- 查询性能指标:
- m -recall@ n = ∣ A ∩ B ∣ ∣ A ∣ = ∣ A ∩ B ∣ m m\text{-recall@}n=\cfrac{|A\cap{}B|}{|A|}=\cfrac{|A\cap{}B|}{m} m-recall@n=∣A∣∣A∩B∣=m∣A∩B∣
- 其中 A = A\text{=} A=查询点真实的前 m m m个最邻近集合, B = B\text{=} B=算法返回的前 n n n个结果集合
- 中位数结点( Medoid \text{Medoid} Medoid):数据集中,到所有其它点平均距离最小的点
- PQ \text{PQ} PQ算法原理: M \text{M} M维原始向量 → 分割 \xrightarrow{分割} 分割 N \text{N} N个 M N \cfrac{\text{M}}{\text{N}} NM维子向量 → ( 寻求每个子向量最近的质心Index ) 向量量化 \xrightarrow[(寻求每个子向量最近的质心\text{Index})]{向量量化} 向量量化(寻求每个子向量最近的质心Index) N \text{N} N维短代码(向量)
3️⃣本文符号表示
符号 含义与备注 P P P 数据集(点集),点集规模用$ G = ( P , E ) G\text{=}(P, E) G=(P,E) 有向图,顶点集为 P P P边集为 E E E N out ( p ) N_{\text {out }}(p) Nout (p) 查询点 p p p的出边集合 x p \mathbf{x}_p xp 查询点 p p p的向量表示 d ( p , q ) d(p, q) d(p,q) 查询点 p p p和 q q q之间的欧氏距离 1.2. \textbf{1.2. } 1.2. 关于大规模索引:本文的背景与研究
1️⃣大规模数据的索引:在十亿级个点中找最邻近,主要有以下两种方法
- 倒排索引 + \text{+} +数据压缩: FAISS \small\text{FAISS} FAISS( TBDATA’19 \small\text{TBDATA'19} TBDATA’19), IVFOADC+G+P \small\text{IVFOADC+G+P} IVFOADC+G+P (ECCV’18) \small\text{(ECCV'18)} (ECCV’18)
- 方法概述:
方法 含义 数据分区 将数据库点分为多个分区,查询最邻近时只考虑查询点附近几个分区 数据压缩 即产品量化,原始高维向量 → 每个子向量量化成低维 分为多个低维子向量 \xrightarrow[每个子向量量化成低维]{分为多个低维子向量} 分为多个低维子向量每个子向量量化成低维低维向量 - 硬件性能:占用内存较小(十亿点索引后小于 64GB \text{64GB} 64GB) + \text{+} +可利用 GPU \text{GPU} GPU加速压缩后的数据
- 查询性能:对于 1-recall@ k \text{1-recall@}k 1-recall@k, k =1 k\text{=1} k=1时较低 k =100 k\text{=100} k=100时较高
- 分片( Shard \text{Shard} Shard)法: MRNG \small\text{MRNG} MRNG (VLDB’19) \small\text{(VLDB'19)} (VLDB’19)
- 索引构建:分割数据为多片 → \to →为每片构建索引(并加载到内存中),其中数据维度并未压缩
- 查询流程:查询请求发给每片/特定几片 → \to →相应片执行查询 → \to →排序合并各片查询结果
- 性能:由于未压缩故占用内存较大,但也正因此查询精准度更高,扩展成本极大
2️⃣大规模索引的难题:物理化 RAM or SSD \text{RAM or SSD} RAM or SSD
- 目前 ANN \text{ANN} ANN算法的索引都存储在 RAM \text{RAM} RAM中,可实现高速存取但存储空间贵
- 将 ANN \text{ANN} ANN索引放在 SSD \text{SSD} SSD的尝试:
- SSD \text{SSD} SSD的读取延时比 RAM \text{RAM} RAM高出几个数量级,对查询性能是灾难性的
- 改进途径在于,减少每次查询所需读取 SSD \textbf{SSD} SSD磁盘的次数
3️⃣本文对大规模索引的贡献:提出依托图算法 Vamana \text{Vamana} Vamana的 DiskANN \text{DiskANN} DiskANN,使 SSD \text{SSD} SSD也能支持大规模 ANN \text{ANN} ANN
- 关于 Vamana \text{Vamana} Vamana图算法:
- 生成图索引直径远小于 NSG/HNSW \small\text{NSG/HNSW} NSG/HNSW的 → { 若放内存中 : 性能优于HNSW/NSG 若放磁盘中 : 减少DiskANN访问磁盘次数 \to\begin{cases}若放内存中:性能优于\text{HNSW/NSG}\\\\若放磁盘中:减少\text{DiskANN}访问磁盘次数\end{cases} →⎩ ⎨ ⎧若放内存中:性能优于HNSW/NSG若放磁盘中:减少DiskANN访问磁盘次数
- 为分割后的每个(重叠)数据分区构建 Vamana \small\text{Vamana} Vamana索引再合并成大索引,与不分割暴力构建性能相当
- Vamana \text{Vamana} Vamana图构建可与 PQ \text{PQ} PQ结合 → { 放内存中的 : 压缩向量 放磁盘中的 : 全精度向量+构建的图索引 \to\begin{cases}放内存中的:压缩向量\\\\放磁盘中的:全精度向量\text{+}构建的图索引\end{cases} →⎩ ⎨ ⎧放内存中的:压缩向量放磁盘中的:全精度向量+构建的图索引
- 关于 DiskANN \text{DiskANN} DiskANN的实际表现:
- 条件: 64GB \text{64GB} 64GB内存,十亿个百维数据点
- 性能: 1-recall@1=95 % \text{1-recall@1=95}\% 1-recall@1=95%并且 Latency<5ms \text{Latency<5ms} Latency<5ms
2. \textbf{2. } 2. Vamana \small\textbf{Vamana} Vamana图构建算法
2.1. \textbf{2.1. } 2.1. ANN \textbf{ANN} ANN图算法: 构建/剪枝/查询
1️⃣图查询算法:贪心搜索 GreedySearch ( s , x q , k , L ) \text{GreedySearch} \left(s, \mathrm{x}_q, k, L\right) GreedySearch(s,xq,k,L)
![]()
2️⃣图构建与剪枝算法
- 图构建算法:稀疏邻居域图 SNG \text{SNG} SNG (SODA’93) \text{(SODA'93)} (SODA’93),对 ∀ p ∈ P \forall{}p\text{∈}P ∀p∈P按照以下方法确定出边
![]()
- 目的:使 GreedySearch \text{GreedySearch} GreedySearch算法能从任意点快速收敛到最邻近的充分条件
- 缺陷:构建时间复杂度为 O ( n 2 ) + {O(n^2)}+ O(n2)+生成图在直径/密度上无灵活性
- 图剪枝算法:健壮性剪枝 RobustPrune ( p , R , α , L ) \text{RobustPrune}(p, R, \alpha, L) RobustPrune(p,R,α,L)
![]()
- 目的:构建稀疏图(避免冗余连接) + + +保持连通性
- 关于剪枝条件:对于 p p p,如果可以通过其最邻近 p ∗ p^* p∗快速到达 p ′ p{'} p′,那么就没必要直连 p p p与 p ′ p{'} p′
2.2. Vamana \textbf{2.2. Vamana} 2.2. Vamana
1️⃣ Vamana \text{Vamana} Vamana的提出的背景
- 贪心搜索:算法从初始点 → 接近 \xrightarrow{接近} 接近最邻近的距离递减模式
图结构 距离递减模式 磁盘读取 示例 传统 SNG \text{SNG} SNG 逐步线性缓慢减少 高频 D → D-d → D-2d →...→ 0
RobustPrune \text{RobustPrune} RobustPrune后 按以 α >1 \alpha\text{>1} α>1的指数快速收敛 低频 D → D/α → D/α² →...→ 0
- 全局候选集的剪枝: RobustPrune ( p , P \ { p } , α , ∣ P ∣ − 1 ) \text{RobustPrune}(p, P \backslash\{p\}, \alpha, |P|-1) RobustPrune(p,P\{p},α,∣P∣−1),其中 ∣ P ∣ = n |P|=n ∣P∣=n
- 含义:考虑所有的结点可作为 p p p的潜在出邻居
- 性能:可使 GreedySearch ( s , p , 1 , 1 ) \text{GreedySearch}(s, p, 1,1) GreedySearch(s,p,1,1)在 O ( log n ) O(\log n) O(logn)事件内收敛到 p p p,但构建复杂度高达 O ( n 2 ) O(n^2) O(n2)
- Vamana \text{Vamana} Vamana改进思路:将候选集大小由 n − 1 n-1 n−1缩小至 O ( log n ) O(\log{}n) O(logn)或 O ( 1 ) O(1) O(1),使构建复杂度降到 O ( n log n ) O(n\log{}n) O(nlogn)
2️⃣ Vamana \text{Vamana} Vamana索引算法
- 算法输入:
类型 备注 数据集 P P P为数据集(用于构建图 G G G),含 n n n个数据点(第 i i i点的坐标为 x i \mathrm{x}_i xi) 参数 α \alpha α为距离阈值(控制剪枝), L L L为列表大小(控制搜索广度), R R R为结点出度限制 - 算法流程:
![]()
- 添加反向边的意义:确保访问集 V V V中所有点都可与 p p p连接,使后续搜索可快速收敛到 p p p
- 算法需两次遍历:令 α = 1 \alpha{}\text{=}1 α=1初步构建一次(确保连通) + + +令 α > 1 \alpha{}\text{>}1 α>1(用户定义)再构建一次(优化收敛)
3️⃣ Vamana/HNSW/NSG \text{Vamana/HNSW/NSG} Vamana/HNSW/NSG的对比
- 共同:都是用 GreedySearch ( s , p , 1 , L ) \text{GreedySearch}\left(s, \mathcal{p}, 1, L\right) GreedySearch(s,p,1,L)和 RobustPrune ( p , V , α , R ) \text{RobustPrune}(p, \mathcal{V}, \alpha, R) RobustPrune(p,V,α,R)来确定 p p p邻居
- 不同:
不同点 Vamana \text{Vamana} Vamana HNSW \text{HNSW} HNSW NSG \text{NSG} NSG 备注 α \alpha α可调 ✅ ❌( α = 1 \alpha\text{=}1 α=1) ❌( α = 1 \alpha\text{=}1 α=1) 使 Vamana \small\text{Vamana} Vamana可很好权衡度数和直径 剪枝 V \mathcal{V} V G S \small{}GS GS访问集 G S \small{}GS GS结果集 G S \small{}GS GS访问集 使 Vamana/NSG \small\text{Vamana/NSG} Vamana/NSG有长距边(无需层次) 初始图 随机图 空图 近似 k -NN \small{}k\text{-NN} k-NN图 随即图质量高于空且成本远低于 k -NN \small{}k\text{-NN} k-NN图 遍历 两次 一次 一次 基于观察,二次遍历可以提高图的质量
3. DiskANN \textbf{3. DiskANN} 3. DiskANN总体设计: 索引 & \& &搜索
3.0. \textbf{3.0. } 3.0. 概览
1️⃣ DiskANN \text{DiskANN} DiskANN总体工作流程
- 索引构建:将数据集 P P P加载入内存 → \to →在 P P P上运行 Vamana \text{Vamana} Vamana → \to →将生成图存储在 SSD \text{SSD} SSD上
- 查询方式:从 SSD \text{SSD} SSD加载图信息到内存 → \to →获取邻居信息 + + +计算/比较距离 → \to →迭代搜索
2️⃣ DiskANN \text{DiskANN} DiskANN索引布局
介质 每点存储 每边(图结构)存储 内存 压缩向量 NULL \text{NULL} NULL SSD \text{SSD} SSD 原始向量 存储每点定长 R R R的邻居标识(邻居 < R \text{<}R <R时补 0 0 0)
- 关于定长邻居标识:
- 使得 SSD \text{SSD} SSD对每点的存储(全精度向量 + + +邻居标识)都是定长的
- 遍历了偏移的计算( OffSet i = i ×FixedSize \text{OffSet}_{i}=i\text{×FixedSize} OffSeti=i×FixedSize),无需再在内存中存储偏移信息
- SSD \text{SSD} SSD存储的扇结构
- 将一点定长的全精度向量 + + +邻居标识 ↔ 统一放在 \xleftrightarrow{统一放在} 统一放在 一个扇区中对齐(如 4KB \text{4KB} 4KB)
- 对 SSD \text{SSD} SSD的读取以扇为单位,比如读取某点邻居信息时 → \to →必定能同时获取该点全精度向量
3.1. \textbf{3.1. } 3.1. 索引构建设计: 面向内存空间的优化
1️⃣存在的问题:
- 构建过程需先将数据点向量载入内存
- 一股脑将全部数据暴力载入内存将导致内存过载
2️⃣构建优化:将数据集 P P P进行重叠分簇
- 步骤:
- 划分:用 k -means k\text{-means} k-means将 P P P分为多干簇(每簇有一中心),再将 P P P所有点分给 ℓ > 1 \ell\text{>}1 ℓ>1个中心以构成重叠簇
- 索引:在每个重叠簇中执行 Vamana \text{Vamana} Vamana算法,构建相应有向边
- 合并:将所有构建的有向边合并在一个图中,完成构建
- 重叠分簇:为了保证图的连通性,以及后续搜索的 Navigable \text{Navigable} Navigable
3.2. \textbf{3.2. } 3.2. 查询方法设计
3.2.1. \textbf{3.2.1. } 3.2.1. 查询方法设计: 面向减少 SSD \textbf{SSD} SSD访问的优化
1️⃣ BeamSearch \text{BeamSearch} BeamSearch算法
- 算法流程: BeamSearch \text{BeamSearch} BeamSearch与 GreedySearch \text{GreedySearch} GreedySearch
- 优化原理:
- 一次性获取多点的邻居信息,有助于减少访问 SSD \text{SSD} SSD的频次
- 从 SSD \text{SSD} SSD获取多个(但少量)随机扇区,与获得一个扇区所需时间几乎相同
- 带宽参数 W \text{W} W:
- 含义: BeamSearch \text{BeamSearch} BeamSearch一次性获取 W W W个结点( p ∗ p^* p∗及其 W - 1 W\text{-}1 W-1个邻居)的邻居信息
- 选取:当 W W W增加时吞吐量增加 + + +延时会因 IO \text{IO} IO饱和而恶化, Trade-Off \text{Trade-Off} Trade-Off实验结果如下
W W W大小 对性能的影响 W = 1 W=1 W=1 BeamSearch \text{BeamSearch} BeamSearch退化为 GreedySearch \text{GreedySearch} GreedySearch W = 2 , 4 , 8 W=2,4,8 W=2,4,8 可在延迟和吞吐量之间取得良好平衡 W > 16 W>16 W>16 容易导致 IO \text{IO} IO队列饱和从而增加延时 2️⃣ DiskANN \text{DiskANN} DiskANN缓存
- 原理:将 SSD \text{SSD} SSD中一部分结点缓存到 DRAM(Cache) \text{DRAM(Cache)} DRAM(Cache)中,以超高速访问并避免访问 SSD \text{SSD} SSD
- 缓存策略:
- 基于已知的查询分布
- 从 s s s开始在 C = 3 , 4 C\text{=}3,4 C=3,4跳的结点 (节点数随 C C C指数增长 → C \text{→}C →C不宜太大)
3.2.2. \textbf{3.2.2. } 3.2.2. 查询方法设计: 面向内存空间的优化
1️⃣存在的问题:
- 查询过程需先将 SSD \text{SSD} SSD中存储的图结点(向量)载入内存
- 将全精度向量暴力载入内存会导致内存过载
2️⃣查询优化:使所有结点能放进内存
- 用 PQ \text{PQ} PQ将所有 p ∈ P p\text{∈}P p∈P(以及查询点)压成低维 x p ~ \widetilde{x_p} xp 并载入内存
- 查询时对比近似距离 d ( x p ~ , x q ) d\left(\widetilde{x_p}, \mathrm{x}_q\right) d(xp ,xq)
3️⃣隐式重排( Implicit Re-Ranking \text{Implicit Re-Ranking} Implicit Re-Ranking):全精度距离计算的隐式方法
- 原理与流程:
- 邻居扩展阶段:先用近似距离运行算法得到若干候选点
- 这一过程中 BeamSearch \text{BeamSearch} BeamSearch缓存结点邻居(候选点)时也会缓存其全精度向量
- 剪裁阶段:再用精确距离重算候选点与查询点距离,排序后返回离 q q q最近的 L L L个候选点
- Ps. \text{Ps. } Ps. 这正是有必要在 SSD \textbf{SSD} SSD中存放全精度向量的原因
4. \textbf{4. } 4. 评估与对比
1️⃣ HNSW/NSG/Vamana \text{HNSW/NSG/Vamana} HNSW/NSG/Vamana的 In-Memory \text{In-Memory} In-Memory搜索性能评估
- 实验设置
item \textbf{item} item 设置 备注 数据集 SIFT1M/GIST1M/DEEP1M \text{SIFT1M/GIST1M/DEEP1M} SIFT1M/GIST1M/DEEP1M 高维/百万数量级数据集 物理实现 数据都完全加载到内存中 Vamana \text{Vamana} Vamana也在内存(而非 SSD \text{SSD} SSD)上实现 - 实验结果(相同延迟下 Recall \text{Recall} Recall更高者视为更好):
- 所有情况下 NSG \text{NSG} NSG和 Vamana \text{Vamana} Vamana好于 HNSW \text{HNSW} HNSW,在最高维数据上 Vamana \text{Vamana} Vamana性能最佳
- Vamana \text{Vamana} Vamana索引构建时间最快
2️⃣ HNSW/NSG/Vamana \text{HNSW/NSG/Vamana} HNSW/NSG/Vamana的跳数( Hops \text{Hops} Hops)评估
- 跳数:搜索关键路径上磁盘读取的轮次数,直接影响搜索延时
- 实验结果:
- Vamana \text{Vamana} Vamana可大幅减少跳数(从而快速收敛),尤其在高维数据上
- 随着 α \alpha α和最大出度的增加 Vamana \text{Vamana} Vamana跳数会减少,而 NSG/HNSW \text{NSG/HNSW} NSG/HNSW的基本不变
3️⃣ HNSW/NSG/Vamana \text{HNSW/NSG/Vamana} HNSW/NSG/Vamana在十亿级数据上的评估
- 两种 Vamana \text{Vamana} Vamana算法:
- 单一 Vamana \text{Vamana} Vamana:将十亿级数据整块载入内存暴力构建
- 合并 Vamana \text{Vamana} Vamana:将十亿级数据进行(重叠)分簇 → \to →每簇分别构建 → \to →合并
- 实验结果
- 单一索引的性能优于合并索引,这源于合并索引需要遍历更多结点才能找到相同邻域
- 合并索引也超过了其它算法,故可认为合并索引更能权衡内存 ↔ \xleftrightarrow{} 性能
4️⃣ IVF-based \text{IVF-based} IVF-based方法 /Vamana \text{/Vamana} /Vamana在十亿级数据上的评估
- 关于 FAISS \text{FAISS} FAISS:由于其性能劣于 IVFOADC+G+P \text{IVFOADC+G+P} IVFOADC+G+P以及需要 GPU \text{GPU} GPU构建索引,故忽略
- 对于 IVFOADC+G+P \text{IVFOADC+G+P} IVFOADC+G+P:分别使用 16/32 \text{16/32} 16/32字节的 OPQ \text{OPQ} OPQ码本构建, IVFOADC+G+P-32 \text{IVFOADC+G+P-32} IVFOADC+G+P-32性能更优
- 对于 DiskANN \text{DiskANN} DiskANN:在与 IVFOADC+G+P-32 \text{IVFOADC+G+P-32} IVFOADC+G+P-32相同内存占用下比较,性能远优