導讀:隨著網際網路的高速發展和資訊科技的普及,企業經營過程中產生的資料量呈指數級增長,AI 模型愈發複雜,在摩爾定律已經失效的今天,AI 的落地面臨著各種各樣的困難。本次分享的主題是分散式機器學習框架如何助力高維實時推薦系統。機器學習本質上是一個高維函式的擬合,可以透過機率轉換做分類和迴歸。而推薦的本質是二分類問題,推薦或者不推薦,即篩選出有意願的使用者進行推薦。本文將從工程的角度,講述推薦系統在模型訓練與預估上面臨的挑戰,並介紹第四正規化分散式機器學習框架 GDBT 是如何應對這些工程問題的。
主要內容包括:
- 推薦系統對於機器學習基礎架構的挑戰
- 大規模分散式機器學習場景下,不同演算法的效能瓶頸和解決思路
- 第四正規化分散式機器學習框架 GDBT
- 面臨的網路壓力及最佳化方向
01
推薦系統對於機器學習基礎架構的挑戰
1. 海量資料+高維特徵帶來極致效果
傳統的推薦系統中,我們只用簡單的模型或者規則來擬合資料,就可以得到一個很好的效果 ( 因為使用複雜的模型,很容易過擬合,效果反而越來越差 )。但是當資料量增加到一定的數量級時,還用簡單的模型或者規則來擬合資料,並不能充分的利用資料的價值,因為資料量增大,推薦的效果上限也隨之提升。這時,為了追求精準的效果,我們會把模型構建的越來越複雜,對於推薦系統而言,由於存在大量的離散特徵,如使用者 ID、物品 ID 以及各種組合,於是我們採用高維的模型來做分類/排序。
2. 強時效性帶來場景價值
隨著時間的推移,推薦場景面臨的問題也在發生著變化,尤其是新聞、資訊類的推薦,物料的變化非常快。同時,使用者的興趣和意願也在時刻發生著變化。我們的模型都是根據歷史資料總結出來的規律,距離當前時間越近的資料,對於預測越有指導意義。為了增強線上效果,就需要增加模型的時效性,按照資料價值的高低,將時效性分為:硬實時、軟實時、離線,這裡重點介紹下硬實時和軟實時。
硬實時:
硬實時是指毫秒級到秒級的特徵。這類特徵往往具有指導性意義,同時對系統的挑戰也是最大的,很難做到毫秒級或秒級的更新模型。通常的做法是透過快速的更新特徵資料庫,獲取實時特徵,來抓取秒級別的變化。尤其是新使用者冷啟動問題,當新使用者登陸 APP,如果在幾秒內,特徵資料庫就能收集到使用者的實時行為,從而快速的抓取到使用者的興趣愛好,可以在一定程度上解決冷啟動問題。
軟實時:
軟實時是指小時級到天級別的時間段。這時有足夠的時間做批次的模型訓練,可以週期性的更新模型的權重,使模型有更好的時效性。同時軟實時對算力的消耗也是最大的,因為天級別的更新和周級別的更新模型,效果差距非常大。
3. 充分發揮資料的價值
因此,為了更好的模型效果,我們需要處理海量資料、高維模型和實時特徵,而這一切都需要 AI 基礎架構提供充沛的算力。
02
大規模分散式機器學習場景下,不同演算法的效能瓶頸和解決思路
1. 算力問題
當前面臨的算力問題主要包括:
a. 資料量指數級增長,而摩爾定律已經失效。曾經有個玩笑,當程式設計師覺得程式跑得慢時,不需要最佳化程式碼,只需睡上一覺,換個新機器就好了。但現在摩爾定律已經失效,我們只能想方設法的最佳化程式碼和工程。
b. 模型維度高,單機記憶體難以承受,需要做分散式處理。
c. 模型時效性要求高,需要快迭代,會消耗大量的算力。這時,如何解決算力問題變得非常有價值。
2. 方案
可行的解決方案有:
- 分散式+異構計算解決擴充套件性問題:由於資料增長很快,單機的算力很難提升,尤其是 CPU 算力增長緩慢。我們可以用 GPU、加速卡來提供強有力的算力,用分散式的儲存來更新模型,解決模型的擴充套件問題。
- 大規模引數伺服器解決高維問題:當模型大到單機放不下時,我們就會使用引數伺服器來解決高維問題。
- 流式計算解決時效性問題:對於模型的時效性有一種省算力的方法是用流式計算來解決,但是流式計算非常容易出錯。
總結來說,就是如何最佳化模型訓練速度,採用流式計算可以一定程度上解決這個問題。
3. 線性加速並非易事
單靠堆機器在機器學習上是不能直接加速的,稍有不慎就會陷入"一核有難八核圍觀"的場景。現在很多分散式的計算都有單點的設計,這會極大的降低系統的擴充套件性。機器學習需要很多機器更新同一個模型,這就需要同步,不管是執行緒同步,還是程式同步,或者機器間依賴網路節點同步。一旦做不好,會消耗大量時間,這時你會發現,寫個單機的程式可能會更快一點。
03
分散式機器學習框架 GDBT
1. 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 加速。
目前,XGBoost、lightGBM 都支援 GPU 加速。我們也支援了用 FPGA 加速整個過程,但是我們發現 Histogram 和 All Reduce 是交替執行的,Histogram 的時間短了,All Reduce 的時間長了,就回到了剛才說到的問題:機器多了之後,發現大家都在互動,但互動的時間比統計 Histogram 的時間還長。
04
面臨的網路壓力及最佳化方向
1. 網路壓力大
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 去更新訓練的引數伺服器,訓練引數伺服器再以一種機制同步到預估的引數伺服器。
有了這樣的一個架構,才能把流式給跑起來,雖然可以做到秒級別的模型更新,但是這個過程非常容易出錯。
今天的分享就到這裡,謝謝大家。