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