背景資料
本文將討論 Milvus 如何排程查詢任務。我們也會討論實施 Milvus 排程的問題、解決方案和未來方向。
背景資料
從大規模向量搜尋引擎的資料管理中,我們知道向量相似性搜尋是透過兩個向量在高維空間中的距離來實現的。向量搜尋的目標是找出最接近目標向量的 K 個向量。
測量向量距離的方法有很多,例如歐氏距離:
1-euclidean-distance.png
其中 x 和 y 是兩個向量。n 是向量的維度。
為了找出資料集中最近的 K 個向量,需要計算目標向量與要搜尋的資料集中所有向量之間的歐氏距離。然後,向量會依距離排序,以獲得 K 個最近的向量。計算工作與資料集的大小成正比。資料集越大,查詢所需的計算工作就越多。專門用於圖形處理的 GPU 恰好有很多核心,可以提供所需的運算能力。因此,在 Milvus 實作過程中也會考慮到多 GPU 支援。
基本概念
資料區(TableFile)
為了提高對大規模資料搜尋的支援,我們優化了 Milvus 的資料儲存。Milvus 將表中的資料按大小分割成多個資料區塊。在向量搜尋時,Milvus 會在每個資料區塊中搜尋向量,然後合併結果。一個向量搜尋作業包含 N 個獨立的向量搜尋作業 (N 是資料區塊的數量) 和 N-1 個結果合併作業。
任務佇列(TaskTable)
每個資源都有一個任務陣列,記錄屬於該資源的任務。每個任務都有不同的狀態,包括 Start、Loading、Loaded、Executing 和 Executed。計算裝置中的 Loader 和 Executor 共用相同的任務佇列。
查詢排程
2-query-scheduling.png
- 當 Milvus 伺服器啟動時,Milvus 會透過
server_config.yaml
配置檔案中的gpu_resource_config
參數啟動相應的 GpuResource。DiskResource 和 CpuResource 仍然無法在server_config.yaml
中編輯。GpuResource 是search_resources
和build_index_resources
的組合,在以下範例中稱為{gpu0, gpu1}
:
3-sample-code.png
3-example.png
- Milvus 接收到一個請求。表元資料儲存在外部資料庫中,單一主機為 SQLite 或 MySQl,分散式為 MySQL。收到搜尋請求後,Milvus 會驗證該表是否存在,以及維度是否一致。然後,Milvus 讀取該表的 TableFile 清單。
4-milvus-reads-tablefile-list.png
- Milvus 建立一個 SearchTask。因為每個 TableFile 的計算都是獨立進行的,所以 Milvus 會為每個 TableFile 建立一個 SearchTask。作為任務排程的基本單位,SearchTask 包含目標向量、搜尋參數和 TableFile 的檔案名稱。
5-table-file-list-task-creator.png
- Milvus 選擇運算裝置。SearchTask 執行計算的裝置取決於每個裝置的估計完成時間。估計完成時間指定目前時間與估計計算完成時間之間的估計間隔。
例如,當 SearchTask 的資料區塊載入到 CPU 記憶體時,下一個 SearchTask 正在 CPU 計算任務佇列中等待,而 GPU 計算任務佇列則處於閒置狀態。CPU 的估計完成時間等於前一個 SearchTask 與目前 SearchTask 的估計時間成本總和。GPU 的估計完成時間等於資料區塊載入 GPU 的時間與目前 SearchTask 的估計時間成本之和。資源中 SearchTask 的估計完成時間等於資源中所有 SearchTask 的平均執行時間。Milvus 隨後會選擇估計完成時間最少的裝置,並將 SearchTask 指派給該裝置。
這裡我們假設 GPU1 的估計完成時間較短。
6-GPU1-shorter-estimated-completion-time.png
Milvus 將 SearchTask 加入 DiskResource 的任務佇列。
Milvus 將 SearchTask 移至 CpuResource 的任務佇列。CpuResource 中的載入線程依序從任務佇列中載入每個任務。CpuResource 讀取對應的資料區塊到 CPU 記憶體。
Milvus 將 SearchTask 移至 GpuResource。GpuResource 中的載入線程將資料從 CPU 記憶體複製到 GPU 記憶體。GpuResource 讀取對應的資料區塊到 GPU 記憶體。
Milvus 在 GpuResource 中執行 SearchTask。由於 SearchTask 的結果相對較小,因此結果會直接回傳到 CPU 記憶體。
7-scheduler.png
- Milvus 將 SearchTask 的結果合併為整個搜尋結果。
8-milvus-merges-searchtast-result.png
所有 SearchTask 完成後,Milvus 會將整個搜尋結果回傳給用戶端。
索引建立
索引建立基本上和搜尋過程相同,沒有合併過程。我們不會詳細談論。
效能最佳化
快取
如前所述,在計算之前,資料區塊需要載入到相應的存儲設備,如 CPU 記憶體或 GPU 記憶體。為了避免重複載入資料,Milvus 引進 LRU (Least Recently Used) 快取記憶體。當快取滿時,新的資料區塊會推走舊的資料區塊。您可以根據目前的記憶體大小,透過設定檔自訂快取大小。建議使用較大的快取記憶體儲存搜尋資料,以有效節省資料載入時間並提昇搜尋效能。
資料載入與計算重疊
快取記憶體無法滿足我們提升搜尋效能的需求。當記憶體不足或資料集大小過大時,需要重新載入資料。我們需要降低資料載入對搜尋效能的影響。資料載入,無論是從磁碟載入到 CPU 記憶體,或是從 CPU 記憶體載入到 GPU 記憶體,都屬於 IO 作業,幾乎不需要處理器做任何計算工作。因此,我們考慮並行執行資料載入與計算,以獲得更好的資源使用。
我們將資料區塊的運算分成 3 個階段 (從磁碟載入到 CPU 記憶體、CPU 運算、結果合併) 或 4 個階段 (從磁碟載入到 CPU 記憶體、從 CPU 記憶體載入到 GPU 記憶體、GPU 運算與結果擷取、結果合併)。以 3 階段計算為例,我們可以啟動 3 個線程來負責 3 個階段,以發揮指令流水線的功能。由於結果集大多較小,因此結果合併並不需要花費太多時間。在某些情況下,資料載入與計算的重疊可以減少 1/2 的搜尋時間。
9-sequential-overlapping-load-milvus.png
問題與解決方案
不同的傳輸速度
之前,Milvus 使用 Round Robin 策略進行多 GPU 任務排程。這個策略在我們的 4-GPU 伺服器上運作完美,搜尋效能提升了 4 倍。然而,對於我們的 2-GPU 主機,效能卻沒有提升 2 倍。我們做了一些實驗,發現一個 GPU 的資料複製速度是 11 GB/秒。然而,對於另一個 GPU 而言,則是 3 GB/秒。參考主機板的說明文件後,我們確認主機板是透過 PCIe x16 與一顆 GPU 連接,而另一顆 GPU 則是透過 PCIe x4 連接。也就是說,這些 GPU 的複製速度不同。之後,我們加入複製時間來測量每個 SearchTask 的最佳裝置。
未來的工作
增加複雜度的硬體環境
在實際條件下,硬體環境可能會更複雜。對於有多顆 CPU、NUMA 架構的記憶體、NVLink 和 NVSwitch 的硬體環境,跨 CPU/GPU 的通訊會帶來許多最佳化的機會。
查詢最佳化
在實驗過程中,我們發現了一些效能改善的機會。例如,當伺服器收到針對相同資料表的多個查詢時,在某些條件下可以合併查詢。透過使用資料位置性,我們可以改善效能。這些優化將在我們未來的開發中實現。 現在我們已經知道單主機、多 GPU 情況下的查詢排程和執行方式。在接下來的文章中,我們會繼續介紹 Milvus 更多的內部機制。
Like the article? Spread the word