編輯整理:白木其爾、Hoh
內容來源:第四范式 | 先薦
出品平臺:DataFunTalk
導讀:隨著互聯網的高速發展和信息技術的普及,企業經營過程中產生的數據量呈指數級增長,AI 模型愈發復雜,在摩爾定律已經失效的今天,AI 的落地面臨著各種各樣的困難。本次分享的主題是分布式機器學習框架如何助力高維實時推薦系統。機器學習本質上是一個高維函數的擬合,可以通過概率轉換做分類和回歸。而推薦的本質是二分類問題,推薦或者不推薦,即篩選出有意愿的用戶進行推薦。本文將從工程的角度,講述推薦系統在模型訓練與預估上面臨的挑戰,并介紹第四范式分布式機器學習框架 GDBT 是如何應對這些工程問題的。
主要內容包括:
推薦系統對于機器學習基礎架構的挑戰
大規模分布式機器學習場景下,不同算法的性能瓶頸和解決思路
第四范式分布式機器學習框架 GDBT
01
推薦系統對于機器學習基礎架構的挑戰
傳統的推薦系統中,我們只用簡單的模型或者規則來擬合數據,就可以得到一個很好的效果 ( 因為使用復雜的模型,很容易過擬合,效果反而越來越差 )。但是當數據量增加到一定的數量級時,還用簡單的模型或者規則來擬合數據,并不能充分的利用數據的價值,因為數據量增大,推薦的效果上限也隨之提升。這時,為了追求精準的效果,我們會把模型構建的越來越復雜,對于推薦系統而言,由于存在大量的離散特征,如用戶 ID、物品 ID 以及各種組合,于是我們采用高維的模型來做分類/排序。
2. 強時效性帶來場景價值
隨著時間的推移,推薦場景面臨的問題也在發生著變化,尤其是新聞、信息類的推薦,物料的變化非常快。同時,用戶的興趣和意愿也在時刻發生著變化。我們的模型都是根據歷史數據總結出來的規律,距離當前時間越近的數據,對于預測越有指導意義。為了增強線上效果,就需要增加模型的時效性,按照數據價值的高低,將時效性分為:硬實時、軟實時、離線,這里重點介紹下硬實時和軟實時。
硬實時:
硬實時是指毫秒級到秒級的特征。這類特征往往具有指導性意義,同時對系統的挑戰也是最大的,很難做到毫秒級或秒級的更新模型。通常的做法是通過快速的更新特征數據庫,獲取實時特征,來抓取秒級別的變化。尤其是新用戶冷啟動問題,當新用戶登陸 APP,如果在幾秒內,特征數據庫就能收集到用戶的實時行為,從而快速的抓取到用戶的興趣愛好,可以在一定程度上解決冷啟動問題。
軟實時:
軟實時是指小時級到天級別的時間段。這時有足夠的時間做批量的模型訓練,可以周期性的更新模型的權重,使模型有更好的時效性。同時軟實時對算力的消耗也是最大的,因為天級別的更新和周級別的更新模型,效果差距非常大。
3. 充分發揮數據的價值
02
大規模分布式機器學習場景下,不同算法的性能瓶頸和解決思路
當前面臨的算力問題主要包括:
a. 數據量指數級增長,而摩爾定律已經失效。曾經有個玩笑,當程序員覺得程序跑得慢時,不需要優化代碼,只需睡上一覺,換個新機器就好了。但現在摩爾定律已經失效,我們只能想方設法的優化代碼和工程。
b. 模型維度高,單機內存難以承受,需要做分布式處理。
c. 模型時效性要求高,需要快迭代,會消耗大量的算力。這時,如何解決算力問題變得非常有價值。
2. 方案
可行的解決方案有:
分布式+異構計算解決擴展性問題:由于數據增長很快,單機的算力很難提升,尤其是 CPU 算力增長緩慢。我們可以用 GPU、加速卡來提供強有力的算力,用分布式的存儲來更新模型,解決模型的擴展問題。
大規模參數服務器解決高維問題:當模型大到單機放不下時,我們就會使用參數服務器來解決高維問題。
流式計算解決時效性問題:對于模型的時效性有一種省算力的方法是用流式計算來解決,但是流式計算非常容易出錯。
總結來說,就是如何優化模型訓練速度,采用流式計算可以一定程度上解決這個問題。
3. 線性加速并非易事
03
分布式機器學習框架 GDBT
GDBT 是一個分布式數據處理框架,配備了高性能分布式大規模離散參數服務器。其核心組件包括:分布式數據源、參數服務器、計算圖。基于 GDBT 框架我們實現了一系列的高維算法:如邏輯回歸、GBM ( 樹模型 )、DSN 等,以及自動特征和 AutoML 相關的算法。GDBT 的工作流程圖如上圖所示。
接下來,選擇 GDBT 框架中的幾個核心組件為大家詳細介紹下:
2. 分布式數據源 ( 數據并行 )
分布式數據源 ( DataSource ) 是做數據并行的必備組件,是 GDBT 框架的入口。DataSource 最重要的一點是做負載均衡。負載均衡有很多種做法,這里設計了一套爭搶機制,因為在線程調度中,線程池會采用 work stealing 機制,我們的做法和它類似:數據在一個大池子中,在每一個節點都盡可能讀屬于自己的數據,當消費完自己的數據時,就會去搶其它節點的數據,這樣就避免了節點處理完數據后的空置時間,規避了'一核有難八核圍觀'的現象。
由于 DataSource 也是對外的入口,因此我們會積極的擁抱開源生態,支持多種數據源,并盡可能多的支持主流數據格式。
最后,我們還優化了 DataSource 的吞吐性能,以求更好的效率。因為有的算法計算量實際上很低,尤其是邏輯回歸這種比較簡單的機器學習算法,對 DataSource 的挑戰是比較大的。
實驗結果:
這里我們用 pDataSource 對比了 Spark 和 Dask。Spark 大家都比較熟悉,Dask 類似 python 版的 Spark,Dask 最開始是一個分布式的 DataFrame,漸漸地發展成了一個分布式的框架。如上圖所示,由于我們在內存上的優化,通過對比吞吐量和內存占用,pDataSource 用30%的內存資源就可以達到 Spark2.4.4 120% 的性能。
3. 參數服務器
參數服務器類似于分布式的內存數據庫,用來存儲和更新模型。參數服務器會對模型進行切片,每個節點只存儲參數的一部分。一般數據庫都會針對 workload 進行優化,在我們的機器學習訓練場景下,參數服務器的讀寫比例各占50%,其訓練的過程是不斷的讀取權重、更新權重,不斷的迭代。
對于大部分高維機器學習訓練,參數服務器的壓力都很大。參數服務器雖然自身是分布式的,但參數服務器往往會制約整個分布式任務的擴展性。主要是由于高頻的特征和網絡壓力,因為所有的機器都會往參數服務器推送梯度、拉取權重。在實際測試中,網絡壓力非常大,TCP 已經不能滿足我們的需求,所以我們使用 RDMA 來加速。
機器學習中的高頻特征更新特別頻繁時,參數服務器就會一直更新高頻特征對應的一小段內存,這制約了參數服務器的擴展性。為了加速這個過程,由于機器學習都是一個 minibatch 更新,可以把一個 minibatch 當中所有高頻 key 的梯度合并成一個 minibatch,交給參數服務器更新,可以有效的減輕高頻 key 的壓力。并且在兩端都合并后再更新,可以顯著減輕高頻特征的壓力。
對于大規模離散的模型,參數服務器往往要做的是大范圍內存的 random massage。由于計算機訪問內存是非常慢的,我們平常寫代碼時可能會覺得改內存挺快的,其實是因為 CPU 有分級緩存,命中緩存就不需要修改內存,從而達到加速。同時 CPU 還有分級的流水線,它的指令是亂序執行的,在讀取內存時,可以有其它的指令插進來,會讓人覺得訪問內存和平常執行一條指令的時間差不多,實際上時間差了幾十到幾百倍。這對于執行一般的程序是可行的,但對于參數服務器的工作負載,是不可行的。因為其工作流程需要高頻的訪問內存,會導致大量的時間用在內存訪問上。所以,如何增加命中率就顯得尤為重要:
我們會修改整個參數服務器的數據結構。
我們做了 NUMA friendly。服務器往往不只一個 CPU,大多數是兩個,有些高端的會有四個 CPU。CPU 周邊會有內存,一個 CPU 就是一個 NUMA。我們盡量讓參數服務器所有的內存綁在 NUMA 上,這樣就不需要跨 CPU 訪問內存,從而提升了性能。
還有個難點是如何保證線程安全。因為參數服務器是多線程的,面臨的請求是高并發的,尤其是離線時,請求往往會把服務器壓滿。這時要保證模型的安全,就需要一個高效的鎖。這里我們自研了 RWSpinLock,可以最大化讀寫并發。受限于篇幅,這里就不再進行展開。
最終的效果可以支持每秒 KV 更新數過億。
4. 分布式機器學習框架的 Workload
① 分布式 SGD 的 workload
分布式 SGD 的 workload:
首先 DataSource 會從第三方的存儲去讀數據。這里畫了三個機器,每個機器是一條流水線,數據源讀完數據之后,會把數據交給 Process,由 Process 去執行計算圖。計算圖當中可能會有節點之間的同步,因為有時需要同步模式的訓練。當計算圖算出梯度之后,會和參數服務器進行交互,做 pull/push。最后 Process 通過 Accumulator 把模型 dump 回第三方存儲 ( 主要是 HDFS )。
② 樹模型的 workload
目前樹模型的應用廣泛,也有不少同學問到分布式的樹模型怎么做。這里為大家分享下:
首先介紹下 GBDT ( Gradient Boost Decision Tree ),通過 GBDT 可以學出一系列的決策樹。左圖是一個簡單的例子,用 GBDT 來預測用戶是否打游戲。對于 Tree1,首先問年齡是否小于15歲,再對小于15歲的用戶問是男性還是女性,如果是男性,會得到一個很高的分值+2。對于 Tree2,問用戶是否每天使用電腦,如果每天都使用,也會得到一個分值+0.9,將 Tree1 和 Tree2 的結果相加得到用戶的分值是2.9,是一個遠大于零的數字,那么該用戶很有可能打游戲。同理,如果用戶是位老爺爺他的年齡分值是-1,且他每天也使用電腦,分值也是+0.9,所以對于老爺爺來說他的分值是-0.1,那么他很有可能不會打游戲。這里我們可以看出,樹模型的關鍵點是找到合適的特征以及特征所對應的分裂點。如 Tree1,第一個問題是年齡小于15歲好,還是小于25歲好,然后找到這個分裂點,作為這個樹的一個節點,再進行分裂。
樹模型的兩種主流訓練方法:
? 基于排序:
往往很難做分布式的樹模型。
? 基于 Histogram:
DataSource 先從第三方的存儲當中讀數據,然后 DataSource 給下游做 Propose,對特征進行統計,掃描所有特征,為每個特征選擇合適的分類點。比如剛剛的例子,我們會用等距分桶,我們發現年齡基本上都是在0到100歲之間,可以以5歲為一個檔,將年齡進行等分,作為后面 Propose 的方案。有了 Propose 的點之后,由于每個機器都只顧自己的數據,所以機器之間要做一次 All Reduce,讓所有的機器都統一按照這些分裂點去嘗試分裂,再后面就進入了一個高頻更新、高頻找特征的過程:
首先我們會執行 Histogram 過一遍數據,統計出某一個特征,如年齡小于15歲的增益是多少,把所有特征的 Propose 點的增益都求出來。由于機器還是只顧自己的數據,所以當所有機器過完自己的數據之后還會做一次 All Reduce,同步總的增益。然后找一個增益最大的,給它進行分裂,不斷的執行這樣的過程。
其實這個過程最開始時,尤其是 XGboost,計算量都用在如何統計 Histogram 上,因為 Histogram 過數據的次數特別多,而且也是一個內存 random massege 的過程,往往對內存的壓力非常大。我們通常會做的優化是使用 GPU,因為顯存比內存快很多,因此樹模型可以用 GPU 加速。
04
面臨的網絡壓力及優化方向
a. 模型同步,網絡延遲成為瓶頸。首先分布式 SGD Workload 主要是模型同步,尤其是同步模式時,當機器把梯度都算好,然后同一時刻,幾十個幾百個節點同時發出 push 請求,來更新參數服務器,參數服務器承擔的壓力是巨大的,消息量和流量都非常大。
b. 計算加速,帶寬成為瓶頸。我們可以用計算卡加速,計算卡加速之后,網絡帶寬成為了瓶頸。
c. 突發流量大。在機器學習中,主要難點是突發流量。因為它是同步完成之后,立刻做下一步,而且大家都齊刷刷的做。另一方面 profile 是非常難做的。當你跑這個任務時會發現,帶寬并沒有用完,計算也沒有用完。這是因為該計算的時候,沒有用網絡帶寬,而用網絡的時候沒有做計算。
2. RDMA 硬件日漸成熟
隨著 RDMA 硬件的日漸成熟,可以帶來很大的好處:
低延遲:首先 RDMA 可以做到非常低的延遲,小于 1μs。1μs 是什么概念,如果是用傳統的 TCP/IP 的話,大概從兩個機器之間跑完整個協議棧,平均下來是 35μs 左右。
高寬帶:RDMA 可以達到非常高的帶寬,可以做到大于 100Gb/s 的速度。現在有 100G、200G 甚至要有 400G了,400G 其實已經超過了 PCIE 的帶寬,一般我們只會在交換機上看到 400G 這個數字。
繞過內核:RDMA 可以繞過內核。
遠端內存直接訪問:RDMA 還可以做遠端內存的直接訪問,可以解放 CPU。
用好這一系列的能力,可以把網絡問題解決掉。
3. 傳統網絡傳輸
傳統網絡傳輸是從左邊發一條消息發到右邊:
首先把樣本模型序列化,copy 到一段連續的內存中,形成一個完整的消息。我們再把消息通過 TCP 的協議棧 copy 到操作系統,操作系統再通過 TCP 協議棧,把消息發到對面的操作系統。對面的 application 從 OS buffer 把信息收回,收到一段連續的內存里,再經過一次反序列化,生成自己的樣本模型,供后續使用。
我們可以看到,在傳統的網絡傳輸中,共發生了四次 copy,且這四次 copy 是不能并行的,序列化之前也不能發送,沒發過去時,對方也不能反序列化。由于 CPU 主頻已達瓶頸,不能無限高,這時你的延遲主要就卡在這個流程上了。
4. 第一步優化
第一步優化是我們自研的序列化框架。我們一開始把樣本模型放在內存池中。而這個內存池是多段連續的內存,使任何數據結構都可以變成多段連續的內存。這個序列化的過程,其實就是打一個標記,標明這個樣本模型要發送,是一個 zero copy 的過程。可以瞬間拿到序列化后的信息,由網絡層通過 TCP 協議棧發到對端,對端收的時候也是不會收成一段大的內存,而是多段連續的內存。通過共享內存池的方式,可以減少兩次 copy,讓速度提升很多,但還是治標不治本。
5. 引入 RDMA
進而我們引入了 RDMA:
RDMA 可以直接繞過內核,通過另一種 API 直接去和網卡做交互,能把最后一次 copy 直接省掉。所以我們引入 RDMA 之后,可以變成一個大的共享內存池,網卡也有了修改操作內存的能力。我們只需要產生自己的樣本模型后,去戳一下網卡,網卡就可以傳輸到對面。對面可以直接拿來做訓練、做參數、做計算,整個流程變得非常快,吞吐也可以做到非常大。
6. 底層網絡 PRPC
我這里對比的是 BRPC 和 GRPC,BRPC 的性能是我現在看到的 RPC 當中最快的,但是因為它不支持 RDMA,所以被甩開了三到五倍。因為 GRPC 兼容性的工作特別多,所以 GRPC 的性能會更差一些。這個對比并不是非常的科學,因為我們最大的收益來源是 RDMA 帶來的收益。
7. 線上預估
線上大部分時間,我們離線訓練出的模型會放在 HDFS 上,然后把模型加載到參數服務器。會有一套 controller 去接受運維請求,參數服務器會給我們提供參數、預估服務對外暴露打分的接口。上圖是一個最簡單的線上預估的 Workload。
8. 流式更新、加速迭代
流式更新比較復雜:
大概是用戶有請求過來,會有數據庫把用戶、物品的信息聚合起來,再去預估打分,和剛剛最簡單的架構是一樣的。打分之后要把做好的特征發送到 message Queue,再實時的做 join。這時 API server 會接受兩種請求,一種是用戶請求打分,還有一種是用戶的 feedback ( 到底是贊,還是踩,還是別的什么請求 )。這時會想辦法得到 label,通過 ID 去拼 label 和 feature,拼起來之后進一步要把特征變成高維向量,因為變成高維向量才能進入機器學習的環節,由 Learner pull/push 去更新訓練的參數服務器,訓練參數服務器再以一種機制同步到預估的參數服務器。
有了這樣的一個架構,才能把流式給跑起來,雖然可以做到秒級別的模型更新,但是這個過程非常容易出錯。