[WIP]nexus 全图化引擎——hnsw算子化实践

 

Backgrounds

我最近一直在想,今后的搜广推架构如何发展,在可预见的未来,全图化必然是一个大趋势。

绝大多数搜广推后端技术选型都是分布式微服务框架,各个组件之间通过RPC调用来实现联动:每个微服务应当只承担单一的职责,这使得服务更加易于开发和维护,各个不同业务组件模块化。然而随着系统熵增(业务复杂度骤增,代码库💩山越堆越烂),开发、维护成本 和 协同工作成本逐步升高,新人上手门槛也越来越高。

以TensorFlow为代表的机器学习推理框架成为现代搜广推的核心推理引擎之后,召回海选粗精排在线全部的算力分配 和 业务迭代开始围绕着TensorFlow的inference领域展开。于是我在想能否借用TF 分布式图执行框架的能力,将业务逻辑抽象成一个一个的算子,最后的业务逻辑。

Q2刚好有个契机,OKR里有一条hnsw op化的P2需求(我其实更感兴趣,但是得高优推进其他项目,因此zentih/neuxs的代码都是周末在自己的服务器上写的,所以直接放在GitHub作为自己的探索项目了),自己按照TF-Op全图化的思路搭建了一下faiss::HNSW的图化项目。

Overview

开发算子,编辑图,用GraphDef的语言来业务逻辑,用nexus 加载 biz graph,rpc协议转发到 runGraph(调用 session->run),如下:

Op defs

以 RequestInitOp 为例,从HNSW模型中,获取 entry_point 给 GatherNeighborsOp

custom variant

可以自定义variant 作为算子的输入输出,注意实现 encode() 和 decode() 方法就行:

Stateful Resource

TensorFlow 算子是没有状态的,Op内随着请求数据流动到各个算子前后,无法为每一次rpc调用update检索时状态(context)。

我们注意到部分算子可以通过ResourceManager来维护状态,但是在Graph serving的时候,有状态算子在多个并发请求中共享状态,可能导致竞争条件和不一致的状态更新。

OpkernelContext 传递的是设备信息,ResourceMgr等,和biz相关的上下文状态无法通过 OpkernelContext* ctx 传递。我们加了两类Resource:SessionResource 和 QueryResource

TensorFlow
contains
contains
contains
references
provides
OpKernel::Compute
uses step_id to find
GET_SESSION_RESOURCE(ctx)
GET_QUERY_RESOURCE(session_resource)
query_resource
SessionResource
IndexManager
Metrics
Logger
Session
LocalDevice
QueryResource

 

SessionResource

SessionResource在从loadGraphDef创建session 的时候初始化,表征当前这个biz graph加载后需要的那些Resource,如IndexManager,Logger, Metrics等。这些基础资源存在于

我在LocalDevice 类加了一个接口,获取session_resource

QueryResource

QueryResource 可以理解为ctx,可以在各个step/node 之间传递一次请求上下文信息,存储在session_resource 中,按照ctx->step_id来对照,

加了几个语法糖, 在OpKernel::Compute(ctx) 的时候,获取query_resource

step_id | run_id

在 TF 中,step_id 是一个用于跟踪每次计算步骤的唯一标识符,特别是在分布式环境或调试中非常有用。它通常在运行计算图时生成,用于标识每个独立的计算步骤。step_id 的生成过程和使用主要与 TensorFlow 的内部执行引擎有关。

每次调用 Session::Run 方法时,都会为当前计算步骤生成一个新的 step_id。这有助于跟踪每个计算步骤的执行,特别是在分布式环境中进行调试或性能分析时。

我们将step_id,从外部传入,单独创建一个RunIdAllocator

在session run之前申请 get(),在run结束之后放回 put(runid), 在biz中,也用一个原子变量,来维护这个状态(fetch_sub ,fetch_add):

 

Biz Grpah Defs

所有的算子都写好之后,我们可以开始搭建业务的GraphDef了,以HNSW serving为例,将我们一个构建好的faiss::IndexHNSW索引加载到python中,读取meta信息:

然后用这些元信息来编辑出一个计算图,并dump 到文本文件中(GraphDef的prototext格式,可读且容易编辑)

 

可视化 netrion

 

runGraph

在runGrpah 内部,实际调用的 GraphContext::run()

 

load
GraphResponse
outputs
run_metas
NamedRunMetadata
name
run_meta_data
GraphRequest
biz
timeout
graph_info
run_options
GraphInfo
inputs
targets
fetches
NamedTensorProto
name
tensor
biz_graph.pbtxt
GraphService::runGrpah

RPC forward | Delegation

GraphServiceImpl::runGraph,为Session::Run的入口,本地server 需要实现rpc 接口的时候:

ZenithService::Recall为例,ZenithService-> GraphService 的转发发生在服务内部,类似于一个Delegation:

ClientZenithServiceGraphServicealt[runGraph Forward]Forwarding rpc within the serverSession::RunZenithRequestProcess ZenithRequestGraphRequestGraphResponseProcess GraphResponseZenithResponseClientZenithServiceGraphService

 


测试效果(WIP)

 

 

Reference