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