通篇乾貨!縱觀 PolarDB-X 平行計算框架

語言: CN / TW / HK

簡介: 本文將對整個分散式並行執行框架做詳細的介紹,希望閱讀之後對我們的執行器有一個全面的認識。

作者:玄弟 七鋒

 

 

PolarDB-X 面向 HTAP 的混合執行器 一文詳細說明了PolarDB-X執行器設計的初衷,其初衷一直是致力於為PolarDB-X注入平行計算的能力,兼顧TP和AP場景,逐漸為其打造一款具備TB級資料處理能力的資料庫。為了做到這一點,借鑑了各種資料庫和大資料庫產品,包括分析型資料庫,實時數倉等,吸取了各方面的優勢來打造出一個全新的並行執行引擎。這裡將對整個分散式並行執行框架做詳細的介紹,希望閱讀之後對我們的執行器有一個全面的認識。

 

 

▶ 整體設計

 

PolarDB-X 是一個 Share Nothing 的資料庫,取樣了計算和儲存分離的架構。其中資料以分片的形式儲存於每個DN節點,計算節點叫CN。在計算過程中,DN和CN間、DN和DN、CN和CN間是通過千兆、萬兆網路通訊。每個CN節點上一般都會有排程元件、資源管理元件、RPC元件等。一個複雜的查詢,會被排程到多個CN節點上執行,考慮到資料一般都會根據均勻策略分到各個DN上,所以每個CN節點同時會訪問多個DN。

 

 

當用戶提交一條複雜的SQL語句,往往需要訪問多個DN節點,這個時候就會啟動並行排程策略,整個執行步驟可以簡單理解:

 

  1. 使用者所連線的這個 CN 將承擔查詢協調者(Query Coordinator)的角色;
  2. Query先發送給Query Coordinator,會首先經過優化器生成最新的Plan,然後會拆分到多個子計劃(Fragment), 每個Fragment可能會包含多個執行運算元。如果有的Framgnt負責掃描DN的話,它裡頭必定包含Scan運算元,從DN拉取資料;Fragment也可以包含Agg或者Join等其他運算元;
  3. Query Coordinator裡頭的排程器(Task Scheduler)會按照定義的排程邏輯將各個Framgnts封裝成Task,排程到合適的CN上執行,這裡頭可能會涉及到一些資源上的計算;
  4. 各個CN收到Task後,會申請執行資源,構造執行的上下文,開始啟動Task,並定期會向Query Coordinator彙報狀態;
  5. 各個Task間會通過資料傳輸通道(DTL)交換資料,當所有的Task都執行完畢後,會將資料結果返回給Query Coordinator,由它負責將結果返回給使用者;
  6. 成功返回給使用者後,Query Coordinator和被排程到的各個CN節點的Task會做清理動作,釋放計算資源。

 

整個流程大致就這樣,有細心的同學會發現我們的框架在DN上有一層叫Split概念。我們的資料是按分片儲存在各個DN上的,Split指的是資料分片partition的地址。對於包含掃描運算元Scan的 Task,會計算出需要訪問哪些 partition,這些 partition 分佈在哪些 DN 上,然後封裝成splits按比例劃分給這些掃描Task。但是實際執行過程中每個掃描Task並不是預分配好splits的,而是預分配部分splits給掃描Task,看哪一個Task掃描的更快就會從Query Coordinator繼續獲取餘下splits,這樣可以儘可能避免由於各個掃描Task資源不均衡導致的消費長尾現象。但是如果一個表只被分成了2個分片,是不是意味著掃描任務至多隻能是2,這可能起不到明顯的並行加速效果。所以我們也支援在分片上繼續按照分段做拆分,那麼這個時候的Split除了會記錄分片的地址,也會記錄在分片上分段的位移。按照分段做拆分後,即便資料的分片數量有限,執行過程我們依然可以啟動更多的掃描Task,並行去加速掃描。

 

 

 

▶ 執行計劃

 

執行引擎執行的是由優化器生成的分散式執行計劃。執行計劃由運算元組成。因為PolarDB-X的資料按照分片儲存到各個的DN節點上去,執行計劃執行也會盡可能的滿足資料分佈的locality,能下推的計劃會被放到DN上執行,不能下推的計劃會會被切分成一個個子計劃(Fragment),會被放到各個CN節點上執行。所以這裡我們需要關心如何將一個從優化器出來的計劃,拆分成分散式計劃,放到各個CN上執行?


為了更好地理解這個過程,我們這裡以一條簡單SQL: select * from (select useid, count(*)  as b from user_data group by userid) as T where T.b > 10 為例,經過優化器生成這樣的相對最優計劃:

 

 

針對並行執行計劃,為了更高效地執行儘量減少資料傳輸,可以把執行計劃按照計算過程是否需要資料重分佈(ReDistribution)分為不同片段(fragment)分佈到相應節點執行,並且把一些操作下推來減少掃描輸出的資料,上面的計劃可能就變成這樣的執行計劃,由多個子片段構成。

 

 

不同片段之間通過 NetWork Write/Read 運算元進行資料交換。更復雜的比如多表關聯(join)查詢,會有更多的片段和更復雜的資料交換模式。每個片段的併發度可以不同, 併發度是基於代價推匯出來的。多機排程的基本單位是Stage,Stage記錄了上下游片段的位置資訊,以便上下游之間建立網路通道(DTL)。每個片段排程到計算CN節點後,會被封裝成邏輯執行Task,比如fragment-1併發度是2的話,那麼會將Task-1.0和Task-1.1 兩個Task分別排程到兩個CN節點。

 

 

Task仍然是CN節點計算的邏輯單元,PolarDB-X執行器不僅僅可以支援單機並行能力(Parallel Query),也可以做多機並行(MPP)。所以在CN節點還引入了二層排程邏輯。當然二層排程的好處不僅僅於此,後面我們還會提到。這裡會繼續在Task內部根據運算元間資料交換的特性,繼續做切分,切分成不同Pipeline。

 

 

不同的Pipeline併發度也可以不同,每個Pipeline會根據處理的資料規模大小會計算出不同的併發度,生成具體的執行單元Driver,Driver間會根據二層排程確定上下游的本地通道(Local Channel)。

 

 

至此你應該可以瞭解從執行邏輯計劃轉化為分散式物理執行的整個過程。引入了一些新的名稱,這裡統一做下梳理:

 

  • Fragment:指的是邏輯執行計劃按照計算過程中資料是否需要重分佈,切割成的子計劃。
  • Stage:是由Fragment封裝而成的排程邏輯單位,Stage除了封裝Fragment外,還會記錄上下游Stage間的排程位置資訊。
  • Task:Stage並不會在CN上直接執行,他們是通過併發度分解成一系列可排程到CN上的Task, Task依然是邏輯執行單元。
  • Pipeline:對CN上的Task根據二層併發度做進一步切分,切分成不同的Pipeline。
  • Driver:一個Pipeline包含多個Driver,Driver是具體的執行單元,是一系列可執行運算元的集合。

 

一般來說針對一個複雜查詢,一個query包含多個Fragment,每個Fragment和Stage一一對應,每個Stage包含多個Tasks,每個Task會切分成不同的Pipeline,一個Pipeline包含了多個Driver。只有理解上面說的Fragment/Stage/Task/Pipeline/Driver這些概念,你才能更清楚瞭解我們接下來的設計。

 

 

▶ 排程策略

 

平行計算在執行之初,需要解決任務排程問題。排程的直白理解,就是將切分好的Task排程到各個CN節點去執行,充分利用各個CN的計算資源。這裡頭大家很容易有這些疑問:

 

1. 執行過程中各個CN節點的計算資源是不均衡了,那麼在多機排程中是如何將各個Task打散到不同CN節點去執行? 2. 和各個DN互動的Task是如何並行的拉資料的?比如某個邏輯表分成了16個物理表,分佈在4個DN節點上,有4個Driver去並行拉資料,每個Driver並不是均勻拉取4個物理表,而是根據自身的消費能力來確定拉取的物理表數量;多個Driver下發掃描任務會不會同時恰好落地一個DN節點上,導致某個DN成為瓶頸? 3. 我們完全可以在一個CN節點,同時排程多個Task執行,已經可以做到單機並行,為什麼還要二層排程?

 

一層排程(多節點間)

 

為了解決(1) 和 (2) 的問題,我們在CN節點內部引入了排程模組(Task Scheduler),主要負責Task在不同CN節點上的排程,這一層排程我們這裡稱之為一層排程,在這層排程中,同屬於一個Stage的多個Task一定會被排程到不同CN節點上去,確保一個CN節點只能有相同tage的一個Task。排程過程中通過心跳不斷維護Task狀態機,也維護著叢集各個CN節點Load資訊,整個排程是基於CN Load做排程的。多機排程流程如下所示:

 

 

 

Resource Manager(RM)是CN節點上個一個資源管理模組,RM會藉助Task心跳機制實時維護叢集各個CN節點的負載,Task Scheduler元件會基於負載選擇合適的CN節點下發執行任務,比如CN-1 負載相對叢集其他CN節點來說高很多,那麼當前查詢的Task會分發給其他CN節點,避免排程到CN-1節點去。執行器在執行Task任務時,Task並不是建立好的時候就確定了其消費DN splits的對映關係。各個Task按批次動態拉取splits進行消費, 直白理解就是誰的消費能力越強誰就有可能消費更多的splits。同樣為了解決同一個時刻多個任務同時消費同一個DN上的splits問題,我們在排程之初會將splits根據地址資訊按照Zig-zag方式,把各個DN上的splits打散到整個splits queue上去,消費的時候可以儘可能分攤各個DN壓力,這樣計算過程中也會充分利用各個DN的資源。

有了一層排程後,我們也可以將同屬於一個Stage的多個Task排程到同一個CN,這樣其實也可以做到單機並行。如果這樣設計的話,我們容易忽略兩個問題:

 

  • 一層排程的邏輯比較複雜,需要多次互動,一個CN內部需要同時維護各個Task的狀態,代價會比較大,這在TP場景是無法容忍的;
  • 一層排程中,併發度越高,生成Task就越多,這些Task間需要建立更多的網路傳輸通道。

 

二層排程(節點內部)

 

為了解決上述一層排程的不足,為此我們在參考Hyper的論文[1],引入了二層排程,既在CN節點內部單獨做單機並行排程,簡單來說我們會在Task內部藉助CN的本地排程元件(Local Scheduler),對Task做進一步的並行排程,讓Task在CN上執行,也可以做到並行執行。下圖中,Stage-1和Stage-2是上下游關係,各自併發度都是9,排程到3個CN節點執行。如果只有一層併發度的話,每個CN節點還會排程執行3個Task,那麼上下游之間總共會建立81個Channel,CN節點內部Task是相互獨立的,這樣缺點還是很明顯:

 

 

  1. 多個Channel,放大了網路開銷,同一份buffer會被髮送多次,傳送和接收對CPU和Memory都有代價;
  2. 資料傳送的物件是Task,資料本身有傾斜,會導致同節點內Task之間的負載不均衡(hash skew),存在長尾問題。

 

而一層排程和二層排程相結合的話,Stage-1和Stage-2的一層併發度是3,這樣每個CN節點只會有1個Task,Task內部併發度3。由於shuffle的物件是Task,所以Stage-1和Stage-2間只會建立9個Channel,大大減少了網路開銷,同時Task內部的3個Driver內資料是共享的,Task內部的所有的Driver可以共同消費接受到的資料,並行執行,避免長尾問題。針對於HashJoin,假設Ta為大表,Tb為小表,這兩個表做HashJoin,可以讓Ta和Tb同時shuffle到同一個節點做計算;也可以讓小表Tb廣播到Ta所在節點做計算,前者的網路代價是Ta+Tb,而後者的代價是N*Tb(N代表廣播的份數)。所以如果只有一層排程的話,N可能比較大,執行過程中我們可能會選擇兩端做shuffle的執行計劃;而一層和二層相結合的排程策略,可以讓執行過程中選擇BroadcastHashJoin,這樣可以避免大表做shuffle,提高執行效率。


此外在二層排程策略中,task內部的多執行緒很容易做到資料共享,有利於更高效的演算法。如下圖,同樣是HashJoin過程中,build端的Task內部多個執行緒(driver)協同計算:build端收到shuffle的資料後,多執行緒協同建立一個共享的hash表。這樣一個task只有一個build table,probe端收到shuffle資料後,也不用做ReDistribution了,直接讀取接受到資料,進行並行的probe。

 

 

 

 

▶ 並行執行

 

聊完排程,接下來應該是關心任務是如何在CN上執行,執行過程中遇到異常我們系統是如何處理的呢?

 

執行緒模型

 

說到執行,有經驗的同學可能會發現我們的排程並沒有解決排程死鎖問題,比如對於下面這樣一個執行計劃,兩表Join。一般會遇到兩種問題:

 

1. 如果先排程f3和f2的話,這個時候假設叢集沒有排程資源,則f1不能遲遲排程起來。而HashJoin的邏輯就是需要先構建buildTable,這裡f1剛好是build table部分。最終會導致執行死鎖:f1在等待f3和f2的計算資源釋放,而f2和f3又在等待f1構建完buildTable;

2. 如果f1先排程起來了,假設這個時候f2和f3沒有排程資源,這個時候f1從DN拉出來的資料,其實是無法傳送給f3的,因為f3還沒有被排程起來。

 

 

解決問題1,業界有很多方式,比較常見是在排程之初構建排程依賴關係(Scheduler Depedency):f1->f3-f2。而解決問題2,往往是將f1把DN拉出來的資料先放到記憶體中,實在放不下就落盤處理。可見處理上述兩個問題,執行框架不僅僅需要在多機排程上做複雜的排程依賴關係,同時還需要考慮對落盤的支援。而其實我們在排程的時候,並沒有去考慮排程依賴這個事情,我們是一次性把f1/f2/f3全部排程起來了,這個是為何呢?這就要說下我們執行中的邏輯執行緒模型概念。在大多數計算引擎中,一個查詢首先會通過資源排程節點,在各個CN上申請執行執行緒和記憶體,申請成功後,這些執行資源會被排程元件佔用,用來分配當前查詢的Task,不可以再被其他查詢所利用,這種是真實的執行資源,和排程資源相互繫結,當CN上可利用的執行資源不夠的時候,才會出現排程死鎖問題。而在PolarDB-X中,我們並沒有在排程的時候申請真實的執行緒資源,排程只需要考慮各個CN的負載,不需要考慮各個CN到底還剩多少可利用的真實資源。我們的執行緒模型也並沒有和排程資源綁死,每個Driver其實不獨佔一個真實的執行緒,換句話說,真實的執行緒也並沒有和排程資源一一對應。雖然說Driver是執行的基本單元,但是在排程上來看,它又是邏輯的執行緒模型而已。那是不是意味著只要有排程任務,都可以被成功排程到CN上去,答案是肯定的。一次性排程所有的執行單元到CN上去執行,對記憶體和CPU也是一種開銷。比如f2被執行起來後,但是f1並沒有執行完畢,那麼f2也會不斷執行,其資料其實也會被快取起來,但是也不能無限快取資料呀?為了解決這個問題,接下來就需要藉助我們的時間片執行了。

 

時間片執行

 

我們在每個CN節點內部會有一組執行執行緒池來執行這些Driver,每個Driver會排隊進入執行緒池參與計算,如果Driver被阻塞就會退出到Blocking佇列中,等待被喚醒。比如f2 driver 啟動後,從DN拉了資料放到有限空間buffer裡頭去,這個時候假設f1 driver都沒有結束,那麼f2 driver 對應的buffer就會滿,滿了後就會阻塞住,一旦阻塞我們的執行框架就會讓f2 driver從執行器退出來,加入到Blocking佇列中,簡單的說就是將計算資源騰讓出來,等待被喚醒。直到f1 driver都執行完畢後, f2 driver會被喚醒,執行框架就會將他移動到Pending佇列中,等待被排程到執行執行緒池中繼續執行。這裡頭還是會浪費點記憶體,但相對於CPU資源來說,記憶體資源還是比較充裕的。

 

 

時間片執行的核心就是需要判斷Driver何時會被Block的,總結起來被阻塞的原因一般分為三種情況:

 

  • 根據運算元依賴模型來確定,比如圖中f1 driver未執行完畢,那麼f2 driver其實也會被阻塞(這個是一個可配置的選項);
  • 計算資源不足(主要指記憶體),對應的driver會被掛起,等待資源釋放;
  • 等待DN響應,物理SQL下發給DN後,Driver會被掛起,等待物理SQL執行完畢。

 

除此之外我們在借鑑Linux 時間片排程機制,會在軟體層面上統計Driver的執行時長,超過閾值(500ms),也會被強制退出執行執行緒,加入到Pending佇列,等待下一輪的執行排程。這種軟體層面上的時間片排程模型,可以解決複雜查詢長時間佔用計算資源問題。其實實現起來也挺簡單的,就是每計算完一個批資料後,我們會對driver的執行時長進行統計,超過閾值,就退出執行緒池。下面貼出了Driver處理邏輯的部分虛擬碼,Driver在執行採用的是經典的Producer-Consumer模型,每消費一個Chunk我們就會累計時間,當超過既定閾值,就會退出來。

 

 

任務狀態機

 

高併發系統,頻繁地等待或者任務切換是常見的系統瓶頸。非同步處理是一種已經被證明行之有效地避免這些瓶頸,把高併發系統性能推到極致的方法。所以PolarDB-X執行器的整個後端,統一使用全非同步的執行框架;同時MPP執行過程涉及到多機的協調,所以這就要求我們在系統內部維護這些非同步狀態。非同步狀態的維護特別重要,比如某個查詢下的Task執行失敗,需要立即通知到整個叢集中該查詢正在執行的Task任務,以便立即中止,以防出現Suspend Task,造成資源不釋放問題。


所以在執行器內部,我們從三個維度(Task Stage Query)去維護狀態, 這三種State是相互依賴耦合的,比如Query 被Cancel,會立即通知其所有的Stage,各個Stage監聽到狀態變化,會及時通知給其所有的Task,只有等待Task都被Cancel後,Stage 最後的狀態才變更為Cancel,最終Query的狀態才被標記為Cancel。在這個過程中我們會引入對狀態機非同步監聽機制,一旦狀態傳送變更就會非同步回撥相關處理邏輯。通過維護這些狀態,我們也可以及時通過查詢或者監控診斷到任務是否異常,異常發生在哪個環節,也便於我們後期排查問題。

 

 

 

▶ 資源隔離

 

如果併發請求過多的時候,資源緊張會讓請求執行緒參與排隊。但是正在執行的執行緒,需要耗費比較多的計算資源(CPU和Memory)的時候,會嚴重影響到其他正常正在執行的Driver。這對我們這種面向HTAP場景的執行器是決定不被允許的。所以在資源隔離這一塊,我們會針對不同WorkLoad做計算資源隔離,但這種隔離是搶佔式的。

 

CPU

 

在CPU層面上我們是基於CGroup做資源隔離的,根據WorkLoad不同我們把CPU資源分為AP Group和TP Group兩組,其中對TP Group的CPU資源不限制;而對AP Group是基於CGroup做硬隔離,其CPU使用水位的最小閾值(cpu.min.cfs_quota)和最大閾值(cpu.max.cfs_quota)來做限制。執行執行緒分為三組: TP Core Pool 、AP Core Pool、SlowQuery AP Core Pool,其中後兩者會被劃分到AP Croup一組,做嚴格的CPU限制。Driver會根據WorkLoad劃分到不同的Pool執行。看似很美的實現,這裡頭依然存在兩個問題:

 

1. 基於COST識別的WorkLoad不準怎麼辦?

2. AP查詢比較耗資源,在同一個Group下的多個慢查詢相互影響怎麼辦?

 

出現問題(1)主要的場景是我們把AP型別的查詢識別成了TP,結果會導致AP影響到TP,這是不可以接受的。所以我們在執行過程中會監視TP Driver的執行時長,超過一定閾值後仍沒有結束的查詢,會主動退出時間片,然後將其它排程到AP Core Pool執行。而為了解決問題(2),我們會將AP Core Pool中長時間執行都未結束的Driver,進一步做優雅降級,排程到SlowQuery AP Core Pool執行。其中SlowQuery AP Core Pool會設定執行權重,儘可能降低其執行Driver的頻率。

 

 

MEMORY

 

在記憶體層面上,會將CN節點堆內記憶體區域大致可以分為四大塊:

 

  • TP Memory:用於存放TP計算過程中的臨時資料
  • AP Memory:用於存放AP計算過程中的臨時資料
  • Other:存放資料結構、臨時物件和元資料等
  • System Reserverd:系統保留記憶體

 

TP和AP Memory分別會有最大閾值和最小閾值限制,兩者記憶體使用過程中可以相互搶佔,但是基本原則是:TP Memory可以搶佔AP Memory,直到查詢結束才釋放;而AP Memory可以搶佔記憶體TP,但是一旦TP需要記憶體的時候,AP Memory需要立即釋放記憶體,釋放方式可以是自殺或者落盤。

 

 

 

▶ 資料傳輸層(DTL)

 

平行計算是充分利用各個CN資源參與計算,那麼DN與DN之間必然會存在資料互動。各個DN上的上下游的Task資料需要傳輸,比如上游的Task數量N,下游的Task數量是M,那麼他們之間的資料傳輸通道需要用到M*N個通道(Channel),同樣的我們將這些通道(Channel)的概念抽象成資料傳輸層。這個傳輸層的設計往往也會面臨兩個問題:

 

1. 通道分為傳送端和接受端,當傳送端源源不斷髮送資料,而接受端無法處理的話就會造成記憶體雪崩;

2. 資料在傳輸過程中丟失。

 

 

在業界實現資料傳輸主要有兩種傳輸方式:Push和Pull。Push就是傳送端往接受端推送資料,這裡頭為了避免接收端處理不過來,需要引入流控邏輯,一般的做法都是在接收端預留了槽位,當槽位被資料佔滿時會通知傳送端暫停傳送資料,當有接收端資料被消費空閒槽位出現時通知傳送端繼續傳送,這裡頭會涉及到傳送端和接收端的多次互動,流控機制相對比較複雜。Pull就是傳送端將資料先發送到buffer裡頭去,接收端按需從傳送端的的buffer拉資料,而當傳送端傳送的資料到buffer,接收端假設長時間不來拉資料,最後傳送端buffer滿了,也會觸發上游反壓,為了避免頻繁反壓,往往傳送端的buffer不應該設定太小。綜合起來我們選擇了pull方式來做。取樣pull方式也會遇到兩個問題:

 

1. 每個receiver一般會和上游多個sender建立連線,那麼每次都是通過廣播的方式從上游所有的sender拉資料嗎?

2. 一次從sender端到底請求多少的資料呢,即averageBytesPerRequest?

 

我們先回答問題(2),我們這裡會記錄上一次請求的資料量lastAverageBytesPerRequest、當前建連通道個數n以及上一次總共返回的資料量responseBytes,來計算出當前averageBytesPerRequest,具體的公式下面也給出了。至於問題(1),有了當前的averageBytesPerRequest後,結合目前receiver上buffer剩餘空間,可以估算出這一次需要向上遊幾個sender傳送請求。

 

 

 

在非同步通訊過程中為了保證傳輸可靠性,我們採用了類似tcp ack的方式,當receiver端帶著token去上游拉資料的時候,則表示當前token之前的資料均已經被receiver端消費完畢,sender可以釋放這些資料,然後將下一批資料以及nextToken返回給receiver端。

 

 

 

 

▶ 效果展示

 

前後說了很多幹貨,下面咱們來點簡單實際的東西。這裡以TPCH Q13為例來演示下執行器在不同場景下的加速效果,為了方便截圖在Q13後面都加了limit。該測試環環境下,CN和DN規格都是2*16C64G。

 

單機單執行緒下執行,耗時3min31s

 

使用Parallel Query加速,既單機多執行緒執行,耗時23.3s

 

使用MPP加速,既同時利用兩個CN節點的資源計算,耗時11.4s

 

 

▶ 總結

 

不管是簡單查詢,還是 Parallel Query和MPP場景下的複雜查詢,共用的都是一套執行框架。不同場景下對執行器的要求,更多的是併發度設定和排程策略的差異。相對於業界其他產品來說,PolarDB-X執行器主要特點:

 

  1. 在資源模式上使用的是輕量化的資源管理,不像大資料計算引擎,需要額外引入的資源管理的節點,做嚴格的資源預分配,主要考慮到我們的場景是針對於小叢集的線上計算;
  2. 在排程模型上執行器支援DAG排程,相對於MPP排程可以做到更加靈活的併發控制模型,各個Stage間、Pipeline間的併發可以不一樣;
  3. 區別與其他產品,AP加速引用的是外掛平行計算引擎,PolarDB-X並行執行器是內建的,不同查詢間共用一套執行模型,確保TP和AP享有一致的SQL相容性。

 

PolarDB-X平行計算在線上已經平穩運行了近兩年,這兩年來我們不僅僅在執行框架上做了很多穩定性工作,在運算元層的優化我們也沉澱了不少的技術。但這些還不夠,目前比較熱的是自適應執行,結合Pipeline模式的自適應執行挑戰比較大,我們近期也在研究,歡迎感興趣的朋友來拍拍磚,一起進步!

https://developer.aliyun.com/article/782922?utm_content=g_1000255362

本文為阿里雲原創內容,未經允許不得轉載。

 

分享到: