深度乾貨!如何將深度學習訓練性能提升數倍?

ai科技大本營 發佈 2020-04-18T05:10:46+00:00

阿里雲容器服務ACK是阿里雲提供的 Kubernetes 服務,可以在阿里雲平台的 CPU、GPU、NPU、神龍裸金屬實例上運行 Kubernetes 工作負載。

作者 | 車漾,阿里雲高級技術專家

顧榮,南京大學副研究員

責編 | 唐小引

頭圖 | CSDN 下載自東方 IC

出品 | CSDN(ID:CSDNnews)

近些年,以深度學習為代表的人工智慧技術取得了飛速的發展,正落地應用於各行各業。隨著深度學習的廣泛應用,眾多領域產生了大量強烈的高效便捷訓練人工智慧模型方面的需求。另外,在雲計算時代,以 Docker、Kubernetes 為主的容器及其編排技術在應用服務自動化部署的軟體開發運維浪潮中取得了長足的發展。Kubernetes 社區對於 GPU 等加速計算設備資源的支持方興未艾。

鑒於雲環境在計算成本和規模擴展方面的優勢,以及容器化在高效部署和敏捷疊代方面的長處,基於「容器化彈性基礎架構+雲平台 GPU 實例」進行分布式深度學習模型訓練成為了業界生成 AI 模型的主要趨勢。

為了兼顧資源擴展的靈活性,雲應用大多採用計算和存儲分離的基本架構。其中,對象存儲因為能夠有效地降低存儲成本、提升擴展彈性,經常用來存儲管理海量訓練數據。除了採用單一雲上存儲之外,很多雲平台的用戶因為安全合規、數據主權或者遺產架構方面的因素,大量數據還存儲在私有數據中心。

這些用戶希望基於混合雲的方式構建人工智慧訓練平台,利用雲平台的彈性計算能力滿足高速增長的 AI 業務模型訓練方面的需求,然而這種「本地存儲+雲上訓練」的訓練模式加劇了計算存儲分離架構帶來的遠程數據訪問的性能影響。計算存儲分離的基本架構雖然可以為計算資源和存儲資源的配置和擴展帶來更高的靈活性,但是從數據訪問效率的角度來看,由於受限於網絡傳輸帶寬,用戶不經調優簡單使用這種架構通常會遇到模型訓練性能下降的問題。

常規方案面臨的數據訪問挑戰

目前雲上深度學習模型訓練的常規方案主要採用手動方式進行數據準備,具體是將數據複製並分發到雲上單機高效存儲(例如 NVMe SSD)或分布式高性能存儲(例如,GlusterFS 並行文件系統)上。這種由用戶手工或者腳本完成的數據準備過程通常面臨如下三個問題:

  • 數據同步管理成本高:數據的不斷更新需要從底層存儲定期進行數據同步,這個過程管理成本較高。

  • 雲存儲成本開銷更多:需要為雲上單機存儲或高性能分布式存儲支付額外費用。

  • 大規模擴展更加複雜:隨著數據量增長,難以將全部數據複製到雲上單機存儲;即使複製到 GlusterFS 這樣的海量並行文件系統也會花費大量的時間。

基於容器和數據編排的模型訓練架構方案

針對雲上深度學習訓練常規方案存在的上述問題,我們設計並實現了一種基於容器和數據編排技術的模型訓練架構方案。具體系統架構如下圖所示:

系統架構核心組件

  • Kubernetes 是一種流行的深度神經網絡訓練容器集群管理平台,它提供了通過容器使用不同機器學習框架的靈活性以及按需擴展的敏捷性。阿里雲容器服務 ACK(Alibaba Cloud Kubernetes)是阿里雲提供的 Kubernetes 服務,可以在阿里雲平台的 CPU、GPU、NPU(含光 800 晶片)、神龍裸金屬實例上運行 Kubernetes 工作負載。

  • Kubeflow 是開源的基於 Kubernetes 雲原生 AI 平台,用於開發、編排、部署和運行可擴展的可攜式機器學習工作負載。Kubeflow 支持兩種 TensorFlow 框架分布式訓練,分別是參數伺服器模式和 AllReduce 模式。基於阿里雲容器服務團隊開發的 Arena,用戶可以提交這兩種類型的分布式訓練框架。

  • Alluxio 是面向混合雲環境的開源數據編排與存儲系統。通過在存儲系統和計算框架之間增加一層數據抽象層,提供統一的掛載命名空間、層次化緩存和多種數據訪問接口,可以支持大規模數據在各種複雜環境(私有雲集群、混合雲、公有雲)中的數據高效訪問。

Alluxio 發軔於大數據時代,流觴自誕生了 Apache Spark 的 UC Berkeley AMP 實驗室。Alluxio 系統設計的初衷是為了解決大數據處理流水線中不同計算框架在通過磁碟文件系統(如 HDFS)互換數據,造成整個分析性能瓶頸耗時在 I/O 操作方面的問題。Alluxio 項目開源於 2013 年,經過 7 年的不斷開發疊代,在大數據處理場景下的應用日趨成熟。另外,近些年隨著深度學習的崛起,Alluxio 分布式緩存技術正逐步成為業界解決雲上 I/O 性能問題的主流解決方案。進一步地,Alluxio 推出接口 FUSE,為雲上 AI 模型訓練提供了高效的數據訪問手段。

為了能夠更好地將 Alluxio 融入 Kubernetes 生態系統發揮兩者結合的優勢,Alluxio 團隊和阿里雲容器服務團隊協作開發提供了 Alluxio 的 Helm Chart 方案,極大地簡化了在 Kubernetes 內的部署和使用。

雲上訓練——Alluxio 分布式緩存初探

深度學習實驗環境

  • 我們使用 ResNet-50 模型與 ImageNet 數據集,數據集大小 144GB,數據以 TFRecord 格式存儲,每個 TFRecord 大小約 130MB。每個 GPU 的 batch_size 設置為 256

  • 模型訓練硬體選擇的是 4 台 V100(高配 GPU 機型),一共 32 塊 GPU 卡。

  • 數據存儲在阿里雲對象存儲服務中,模型訓練程序通過 Alluxio 讀取數據,並在讀取過程中將數據自動緩存到 Alluxio 系統。Alluxio 緩存層級配置為內存,每台機器提供 40GB 內存作為內存存儲,總的分布式緩存量為 160GB,沒有使用預先加載策略。

初遇性能瓶頸

在性能評估中,我們發現當 GPU 硬體從 P100 升級到 V100 之後,單卡的計算訓練速度得到了不止 3 倍的提升。計算性能的極大提升給數據存儲訪問的性能帶來了壓力。這也給 Alluxio 的 I/O 提出了新的挑戰。

下圖是在分別在生成數據(Synthetic Data)和使用 Alluxio 緩存的性能對比,橫軸表示 GPU 的數量,縱軸表示每秒鐘處理的圖片數。生成數據指訓練程序讀取的數據由程序自身產生,沒有 I/O 開銷,代表模型訓練性能的理論上限; 使用 Alluxio 緩存指訓練程序讀取的數據來自於 Alluxio 系統。

在 GPU 數量為 1 和 2 時,使用 Alluxio 和生成數據對比,性能差距在可以接受的範圍。但是當 GPU 的數量增大到 4 時,二者差距就比較明顯了,Alluxio 的處理速度已經從 4981 images/second 降到了 3762 images/second。而當 GPU 的數量達到 8 的時候,Alluxio 上進行模型訓練的性能不足生成數據的 30%。而此時通過系統監控,我們觀察到整個系統的計算、內存和網絡都遠遠沒有達到瓶頸。這間接說明了簡單使用 Alluxio 難以高效支持 V100 單機 8 卡的訓練場景。

為了能夠深入了解是什麼因素影響了性能並進行調優,需要首先研究分析 Alluxio 在 Kubernetes 下支持 FUSE 的整個技術棧。如下圖所示:

原因剖析

通過深度分析整個技術棧和 Alluxio 內核,我們將造成相關性能影響的原因總結如下:

1. Alluxio 讀文件引入多次 grpc 交互,造成性能開銷。

Alluxio 不只是一個單純的緩存服務。它首先是一個分布式虛擬文件系統,包含完整的元數據管理、塊數據管理、UFS 管理(UFS 是底層文件系統的簡稱)以及健康檢查機制,尤其是它的元數據管理實現比很多底層文件系統更加強大。這些功能是 Alluxio 的優點和特色,但也意味著如果每次都完整地使用 Alluxio 的全部功能,會產生多次 grpc 交互通信。完成整個讀操作的鏈路額外開銷在傳統大數據場景下並不明顯,但是深度面對學習場景下高吞吐和低延時的需求就顯得捉襟見肘了。

2. Alluxio 的數據緩存和驅逐策略會頻繁觸發節點數據緩存震盪。

深度學習場景數據冷熱經常不明顯,因此每個 Alluxio Worker 都會完整讀取數據。而 Alluxio 默認模式會優先數據本地讀取,即使數據已經保存在 Alluxio 集群中,也會從其他緩存節點拉取到本地存一份副本。這個特性在我們的場景下會帶來兩個額外開銷:1.異步數據緩存的額外開銷 2.本地空間不足會觸發自動數據驅逐的開銷,特別當節點緩存數據接近飽和的情況下性能開銷巨大。

3. Alluxio 和 Fuse 的集成性能有待優化。

很多的文件系統客戶端都是通過 Fuse 實現的,基於 Fuse 進行文件系統的開發、部署、使用都很簡單,但是默認性能並不理想,原因如下:

  • Fuse 讀操作效率不高,每次 read 最多只能讀 128KB,讀一個 128MB 的文件需要 1000 次調用 read。

  • Fuse 讀操作屬於非阻塞行為,由 libfuse 非阻塞線程池處理,一旦並發請求數量遠超過線程池(max_idle_threads)的大小,就會觸發頻繁的大量線程創建和刪除,從而影響讀性能。而在 Fuse 中,這個默認配置是 10。

  • 元數據的頻繁訪問,因為 Fuse 內核模塊是個橋樑角色,連接了應用程式和 Alluxio 的文件系統,而每一次讀獲取文件/目錄的 inode 以及 dentry,Fuse 內核模塊都會到 Alluxio 系統運行一趟,增加了系統壓力。

  • Alluxio 目前的工作模式不支持 Fuse 使用 page cache,Alluxio 原先的設計是每個線程會有自己的 FileInputStream, 而不是大家同步都在用一個 FileInputStream。如果打開 page cache,Alluxio Fuse 會有些並發預先讀到 cache 的操作,此時會產生報錯。

4. Kubernetes 對於 Alluxio 的線程池影響。

Alluxio 基於 Java 1.8 版本實現,其中的一些線程池的計算會依賴於 Runtime.getRuntime.availableProcessors,但是在 Kubernetes 環境下,默認配置中 cpu_shares 的值為 2,而 JVM 對於 CPU 的核心數的計算公式 cpu_shares/1024,導致結果是 1。這會影響 Java 進程在容器內的並發能力。

雲上模型訓練的性能優化

在分析了上述性能問題和因素之後,我們將設計了一系列性能優化策略以提升雲上模型訓練的性能。首先,需要明白數據訪問的「多快好省」是無法全部兼顧,我們針對的主要是模型訓練下只讀數據集的數據訪問加速。優化的基本思路是關注高性能和數據一致性,而犧牲一部分靈活的自適應性(比如讀寫同時發生,數據內容不斷更新等場景)。

基於上述思路,我們設計了具體的性能優化策略,這些策略遵循以下核心原則:

  • 尋找資源限制,包括線程池以及 JVM 在容器中的配置;

  • 藉助各級緩存,包括 Fuse 層和 Alluxio 元數據緩存;

  • 避免額外開銷,減少非必須的調用鏈路。比如避免不必要的元數據交互,引入上下文切換的 GC 線程和 compiler 進程;以及 Alluxio 內部的一些可以簡化的操作。

下面將從各層的組件優化角度,對這些優化策略逐一介紹:

對 Fuse 的優化

升級 Kernel 版本

選擇更高的 Kernel 版本,由於 Fuse 實現分為兩層:用戶態的 libfuse 和 Fuse Kernel,高版本的 Kernel 在 Fuse 上做了大量的優化。我們對比了 Kernel 3.10 和 4.19 的性能,可以發現讀性能可以達到 20%的提升。

優化 Fuse 參數

  • 延長 Fuse 元數據有效時間

Linux 中每個打開文件在內核中擁有兩種元數據信息:struct dentry 和 struct inode,它們是文件在內核的基礎。所有對文件的操作,都需要先獲取文件這兩個結構。所以,每次獲取文件/目錄的 inode 以及 dentry 時,Fuse 內核模塊都會從 LibFuse 以及 Alluxio 文件系統進行完整操作,這樣會帶來數據訪問的高延時和高並發下對於 Alluxio Master 的巨大壓力。可以通過配置 –o entry_timeout=T –o attr_timeout=T 進行優化。

  • 配置 max_idle_threads 避免頻繁線程創建銷毀引入 CPU 開銷

這是由於 FUSE 在多線程模式下,以一個線程開始運行。當有兩個以上的可用請求,則 FUSE 會自動生成其他線程。每個線程一次處理一個請求。處理完請求後,每個線程檢查目前是否有超過 max_idle_threads (默認 10)個線程;如果有,則該線程回收。而這個配置實際上要和用戶進程生成的 I/O 活躍數相關,可以配置成用戶讀線程的數量。而不幸的是 max_idle_threads 本身只在 Libfuse3 才支持,而 Alluxio Fuse 只支持 Libfuse2, 因此我們修改了 Libfuse2 的代碼支持了 max_idle_threads 的配置。

對 Alluxio 的優化

避免頻繁逐出(Cache Eviction)造成緩存抖動

由於深度學習訓練場景下,每次訓練疊代都是全量數據集的疊代,緩存幾個 TB 的數據集對於任何一個節點的存儲空間來說都是捉襟見肘。而 Alluxio 的默認緩存策略是為大數據處理場景(例如,查詢)下的冷熱數據分明的需求設計的,數據緩存會保存在 Alluxio 客戶端所在的本地節點,用來保證下次讀取的性能最優。具體來說:

alluxio.user.ufs.block.read.location.policy 默認值為 alluxio.client.block.policy.LocalFirstPolicy, 這表示 Alluxio 會不斷將數據保存到 Alluxio 客戶端所在的本地節點,就會引發其緩存數據接近飽和時,該節點的緩存一直處於抖動狀態,引發吞吐和延時極大的下降,同時對於 master 節點的壓力也非常大。因此需要 location.policy 設置為 alluxio.client.block.policy.LocalFirstAvoidEvictionPolicy 的同時,指定 alluxio.user.block.avoid.eviction.policy.reserved.size.bytes 參數,這個參數決定了當本地節點的緩存數據量達到一定的程度後,預留一些數據量來保證本地緩存不會被驅逐。通常這個參數應該要大於 節點緩存上限 X (100% - 節點驅逐上限的百分比) 。

alluxio.user.file.passive.cache.enabled 設置是否在 Alluxi 的本地節點中緩存額外的數據副本。這個屬性是默認開啟的。因此,在 Alluxio 客戶端請求數據時,它所在的節點會緩存已經在其他 worker 節點上存在的數據。可以將該屬性設為 false,避免不必要的本地緩存。

alluxio.user.file.readtype.default 默認值為 CACHE_PROMOTE。這個配置會有兩個潛在問題,首先是可能引發數據在同一個節點不同緩存層次之間的不斷移動,其次是對數據塊的大多數操作都需要加鎖,而 Alluxio 原始碼中加鎖操作的實現不少地方還比較重量級,大量的加鎖和解鎖操作在並發較高時會帶來不小的開銷,即便數據沒有遷移還是會引入額外開銷。因此可以將其設置為 CACHE 以避免 moveBlock 操作帶來的加鎖開銷,替換默認的 CACHE_PROMOTE。

緩存元數據和節點列表

通過 Alluxio 進行文件訪問的時候,默認會走遍從 master 獲取文件元數據->獲取 block 元數據->從 worker 獲取 block 的具體位置->真正讀取 block 數據的完整鏈路,這實際上會引入明顯的文件訪問延時。如果能將該數據文件的 block 信息緩存到客戶端內存中,會非常明顯的提升文件的訪問性能。

將 alluxio.user.metadata.cache.enabled 設置為 true, 可以避免二次訪問時仍需要訪問元數據的問題。alluxio.user.metadata.cache.max.size 可以設置最多緩存文件數量,當然這也要結合 Alluxio 客戶端的堆大小進行配置。

同時在每次選擇讀取數據的 worker 節點時,Alluxio master 節點也會不斷去查詢所有 worker 節點的狀態,這也會在高並發場景下引入額外開銷。

將 alluxio.user.worker.list.refresh.interval 設置為 2min 或者更長。

讀取文件也會不斷更新 last accesstime,實際上在高並發的場景下,這會對 Alluxio master 造成很大壓力。我們通過修改 Alluxio 代碼增加了開關,可以關閉掉 last accesstime 的更新。

充分利用數據本地性

數據本地性就是儘量將計算移到數據所在的節點上進行,避免數據在網絡上的傳輸。分布式並行計算環境下,數據的本地性非常重要。在容器環境下支持兩種短路讀寫方式:Unix socket 方式和直接文件訪問方式。

Unix Socket 的方式好處在於隔離性好,不需要 Alluxio Client 和 Alluxio Worker 容器運行在同樣的 Network,UTS,Mount 的 Namespace。但是它的性能比直接文件訪問要差一些,同時會引發 netty 的 OutOfDirectMemoryError。

而直接訪問文件的方式則所以需要確保同一台機器上運行的 Alluxio Worker 和 Alluxio Fuse 的主機名和 IP 地址一致,同時要保證 Alluxio Client 和 Worker 共享同樣緩存目錄,這種方式性能更好同時更加穩定。但是它實際上犧牲了隔離性,需要二者共享 Network,UTS,Mount 的 Namespace

我們目前選擇的方案是優先採用後者。

對 Java & Kubernetes 的優化

配置 ActiveProcessorCount

Runtime.getRuntime.availableProcessors控制的;而如果通過 Kubernetes 部署容器而不指定 cpu 資源的 request 數量,容器內 Java 進程讀到 proc 文件系統下的 cpushare 數量為 2, 而此時的 availableProcessors來自於 cpu_shares/1024,會被算成 1。實際上限制了容器內 Alluxio 的並發線程數。考慮到 Alluxio Client 屬於 I/O 密集型的應用,因此可以通過-XX:ActiveProcessorCount 設置處理器數目。這裡的基本原則是 ActiveProcessorCount 儘量設置得高些。

調整 GC,JIT 線程

JVM 的預設 GC,JIT 編譯線程數量取決於-XX:ActiveProcessorCount 的數量,但實際上也可以通過-XX:ParallelGCThreads -XX:ConcGCThreads -XX:CICompilerCount 等參數配置,可以將其設置的小些,避免這些進程頻繁的搶占切換,導致性能下降。

性能優化效果

在優化 Alluxio 之後,ResNet50 的訓練性能單機八卡性能提升了 236.1%,並且擴展性問題得到了解決,訓練速度在不但可以擴展到了四機八卡,而且在此場景下和生成數據相比性能損失為 3.29%(31068.8 image/s vs 30044.8 image/s)。而實際訓練時間方面,四機八卡在生成數據場景下需要 63 分鐘,而使用 Alluxio 需要 65 分鐘。

總結與進一步工作

在本文中,我們總結了 Alluxio 在高性能分布式深度學習模型訓練場景中落地的挑戰點,以及我們在優化 Alluxio 的實踐。進一步地,我們介紹了如何從多個層面提升 Alluxio Fuse 在高並發場景下性能優化的經驗。最後,我們實現的基於 Alluxio 優化的分布式模型訓練方案,並在 4 機 8 卡的 ResNet50 場景下進行了性能驗證,取得了很好的效果。

在進一步工作方面,對於高吞吐海量規模的小文件和高並發讀場景,Alluxio 還有一些在 page cache 的支持和 Fuse 層的穩定性方面的工作,我們阿里雲容器服務團隊也會和 Alluxio 開源社區以及南京大學戴海鵬、顧榮等老師一起繼續合作努力改進。我們相信通過工業界、開源社區和學術界和聯合的創新力量,能夠逐步降低計算存儲分離場景下深度學習訓練的數據訪問高成本和複雜度,進一步助力雲上普惠 AI 模型訓練。

致謝

感謝 Alluxio 團隊的范斌,邱璐,Calvin Jia,常鋮在整個方案的設計和優化過程中的巨大幫助,從 Alluxio 自身能力上對於元數據緩存系統做了顯著的提升,為 Alluxio 落地 AI 場景開啟了可能性。

作者簡介:

車漾,阿里雲高級技術專家,從事 Kubernetes 和容器相關產品的開發。尤其關注利用雲原生技術構建機器學習平台系統,是 GPU 共享調度的主要作者和維護者。

顧榮,南京大學副研究員,Alluxio 項目核心開發者,研究方向大數據處理,2016 年獲南京大學博士學位,曾在微軟亞洲研究院、英特爾、百度從事大數據系統實習研發。

今日福利

遇見陸奇

同樣作為「百萬人學 AI」的重要組成部分,2020 AIProCon 開發者萬人大會將於 7 月 3 日至 4 日通過線上直播形式,讓開發者們一站式學習了解當下 AI 的前沿技術研究、核心技術與應用以及企業案例的實踐經驗,同時還可以在線參加精彩多樣的開發者沙龍與編程項目。參與前瞻系列活動、在線直播互動,不僅可以與上萬名開發者們一起交流,還有機會贏取直播專屬好禮,與技術大咖連麥。

關鍵字: