Seldon 使用 (五): engine & graph
在上文,我们提到每个SeldonDeployment(简称sdep)的pod中都包含一个engine容器。这个engine容器,是由seldon-manager服务为每个sdep插入的,用于满足复杂推理场景的需求,如合并(Combine)多个模型推理结果,推理请求处理(Transform-Input),推理结果转换(Transform-Output),以及推理请求路由等对模型推理的请求和结果进行编排
1 示例一(Route):多臂赌博机模型服务
该示例是由seldon官方提供的,详细可见。创建sdep定义文件egreedy.yaml,如下:
apiVersion: machinelearning.seldon.io/v1alpha2
kind: SeldonDeployment
metadata:
name: egreedy
spec:
predictors:
- componentSpecs:
- spec:
containers:
- image: seldonio/mock_classifier:1.9.1
name: classifier-1
env:
- name: PREDICTIVE_UNIT_HTTP_SERVICE_PORT
value: "9000"
- image: seldonio/mock_classifier:1.9.1
name: classifier-2
env:
- name: PREDICTIVE_UNIT_HTTP_SERVICE_PORT
value: "9001"
- image: seldonio/mock_classifier:1.9.1
name: classifier-3
env:
- name: PREDICTIVE_UNIT_HTTP_SERVICE_PORT
value: "9002"
- image: seldonio/mab_epsilon_greedy:1.9.1
name: eg-router
env:
- name: PREDICTIVE_UNIT_HTTP_SERVICE_PORT
value: "9003"
graph:
children:
- name: classifier-1
type: MODEL
- name: classifier-2
type: MODEL
- name: classifier-3
type: MODEL
name: eg-router
parameters:
- name: n_branches
type: INT
value: '3'
- name: epsilon
type: FLOAT
value: '0.3'
- name: verbose
type: BOOL
value: '1'
type: ROUTER
svcOrchSpec:
env:
- name: SELDON_ENABLE_ROUTING_INJECTION
value: 'true'
name: multi-models-predictor
replicas: 1
说明如下:
graph定义中模型推理的入口是name指定的eg-router容器,该容器使用多臂赌博机算法,从三个childen容器中选择一个模型服务,然后将推理请求发送给该childen容器模型服务处理
graph中定义的eg-router容器和三个children容器,分别和ComponentSpec中定义了四个容器一一对应
eg-router容器的多臂赌博机算法,是使用1-epislon的概率选择使用历史最佳模型,或者epislon概率选择其他模型服务
当pod启动后,使用kubectl describe pod可以看到,该pod内有五个容器(container)。除了上述4个容器外,还有一个seldon-container-engine容器。engine容器负责编排上述四个容器,根据graph指定的类型(type)为ROUTER,将先请求发送给eg-router获取childern容器的index,然后根据这个index获取该容器的端口,完成最后的推理请求
可使用如下脚本,测试推理服务。其中sys.argv[1]参数,使用podIP+engine端口(可通过kubectl get pod -owide |grep seldon),或者serviceIP+engine端口(可通过命令kubectl get svc -owide |grep seldon获得)都可以。注:engine端口号是8000。
import requests
import sys
url = "http://%s/api/v1.0/predictions" % sys.argv[1]
req = {"data": {"ndarray": [[1.0, 2.0, 5.0]]}}
res_raw = requests.post(url, json=req)
print('status_code:', res_raw.status_code)
print(res_raw.text)
返回结果如下所示。注,本次请求使用的是classifier-3模型服务的推理结果。
{
"data":{
"names":["proba"],"ndarray":[[0.43782349911420193]]
},
"meta":{
"requestPath":{"classifier-3":"seldonio/mock_classifier:1.9.1"}
}
}
2 示例二(Transform):输入输出转换
该示例是seldon官方提供的,详细可查看。创建seldon-model.yaml,如下所示:
apiVersion: machinelearning.seldon.io/v1alpha2
kind: SeldonDeployment
metadata:
name: seldon-model
spec:
name: test-deployment
predictors:
- componentSpecs:
- spec:
containers:
- name: step_one
image: seldonio/step_one:1.0
- name: step_two
image: seldonio/step_two:1.0
- name: step_three
image: seldonio/step_three:1.0
graph:
name: step_one
endpoint:
type: REST
type: MODEL
children:
name: step_two
endpoint:
type: REST
type: MODEL
children:
name: step_three
endpoint:
type: REST
type: MODEL
children: []
name: example
replicas: 1
说明:
graph定义的模型推理入口是step_one容器,该容器负载完成transformer-input(即输入特征转换,例如过滤,默认值,标准化等)
然后模型推理服务由step_one容器的children,即step_two容器完成
最后,step_two容器的推理结果,再由它的children,即step_three容器完成
graph所定义的三个容器,分别对应到componentSpec中定义的三个容器
上述,step_one, step_two, step_three三个容器的处理过程,同样是由engine容器编排完成的。
3 engine服务如何实现
上面描述的两个推理服务编排(graph),它们都是由engine容器服务实现的。那么engine容器是如何实现的呢。
engine容器实现的服务是一个http服务,我们这里只看它的主要接口predictions。实现如下,详细可查看executor/api/rest/server.go源码文件
func (r *SeldonRestApi) predictions(w http.ResponseWriter, req *http.Request) {
seldonPredictorProcess := predictor.NewPredictorProcess(ctx, r.Client, logf.Log.WithName(LoggingRestClientName), r.ServerUrl, r.Namespace, req.Header, modelName)
reqPayload, err := seldonPredictorProcess.Client.Unmarshall(bodyBytes, req.Header.Get(http2.ContentType))
graphNode = &r.predictor.Graph
resPayload, err := seldonPredictorProcess.Predict(graphNode, reqPayload)
r.respondWithSuccess(w, http.StatusOK, resPayload)
}
说明:
对于每个请求,创建一个PredictorProcess。然后将反序列化后的请求,以及Graph,做为参数,调用PredictorProcess.Predict()方法,得到推理结果
Predict()方法处理过程如下,详细可查看executor/predictor/predictor_process.go源码:
func (p *PredictorProcess) Predict(node *v1.PredictiveUnit, msg payload.SeldonPayload) (payload.SeldonPayload, error) {
tmsg, err := p.transformInput(node, msg)
cmsg, err := p.predictChildren(node, tmsg)
response, err := p.transformOutput(node, cmsg)
return response, err
}
说明:
transformInput():如果node的type是MODEL,则调用该节点的Predict()方法处理。如果是TRANSFORMER,则调用该节点的TransformInput()方法处理。
predictChildren(): 如果该节点包含children,且type是ROUTER,则调用其route()方法获得index(即后续请求发给哪个children节点)
transformOutput(): 如果node的type是TRANFORMER,则调用该节点的TransformOutput()方法转换推理结果
总体,逻辑是比较清晰易懂的。这里只做简单介绍,可大致了解,如需要则可自行阅读源码。
4 总结
seldon通过提供engine容器及其所定义的type和相应的逻辑,一定程度上满足了模型服务编排的需要。
通常编排服务,都会涉及到底层资源的信息,包括服务地址,如何寻址,通信方式等,这些细节,对于不同的底层资源,实现方式会有所不同。但是这些细节,却又不是模型服务所关心的。而engine容器,正好可以满足这种需求,即屏蔽了底层资源细节,同时又满足了模型服务编排(graph)的需求。