處理大量數據的工作負載 (尤其是在云端運行的工作負載) 通常會使用對象存儲服務 (S3、Google Cloud Storage、Azure Blob Storage 等) 作為數據源。對象存儲服務可以存儲和提供海量數據,但要想獲得最佳性能,可能需要根據遠程對象存儲的行為方式調整工作負載。本文適用于希望盡快將數據讀或寫到對象存儲,以便 IO 不會限制工作負載的 RAPIDS 用戶。
您對本地文件系統行為方式的一些了解可轉換為遠程對象存儲,但它們本質上是不同的。這兩者之間的最大區別 (至少對于數據分析工作負載而言) 可能在于,對象存儲上的讀取和寫入操作具有越來越高的可變延遲。每個存儲服務 (AWS、Azure) 都有自己的一套最佳實踐和性能指南。在這里,我們將提供一些專注于數據分析工作負載的一般指南。
地址
將計算節點放置在存儲服務附近 (理想情況下,應位于同一云區域),可在運行工作負載的計算機和為數據提供服務的計算機之間提供速度最快、最可靠的網絡。在一天結束時,傳輸將受到光速的限制,因此最大限度地減少物理距離不會造成傷害。
文件格式
“云原生”文件格式的開發能夠很好地與對象存儲配合使用。這些文件格式通常可讓用戶快速輕松地訪問元數據 (元數據包括列名稱或數據類型等高級信息,以及文件特定數據子集所在位置等低級信息)。 Apache Parquet 、 Zarr 和 Cloud Optimized GeoTIFF 是適用于各種類型數據的云原生文件格式的一些示例。
由于對象存儲服務通常支持范圍請求,因此客戶端 (如 cuDF ) 可以讀取元數據,然后只下載您實際需要的數據。例如,cuDF 只能從包含多列的 Parquet 文件中讀取幾列數據,或者 Zarr 客戶端可以從大型 n 維數組中讀取單個 chunk。這些讀取只需通過幾次 HTTP 請求即可完成,而且無需下載一堆剛剛被過濾掉的不相干數據。
文件大小
由于每個讀取操作都需要 (至少) 一個 HTTP 請求,因此我們傾向于在合理數量的字節數上分擔每個 HTTP 請求的用度。如果您控制數據寫入過程,則需要確保文件足夠大,以便下游處理任務獲得良好性能。最佳值取決于您的工作負載,但 parquet 文件的大小通常介于數十 MB 到數百 MB 之間 (請參閱下文,了解一些特定示例)。
也就是說,您需要注意文件大小與 Kit 中的下一個工具:并發的交互方式。
并發
使用并發同時下載多個 blobs (或單個 blob 的多個部分) 對于從遠程存儲服務中獲得良好性能至關重要。由于這是一項遠程服務,您的流程將花費一些時間 (可能會花費大量時間) 四處等待,不執行任何操作。此等待時間為 HTTP 請求被發送到響應被接收之間的時間。在此期間,我們會等待網絡執行請求,等待存儲服務處理并發送響應,等待網絡執行響應 (可能較大)。雖然該請求/響應周期的一部分會隨所涉及的數據量而擴展,但其他部分只是固定的開銷。
對象存儲服務旨在處理許多并發請求。我們可以將這一點與每個請求都涉及一些時間來等待不執行任何操作的事實相結合,以發出許多并發請求來提高整體吞吐量。在 Python 中,這通常使用線程池完成:
pool = concurrent.futures.ThreadPoolExecutor() futures = pool. map (request_chunk, chunks) |
或使用 異步 :
tasks = [request_chunk_async(chunk) for chunk in chunks] await asyncio.gather( * tasks) |
我們能夠讓大量讀取 同時 不執行任何操作,從而提高吞吐量。由于每個線程/任務通常不執行任何任務,因此擁有比計算機核心數更多的線程/任務也是可以的。如果并發請求數量足夠多,您最終會使存儲服務飽和,而存儲服務試圖滿足一些每秒請求數和帶寬目標數。但這些目標很高;您通常需要多臺機器使存儲服務飽和,并且應該實現非常高的吞吐量。
庫
上述內容基本上適用于從對象存儲服務執行遠程 IO 的任何庫。在 RAPIDS 環境中, NVIDIA KvikIO 值得注意,因為
- 它會自動將大型請求分塊為多個較小的請求,并并發發出這些請求。
- 它可以高效讀取主機或設備內存,尤其是啟用 GPU Direct Storage 時。
- 速度很快。
正如 RADIDS 24.12 發布公告中提到的那樣,從 S3 讀取數據時,KvikIO 可以實現驚人的吞吐量。我們來看看一些基準測試,看看效果如何。
基準測試
當您讀取文件時,KvikIO 會將讀取的文件拆分成較小的 kvikio.defaults.task_size
字節讀取。它使用具有 kvikio.defaults.num_threads
工作線程的線程池并行執行這些讀取請求。可以使用環境變量 KVIKIO_TASK_SIZE
和 KVIKIO_NTHREADS
控制這些內容,也可以通過 Python 使用:
with kvikio.defaults.set_num_threads(num_threads), kvikio.defaults.set_task_size(size): ... |
詳情請參閱 Runtime Settings 。
此圖表顯示了在同一區域內,針對不同大小的線程池,從 S3 到 g4dn EC2 實例讀取 1 GB Blob 的吞吐量 (以 Mbps 為單位) (越高越好)。
圖 1、從 S3 讀取 1 GB 文件的基準測試,到具有高達 25 Gbps 已發布帶寬的 g4dn.xlarge EC2 實例。這是kvikio.RemoteFile.read
的吞吐量,適用于各種值的 kvikio.defaults.num
_threads 和 16 MiB 的任務。隨著我們添加更多線程并對讀取進行并行化,吞吐量會增加到一定程度。
線程越少 (少于 4 個),吞吐量越低,讀取文件的時間越長。更多線程 (64、128、256) 通過將請求并行化到以并行方式提供服務的存儲服務,實現更高的吞吐量。當我們遇到系統中存儲服務、網絡或其他瓶頸的限制時,會出現遞減甚至負回報的情況。
借助遠程 IO,每個線程都會在相對較長的時間內等待響應,因此對于您的工作負載,可能適合使用更多線程 (相對于核心數量而言)。我們看到,在本例中,吞吐量最高,介于 64 到 128 個線程之間。
如下圖所示,任務大小也會影響最大吞吐量。
圖 2、從 S3 讀取 1 GB 文件的基準測試,到具有高達 25 Gbps 已發布帶寬的 g4dn.xlarge EC2 實例 。這顯示了kvikio.RemoteFile.read
吞吐量的熱圖。水平軸顯示各種任務大小的吞吐量,而垂直軸顯示各種線程數量。
只要任務大小不是太小(大約或低于 4 MiB)或太大(大約或超過 128 MiB),吞吐量就會達到 10 Gbps 左右。由于任務規模過小,發出許多 HTTP 請求會降低吞吐量。由于任務規模過大,我們無法獲得足夠的并發能力來最大限度地提高吞吐量。
與 boto3 (適用于 Python 的 AWS SDK) 相比,即使在線程池中使用 boto3 并發執行請求,KvikIO 也能實現更高的吞吐量。
圖 3、從從 S3 讀取 1 GB 的基準測試,到具有高達 25 Gbps 已發布帶寬的 g4dn.xlarge EC2 實例。KvikIO 基準測試使用 64 個線程和 16 MiB 任務大小。Boto3 基準測試使用 ThreadPool 并行讀取許多 4 MB 字節的塊,而參數搜索表明,對于 Boto3 而言,這是最快的塊大小。對于略為逼真的工作負載 (盡管仍然僅有一個工作負載專注于 IO),我們比較了讀取一批 360 個 parquet 文件 (每個文件約 128 MB) 的性能。這在 AWS g4dn.12xlarge
實例上運行,該實例包含 4 個 NVIDIA T4 GPU 和 48 個 vCPUs。
啟用 KvikIO 后,四個 Dask 工作進程能夠共同實現從 S3 到此單個節點的近 20 Gbps 吞吐量。
結束語
隨著 RAPIDS 加速工作負載的其他部分,IO 可能會成為瓶頸。如果您使用的是對象存儲,并且已經疲于等待數據加載,請嘗試本博文中的一些建議。讓我們了解如何在 Github 上使用 KvikIO。您還可以與 RAPIDS Slack 社區的 3,500 多名成員一起討論 GPU 加速的數據處理。
?