🚀 免費嘗試 Zilliz Cloud,完全托管的 Milvus,體驗速度提升 10 倍!立即嘗試

milvus-logo
LFAI
  • Home
  • Blog
  • DiskANN,一種基於磁碟的 ANNS 解決方案,在十億級資料集上具有高回復率和高 QPS

DiskANN,一種基於磁碟的 ANNS 解決方案,在十億級資料集上具有高回復率和高 QPS

  • Engineering
September 24, 2021
Zilliz

李成明,Zilliz 研發工程師,畢業於東南大學,擁有計算機科學碩士學位。他目前的研究重點是高維數據上的 ANNS 問題,包括基於圖和基於量化的解決方案。

"DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node "是 2019 年在 NeurIPS 上發表的一篇論文。這篇論文介紹了一種最先進的方法,使用一台只有 64GB RAM 和足夠大的 SSD 的機器,在十億級資料集上執行索引建立和搜索。此外,它還滿足了 ANNS(近似最近鄰搜索)在大尺度資料集上的三個要求:高召回率、低延遲和高密度(單機中的節點數)。本方法使用 64GB 記憶體和 16 核心 CPU 的單機,在十億級資料集 SIFT-1B 上建立了基於圖的索引,達到 5000 QPS (每秒查詢)、95 % 以上的召回率@1,且平均延遲低於 3 毫秒。

作者

Suhas Jayaram Subramanya:微軟印度研究院前員工,CMU 博士生。主要研究興趣為高效能運算與大規模資料的機器學習演算法。

Devvrit:德州大學奧斯丁分校研究生研究助理。他的研究興趣是理論電腦科學、機器學習和深度學習。

Rohan Kadekodi:德州大學博士生。他的研究方向是系統與儲存,主要包括持久性儲存、檔案系統與 kV 儲存。

Ravishankar Krishaswamy:微軟印度研究所首席研究員。CMU 博士。研究方向為以圖形與聚類為基礎的近似演算法。

Harsha Vardhan Simhadri:微軟印度研究所首席研究員。CMU 博士。過去研究平行演算法與執行時系統。現在他的主要工作是開發新演算法和撰寫程式模型。

動機

大多數主流的 ANNS 演算法都會在索引建立效能、搜尋效能和召回率之間做一些取捨。基於圖的演算法,例如 HNSW 和 NSG,是目前在搜尋效能和召回率方面最先進的方法。由於記憶體駐留的圖形索引方法會佔用太多的記憶體,因此在記憶體資源有限的情況下,使用單台機器來索引和搜尋大型資料集相對較為困難。

許多應用需要基於歐氏距離的 ANNS 在十億級資料集上快速反應。以下是兩種主要的解決方案:

  1. 反向索引 + 量化:將資料集聚類為 M 個分區,並使用 PQ (Product Quantization) 等量化方案壓縮資料集。此解決方案會產生低召回率,因為資料壓縮會造成精確度的損失。增加 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 值並放寬修剪條件後,很明顯又增加了許多邊。在最終圖中,有相當多的長邊被加入。這可以有效縮小搜尋半徑。

磁碟區

一台只有 64GB 記憶體的個人電腦甚至無法容納十億筆原始資料,更遑論在這些資料上建立索引。前面有兩個挑戰1.如何在有限的記憶體資源下為如此大規模的資料集建立索引?2.如果原始資料無法載入記憶體,如何計算搜尋時的距離?

本文提出以下解決方案:

  1. 針對第一個挑戰:首先利用 k-means 將資料分成 k 個群組,再將每個點分配到最近的 i 個群組。一般來說,i 的數量為 2 就足夠了。為每個叢集建立一個基於記憶體的 Vamana 索引,最後將 k 個 Vamana 索引合併為一個。
  2. 針對第二個挑戰:在原始向量上建立索引,並查詢壓縮向量。在原始向量上建立索引可確保圖形的品質,而壓縮向量則可載入記憶體進行粗粒度搜尋。雖然使用壓縮向量進行搜尋可能會造成精確度的損失,但只要圖形的品質夠高,大方向還是會正確的。最終的距離結果會使用原始向量計算。

DiskANN 的索引佈局與一般圖形索引的佈局相似。每個點的鄰居集和原始向量資料會儲存在一起。這樣可以更好地利用資料的位置性。

如前所述,如果索引資料儲存在 SSD 上,則必須儘可能減少磁碟存取次數和磁碟讀寫請求,以確保低搜尋延遲。因此 DiskANN 提出了兩種優化策略:

  1. 快取熱點:從記憶體中的起始點起,快取 C 跳躍範圍內的所有點。C 的值最好設定在 3 到 4 之間。
  2. 光束搜尋:簡單來說,就是預先載入鄰居資訊。搜尋點 p 時,如果 p 的鄰居點不在記憶體中,就需要從磁碟載入。由於少量的 SSD 隨機存取作業與 SSD 單扇區存取作業所需的時間差不多,因此一次可以載入 W 個未存取點的鄰接點資訊。W 不能設定得太大或太小。W 過大會浪費計算資源和 SSD 頻寬,而 W 過小則會增加搜尋延遲。

實驗

實驗包括三組:

記憶體索引之間的比較:Vamana VS.NSG VS.HNSW

資料集:SIFT1M (128 維度)、GIST1M (960 維度)、DEEP1M (96 維度) 以及從 DEEP1B 隨機抽樣的 1M 資料集。

索引參數(所有資料集使用相同的參數集):

HNSW:M = 128,efc = 512。

Vamana:R = 70,L = 75,alpha = 1.2。

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

文中未提供搜尋參數,可能與索引參數一致。在參數選擇上,文中提到的 NSG 參數是根據 NSG 的 GitHub 套件庫所列出的參數來選擇性能較佳的群組。Vamana 與 NSG 的參數比較接近,所以參數設定也比較接近。然而,HNSW 參數選擇的原因並沒有給出。我們認為 HNSW 的參數 M 設定得相對較大。我們認為 HNSW 的參數 M 設定得比較大,如果它們的 out-degrees 不是設定在相同的水準,可能會導致基於圖的索引之間的比較缺乏說服力。

在上述索引參數下,Vamana、HNSW 及 NSG 的索引時間分別為 129s、219s 及 480s。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。最後的 ndimsizeof(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 的 meta,得到 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 快取記憶體的寫入器開啟輸出檔案,準備合併;
    • 將 num_parts 導航點的 Vamana 索引放入中心點檔案,搜尋時會用到;
    • 按照 ID 由小到大開始合併,依次按照反向映射讀取每個片段中每個原始向量的鄰點集,進行重複、洗牌、截短,並寫入輸出文件。因為原本分片是全局有序的,現在合併也是有序的,所以最終沖洗索引中的 ID 和原始資料的 ID 是一一對應的。
    • 刪除臨時檔案,包括片段檔案、片段索引和片段 ID 檔案。
  1. create_disk_layout:在步驟 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. 使用索引過程中取樣的資料集做 cached_beam_search,計算每個點的存取次數,並將存取頻率最高的 num_nodes_to_cache 點載入快取記憶體。
  3. 預設有 WARMUP 作業。和步驟 2 一樣,這個樣本資料集也是用來做 cached_beam_search。
  4. 根據給定的參數 L,每個 L 都會用查詢集再執行一次 cached_beam_search,並輸出召回率和 QPS 等統計資料。熱身與統計熱點資料的過程不計入查詢時間。

關於 cached_beam_search:

  1. 從候選人起始點找出最接近查詢點的候選人。這裡使用 PQ 距離,並將起點加入搜尋隊列。
  2. 開始搜尋:
  • 從搜尋佇列中,不超過 beam_width + 2 個未到訪的點數。如果這些點在快取記憶體中,則將它們加入快取記憶體命中佇列。如果未命中,則將它們加入未命中佇列。確保未命中佇列的大小不超過 beam_width。
  • 向未命中佇列中的點傳送異步磁碟存取請求。
  • 對於快取命中的點,使用原始資料和查詢資料計算精確距離,加入結果佇列,然後在加入搜尋佇列前,使用 PQ 計算未到訪過的鄰近點距離。搜尋佇列的長度受參數限制。
  • 在步驟 a 中處理緩存的遺漏點,類似於步驟 c。
  • 當搜索佇列為空時,搜尋結束,並返回結果佇列 topk。

總結

雖然這是一篇相對較長的作品,但整體來說非常優秀。論文與程式碼的思路都很清楚:透過 k-means 分出一些重疊的 bucket,再將這些 bucket 分割建立一個 map 索引,最後將索引合併,這是一個比較新的想法。至於基於記憶體的圖索引 Vamana,本質上是 NSG 的隨機初始化版本,可以控制修剪粒度。在查詢時,它可以充分利用 cache + pipeline,掩蓋部分 io 時間,提高 QPS。然而,根據論文指出,即使機器條件不是很特殊,訓練時間也長達 5 天,可用性相對較低。未來肯定需要對訓練進行優化。從程式碼的角度來看,其品質相對較高,可直接用於生產環境中。

參考文獻

  1. Suhas Jayaram Subramanya、Fnu Devvrit、Harsha Vardhan Simhadri、Ravishankar Krishnawamy、Rohan Kadekodi。DiskANN:在單一節點上進行快速精確的十億點最近鄰搜索。NeurIPS 2019。
  2. [Cong Fu, Chao Xiang, Changxu Wang, and Deng Cai.快速近似近鄰搜尋與導航展開圖。PVLDB, 12(5):461 - 474, 2019. doi: 10.14778/3303753.3303754.] 。(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

繼續閱讀