DPP推薦引擎架構(gòu)升級演進之路
一、DPP整體架構(gòu)
DPP依賴于算法平臺的引擎服務(FeatureServer,召回引擎, 精排打分),提供“開箱即用”的召回,粗排,精排服務。采用“熱加載技術”解決算法平臺的工程和算法同學策略迭代效率問題,支持策略隨時發(fā)布,讓他們可以專注于業(yè)務邏輯,即可擁有穩(wěn)定的推薦在線服務。
圖1.0 DPP服務整體架構(gòu)
平臺特性
- 快速迭代:通過系統(tǒng)解耦,實現(xiàn)算法、策略的快速迭代。
- 效果分析自動化:打通數(shù)據(jù)平臺,BI數(shù)據(jù)分析標準化。
- 靈活實驗:通過分層實驗平臺,支持多層多實驗的靈活配置。
- 診斷方便:落地各子流程中間結(jié)果,支持算法、策略的細化分析;提供方便的監(jiān)控告警,運維,時光機等問題排查工具。
二、DPP引擎演進
DPP編排引擎的迭代分為了3個階段:固定編排,靈活編排,圖化DAG編排;均是在策略迭代過程中,圍繞著“迭代效率”提升的不斷進化。下面分別介紹下各階段引擎產(chǎn)生的背景及其方案。
固定編排 - DPP-Engine
推薦業(yè)務一般都可以抽象為“召回->融合->粗排->精排->干預”等固定的幾個階段,每個階段通常是有不同的算法或工程同學進行開發(fā)和維護,為了提升迭代效率,通過對推薦流程的抽象,將各階段的邏輯抽象為“組件"+"配置”,整體的流程同樣是一個配置,統(tǒng)一由“編排引擎”進行調(diào)度,同時提供統(tǒng)一的埋點/日志等。讓工程或算法同學可以關注在自己的業(yè)務模塊和對應的邏輯,而框架側(cè)也可以做統(tǒng)一的優(yōu)化和升級。
DPP-Engine就是在此基礎上,將業(yè)務策略抽象為“初始層->召回層->融合層->粗排層->精排層->干預層”這6層, 有DPP負責串行調(diào)度這6層,每一層有若干個組件組成,各層將結(jié)果進行合并后傳遞到下一層(也就是List)。
圖1.2-1 DPP-Engine層編排
通過分層,DPP-Engine較好的支持了業(yè)務的快速迭代,業(yè)務“各層”的開發(fā)同學可以獨立迭代。但是隨著場景的增多,對“靈活”編排有了更多的需求,比如不固定6層,層內(nèi)可有自己的"編排"等。
其次對于DPP平臺同學來說,DPP-Engine嵌入在DPP系統(tǒng)內(nèi), 不利于引擎的迭代和維護。
靈活編排 - BizEngine
BizEngine根據(jù)策略同學提供的組件及其編排流程,負責執(zhí)行和調(diào)度,包括組件間的并發(fā)。它在推薦系統(tǒng)鏈路中的位置如下圖:
圖1.3-1 DPP系統(tǒng)(BizEngine)
目前在BizEngine看來,“組件”是策略開發(fā)的最小粒度,策略同學在DPP-后臺中可以在場景維度劃分桶(小流量桶, 分層桶),在桶可以配置不同的層編排,默認為6層:INIT層->召回層->融合層->粗排層->精排層->干預層。分別在層內(nèi)可以配置不同的組件。一次請求中,BizEngine負責按層進行調(diào)度(層與層之間為串行調(diào)度),層內(nèi)的組件根據(jù)組件間的依賴進行串行或者并發(fā)調(diào)度。
圖1.3-2 編排管理及其配置協(xié)議
用戶請求到DPP后, 會通過AB分流得到該請求(用戶)命中的所有實驗(包括桶,層,實驗),DPP解析命中配置后,可以構(gòu)建出BizEngine需要的入?yún)?編排配置(桶配置+實驗配置+組件配置),它會根據(jù)層及組件的配置構(gòu)建出執(zhí)行的層Stages,按組件維度提交到各線程池進行同步或異步的調(diào)度,流程可參考下圖:
圖1.3-3 BizEngine的組件調(diào)度和執(zhí)行
從上圖可以看到我們是按層進行串行調(diào)度的,“分層”是按推薦的業(yè)務策略邏輯來分的,符合工程算法同學的分工和職責,特別是算法同學通常有各自負責的領域(召回模型,粗排模型,精排模型,干預),按層劃分和進行實驗可以有效提高迭代效率,做到相互之間不影響?!敖M件”則是BizEngine層內(nèi)調(diào)度的單元,但是目前組件的粒度可大可小,比如社區(qū)的部分場景,他們在組件內(nèi)拆分了更細粒度的Steps,并且獨立于組件進行調(diào)度(依賴DPP場景線程池或自定義線程池),因此策略代碼即負責了策略的邏輯, 還需要負責策略邏輯單元(Step)的調(diào)度。由此可以看出BizEngine未來的可進一步發(fā)展的方向:
- 按層進行串行調(diào)度,即便層與層組件之間為串行,也需要按層調(diào)度,存在一定開銷。
- BizEngine的線程調(diào)度和策略內(nèi)自定義調(diào)度的沖突,線程池資源難于實現(xiàn)高效利用。
- “組件粒度”問題:目前看策略同學實現(xiàn)的組件對BizEngine來說是“邏輯黑盒”,里面可能是CPU,也可能是IO,也可能是一個發(fā)起并發(fā)任務的模塊,可能涉及自定義的線程池資源。
- 隨著業(yè)務不斷迭代, 策略組件的遷移和重構(gòu)成本逐漸上升;缺少“組件”/“代碼”共享及發(fā)現(xiàn)的機制,不利于我們通過“組件復用”的方式去提升迭代效率。
圖化DAG - DagEngine
為什么需要做圖化?
那為什么要去做“圖化”/“DAG”呢?其實要真正要回答的是: 如何應對上面看到的挑戰(zhàn)?如何解決BizEngine目前發(fā)展碰到的問題?
從業(yè)界搜推領域可以看到不約而同地在推進“圖化”/“DAG”。 從TensorFlow廣泛采用之后,我們已經(jīng)習慣把計算和數(shù)據(jù)通過采用算子(Operation)和數(shù)據(jù)(Tensor)的方式來表達,可以很好的表達搜索推薦的“召回/融合/粗排/精排/過濾”等邏輯,圖化使得大家可以使用一套“模型”語言去描述業(yè)務邏輯。DAG引擎也可以在不同的系統(tǒng)有具體不同的實現(xiàn),處理業(yè)務定制支持或者性能優(yōu)化等。
通過圖(DAG)來描述我們的業(yè)務邏輯,也帶來這些好處:為算法的開發(fā)提供統(tǒng)一的接口,采用算子級別的復用,減少相似算子的重復開發(fā);通過圖化的架構(gòu),達到流程的靈活定制;算子執(zhí)行的并行化和異步化可降低RT,提升性能。
圖化架構(gòu)
圖化是要將業(yè)務邏輯抽象為一個DAG圖,圖的節(jié)點是算子,邊是數(shù)據(jù)流。不同的算子構(gòu)成子圖,用于邏輯高一層的封裝,子圖的輸出可以被其他子圖或者算子引用。圖化后,策略同學的開發(fā)任務變成了開發(fā)算子,抽象業(yè)務領的數(shù)據(jù)模型。不用再關心“并行化異步化”邏輯,交由DAG引擎進行調(diào)度?!八阕印币笪覀円暂^小粒度支持,通過數(shù)據(jù)實現(xiàn)節(jié)點的依賴。
圖化定義了新的業(yè)務編排框架,對策略同學來說是“新的開發(fā)模式”,可分為3個部分:一個是我們會定義算子/圖/子圖的標準接口和協(xié)議,策略同學實現(xiàn)這些接口,構(gòu)建業(yè)務的邏輯圖;二是DAG引擎,負責邏輯圖的解析,算子的調(diào)度,保證性能和穩(wěn)定性;三是產(chǎn)品化,DAG Debug助手支持算子/圖/子圖的開發(fā)調(diào)試,后臺側(cè)提供算子/子圖/圖的可視化管理。整體架構(gòu)參考下圖:
圖4.0.0 - DPP圖化框架
圖4.0.1 - DagEngine
圖化核心設計和協(xié)議
1.算子
- 算子接口定義Processor<O>
public interface Processor<O> {
/**
* 執(zhí)行邏輯
*
* @param computeContext 執(zhí)行上下文信息
* @return 返回執(zhí)行結(jié)果
*/
DataFrame<O> run(ComputeContext computeContext, DataFrame... inputs);
}
- 算子注解@DagProcessor
通過注解可對算子進行描述和提供運行時信息:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DagProcessor {
/**
* 標記IO/CPU, 影響DagEngine的調(diào)度
* @return
*/
String type() default "IO";
/**
* 算子描述
*
* @return String
*/
String desc() default "";
/**
* 用于標識該算子會輸出的一些中間值, 可用于做運行時的依賴校驗
* 可理解為是算子OP的side effects
*/
String sideValues() default "";
}
- 依賴配置@ConfigAnno)
算子通過注解(@ConfigAnno) 一是聲明算子需要的配置(通過DPP-后臺實驗配置進行配置), 二是運行時DAG引擎會對注解的值進行注入。
- 依賴數(shù)據(jù)@DependsDataAnno
算子節(jié)點上游的數(shù)據(jù),通過接口參數(shù)也會透傳過來(DataFrame數(shù)組),算子內(nèi)可以通過dataFame.getName()獲取數(shù)據(jù)的唯一標識(請求session內(nèi)唯一)。
算子的返回作為該算子的輸出數(shù)據(jù),通過name可以獲取, 比如 @DependsDataAnno(name = "某一路的輸出",desc = "recall1")。
寫策略邏輯過程中的中間變量是我們必不可少的,算子可以通過注解@DagProcessor#sideValues聲明會輸出那些數(shù)據(jù)(names),通過name 可以獲取。
比如依賴了同一個算子(多個實例),它的輸出name是一樣的,下游獲取需要通過這個優(yōu)先級決定。
Note:@DagProcessor#sideValues 可能作為必須的,只有sideValues聲明了的數(shù)據(jù),才可以被依賴算子引用,這有助于我們管理和防止依賴不存在的數(shù)據(jù)。
Note:算子獲取sideValue時有多相同name的數(shù)據(jù)時,通過配置指定算子優(yōu)先級。
2.圖/子圖
- 圖/子圖/配置文件
圖分為圖和子圖,一個場景可以有多個圖,可按垂直桶制定不同的圖;子圖定位為業(yè)務邏輯模版,可以將若干個獨立算子組裝為具有特定業(yè)務含義的“子圖”,子圖和算子一樣可在場景大“圖”中進行配置,即運行時可有多個“實例”,實現(xiàn)邏輯的復用和配置化。
圖或子圖通過“配置文件”文件來描述,考慮到可讀性和是否支持注釋等特性,確定選用yaml來定義。
- 協(xié)議
子圖
## 子圖(定位為邏輯模版, 包含: 若干個算子及其依賴關系, 子圖的配置及其默認值
## Note: 子圖的配置實際為算子的配置, 在算子中引用
name: 'Recall子圖1' ## 場景全局唯一
type: 'subgraph' ## 標記圖為"子圖"
configs: ## 子圖包含配置項( 指定默認值 )
- name: 'configKey1' ##
value: '默認值Value, 可為string, json等, xx'
# - 其他配置及其默認值
# ...
nodes: ## 子圖包含的所有算子, 通過dpends指定依賴.
## 比如一路召回
- name: 'fistRecallOp1'
op: 'com.dag.demo.recrecall.FirstRecallOP'
depends: []
# 指定子圖中該算子的默認值
configs:
- name: 'configKey1'
value: 'fistRecallOp1s value'
- name: 'otherRecall1'
op: 'com.dag.demo.recrecall.OtherRecallOP'
depends: ['fistRecallOp1']
圖
## 圖(場景邏輯描述, 包含若干個算子或子圖, 及其他們的依賴關系, 圖的配置及其默認值(Note: 圖的配置實際為算子的配置, 在算子中引用)
name: '場景圖Name' ## 場景全局唯一
type: 'graph'
configs: ## 圖包含配置項( 指定默認值 )
- name: 'configKey1'
value: '默認值Value, 可為string, json等'
# - 其他配置及其默認值
# ...
nodes: ## 圖包含的所有算子或子圖, 通過dpends指定依賴.
## 比如一路召回
- name: 'fistRecallOp1'
op: 'com.dag.demo.recrecall.FirstRecallOP'
depends: []
- name: 'otherRecall1'
op: 'com.dag.demo.recrecall.OtherRecallOP'
depends: ['fistRecallOp1']
## 子圖1( 為`Recall子圖1`的實例 )
- name: 'someRecallComplex1'
op: '$Recall子圖1' ## 依賴該子圖
configs: ## 子圖包含配置項( 指定默認值 )
- name: 'configKey1'
value: 'fistRecallOp1s value'
## 覆蓋這兩個算子的默認值
targets: ['recallGroup1', 'dssmRandomBatchRecall']
## todo 修改op的配置
##
depends: ['fistRecallOp1']
## 子圖2( 為`Recall子圖1`的實例 )
- name: 'someRecallComplex1'
op: '$Recall子圖1' ## 依賴該子圖
depends: ['fistRecallOp1']
3.算子配置如何獲取? 如何配置?
圖通過算子(子圖)+數(shù)據(jù)依賴的DAG描述了業(yè)務的邏輯關系,配置的作用就是影響邏輯如何生效。這些配置通過“實驗/AB”來決定,不同的實驗就是對圖或算子的不同配置。
- 默認值
配置的默認值通過兩種方式指定:1/ 算子變量的默認值(代碼方式);2/ 圖或者子圖的Confgis#key#defaultValue
- 運行時的值
算子某個配置在運行時的值,是通過該次請求命中的所有實驗進行配置融合和覆蓋后得到的。
- 如何配置?
實驗配置中:
需要考慮配置key在子圖和算子中的name作為前綴,規(guī)則為<subGraph'sName>.<op'sName>.<key'sName>,若算子不在子圖中(即, 直接配置在主圖中),那么配置為_.<op'sName>.<key'sName>。
算子代碼中:
通過注解 @ConfigAnno(key = "key'sName")來獲取對的key'sName的值. 運行時DAG引擎負責識別<subGraph'sName> 和<op'sName>。
配置支持json和dto對象綁定,DAG運行時實現(xiàn)緩存和校驗指定Json配置和類的映射,@ConfigAnno(key = "somepojo.value",isJson = true,clazz = SomePojo.class),DAG引擎負責反序列化。
圖化相關特性/結(jié)果
- DPP圖化落地廣告/社區(qū)等場景。
- 圖桶推全SOP流程: 通過引入"分支"概念,圖桶推全變?yōu)楹先隡aster,待推全各桶由各Owner自行合并Master。支持一分支綁定多桶。簡化了場景編排迭代流程。
- 圖編輯可視化: 支持算子及其依賴的表單化修改,提升修改效率和易用性。
三、總結(jié)
DPP編排引擎經(jīng)歷了固定編排,靈活編排到圖化DAG編排三個階段,持續(xù)提升策略迭代效率。
圖化DAG編排在我們落地的一些場景中顯著提升了性能,同時新的開發(fā)模式要求策略同學關注算子級別的實現(xiàn),減少對調(diào)度邏輯的關注。在產(chǎn)品側(cè)DPP-后臺提供了產(chǎn)品化工具支持本地調(diào)試和可視化管理。
未來我們可以進一步探索圖化DAG編排在更多業(yè)務場景中的應用,尤其是需要高性能和靈活定制的場景。其次加強算子復用機制和標準化建設,降低組件遷移與重構(gòu)成本, 持續(xù)優(yōu)化DagEngine的高性能特性,如DataFrame數(shù)據(jù)結(jié)構(gòu)的使用,以進一步提升系統(tǒng)性能。 并且隨著引擎及機器學習平臺圖化的推進,我們有可能也去端到端鏈路上實現(xiàn)“全圖化”。用一張圖描述一個業(yè)務的策略邏輯。