模型部署的主要技术


在已有一个模型之后,怎么在线部署成为服务。

模型部署的背景

当我们已经进行了一次模型训练,得到一个心仪的模型后,如果部署成服务,通俗的说,就是让这个模型让需要的人随时能使用。通俗的说,服务或者我们所谓的上线,可以理解为打包了一个商品把它放到货架上,谁有需要就通过特定的暗号来随用随拿。
用户与服务间的关系
例如按需的,用户通过手机、电脑等入口进行提问,服务能马上响应返回一个结果,这就是一个服务。当然了,我们不是前端客户端等,并不需要直接面向他们做成服务,只需要做一个能满足后端调用需求的服务就够了。因此,本文其实要谈的就是怎么把模型打包成一个服务,这个服务在部署后就能够供调用方随时使用了。

模型的开发和部署流程

这里一般会涉及两个流程,分别是离线流程和在线流程。

离线流程中的内容是不需要放在服务里的,主要涉及模型的训练和校验部分,这个过程相信大家都非常熟悉了,除了打包服务之外,别的东西相信大家都懂,具体如下:

  • 数据收集
  • 模型训练
  • 模型验证
  • 打包服务上线

另外一个是在线流程,在线其实就是一个提供服务,内部提供模型预测能力的过程,所以主要流程如下:

  • 模型加载(一般每次启动一次就够)
  • 模型预测

当然了,可能还有一些类似预热的操作,这里一切从简,不展开。

技术点

  • 网络协议

所谓的网络协议,是指在网络通信过程中请求方和被请求方之间的通信协议,只有按照这份协议来通信才能生效,“协议”这两个字可以说用的非常传神了。算法服务常用的协议,基于rpc协议和基于http协议两种。

不谈原理,只谈优缺点和技术特点。RPC是一种比较严格但因为严格而传输效率比较高的方案,对于关系非常紧密的两个系统通常用RPC的方式传输,甚至希望达到“函数调用”级别的流畅感,这点相比http是有很大优势的。而HTTP则有更加灵活的方式,主要体现在字段和接口协议上,更灵活的接口协议非常适合大模块之间的通信。

算法角度,一般是这么考虑的:

  • RPC的传输很丝滑,非常适合小模块之间的通信,模型一般是大方案里面的一个组件,所以其实模型服务用RPC进行传输非常合适。
  • 对于算法的大模块,里面可能有各种规则、模型的大组合,这时候用HTTP协议能保证其和外界沟通的灵活性,例如算法多了新功能,不着急后端马上跟进完成接口开发。

所以更多情况下,对于单一的模型服务,一般采用RPC协议。

  • RPC

RPC的选择其实不少,例如百度的BRPC、谷歌的GRPC,这里比较常用的。GRPC本身不是局限语言的,除了python,c++之类的其实都有工具,服务化本身也有很大好处,语言上并不约束请求方和被请求方一致,c++一样可以请求python,沟通非常通畅,从参与的c++项目和python项目来看,都可以选着GRPC直接用。

另外,需要提到的是,针对Java,通过比较常用的方式还是Dubbo这套RPC框架,可以和Spring集成。

  • HTTP

HTTP本身不是一个很新的东西,计算机专业应该是基操了,结合简单的功能和外部支持,很快就能搞定一个HTTP。对于非计算机专业,学起来有些成本,但是结合文本其实入门门槛其实不是很高。

常用来做python的http服务的工具,主要是这3个,当然还有别的很多,可以在评论区推荐。

  • flask
  • tornado
  • django

通过这种工具能轻松完成python模型的服务化,可以说是非常轻松了,文章后面我用flask写一个简单的服务,并用request工具写一个请求脚本带大家理解一下简单服务化的流程吧。

  • 服务的负载均衡

说到服务化,很难不考虑负载均衡这个问题。所谓的负载均衡,主要发生在多个服务器或者是多进程之类的场景,为了保证能在线每秒几千几万甚至更多的请求,也就是所谓的并发,最好的方式就是增加服务器,每台机器部署同样的服务,说白了就是事太多了干不完那就招人,但是需要保证的是,每个人干的活要一样,不能招了人还盯着特定的一两个人干活,这肯定是不合理的,所以我们需要考虑的就是合理分配任务,保证大家都能干活,同时也能在有人生病的时候能对任务分配进行调整,保证不出现任务没完成的情况。

  • 单机多进程

单机多进程是python中常用的单机提升并发的技术,最简单的方式就是多进程,一般的会用gunicon进行控制,来个教程链接自己看吧。

当然,类似grpc之类的,python是支持多个worker进行的,具体也可以去看grpc的文档,里面有详细介绍:

当然了,如果想自己写,当然也是可以的,python有自己的多进程和多线程工具(注意,python的多线程是伪多线程,对非CPU非密集型的任务,如爬虫才能有用,像模型服务肯定要用做多进程了),自己可以写。

  • 多机房

多机房肯定就是多个服务了,我就介绍介绍zookeeper吧。

  • zookeeper

我觉得菜鸟教程上的这玩意说的是比较好的:

  • 一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。——菜鸟教程。
    学习链接

这套东西本身用来承接对GRPC服务的负载均衡非常合适,所以非常推荐能够学起来吧。

在线服务的要求

做了个服务,要上线,肯定要保证可用,具体可用上可以按照这几个角度来评估:

  • 单机性能:耗时,端到端耗时,模型耗时,即大到整体,小到每个模块的耗时。
  • 并发:能承受多少QPS(query per second),成功率等。
  • 单机资源:你有几台机器,分别什么配置,CPUGPU内存等,这里基本其实充钱不充钱的问题。

我一直对BERT之类的方案谨慎,很核心的原因就在这里,难度非常大,单机耗时高(这几年通过硬件和编译技术有所下降)、机器资源占用过等,所以成熟的算法应该是要把这些因素考虑到里面去的。

一个极简栗子

那就来个简单的例子吧。这里的例子用我用的比较多的tornado,我就以他为例来简单讲讲。

首先肯定是要准备一个模型的推理类,里面需要包含这几个东西:

  • 模型的初始化
  • 模型的预测

另外,由于tornado的原因,还需要一个初始化函数和一个预测函数,初始化主要用于特定场景的模型初始化,形成全局变量,预测函数主要用在服务内执行模型函数,来看看具体代码吧:

class DemoModel():
    # 写一个超简单的模型类
    def __init__(self):
        # 模型初始化
        self.cal_number = 0
    def predict(self, cal_number):
        # 模型预测
        self.cal_number += cal_number
        return self.cal_number
def init():
    global demo_model
    demo_model = DemoModel()
init()
def predict(number):
    return demo_model.predict(number)
if __name__ == "__main__":
    print(predict(2))
    print(predict(10))

这里的模型类我写的非常简单,基本的包括初始化用的是初始化一个参数来定的,一般大家都是用的“load”之类的方式来加载模型,另一个则是预测函数,这里的预测函数我也写的也很简单,其实大都是模型的预测,大家能理解就好。

另外值得注意的是两个函数,一个是初始化函数,另一个是预测函数,前者是为了初始化一个“global”的demo_model对象,而另一个则是预测函数,在tornado打包模型的时候会用到。

这个完成了一个模型类,然后来看怎么用tornado包装一个模型。这里设计一些tornado的基本知识,基本入门可以看看这个,看完后直接上代码。

import tornado.web
import tornado.ioloop
from loguru import logger
import json
from nlu_model.util.server.demo_model import DemoModel, predict
class DemoModelServer(tornado.web.RequestHandler):
    def post(self):
        # 获取post方式的参数
        recordid = self.get_argument("recordid", "TEST")
        cal_number = self.get_argument("cal_number", -1)
        request_body = json.loads(self.request.body.decode('utf-8'))
        recordid = request_body.get("recordid", "TEST")
        cal_number = request_body.get("cal_number", -1)
        logger.info("[{}]request: {}".format(recordid, cal_number))
        # 模型预测
        result = predict(cal_number)
        # 结果整理和返回
        output_result = {"recordid": recordid, "result": result}
        logger.info("[{}]response: {}".format(recordid, output_result))
        self.write(output_result)
if __name__ == '__main__':
    #创建一个应用对象
    app = tornado.web.Application([(r'/demo',DemoModelServer)])
    #绑定一个监听端口
    app.listen(8081)
    #启动web程序,开始监听端口的连接
    tornado.ioloop.IOLoop.current().start()

这个tornado类里面只有一个函数,就是POST,即这个服务内部使用post来进行传输的,POST函数里其实就是整个数据处理的流程:

  • 获取参数和整理参数
  • 模型预测
  • 结果整理和返回

流程非常清晰明确,大家可以根据这个格式去进行自己的拓展,例如数据合法性校验、模型和规则的协调等。

至于服务的启动,只需要在命令行里整这个就好了:

python server.py

服务就算起来了,注意这里是不会运行结束的,而是像一个长期挂着,一旦有人请求,这里就会打日志,打的时候POST中内容。

这里面还有一件事,就是我们要测一下我们的服务是否OK,这里就需要一个模拟请求的工具,也就是一个用于测试的客户端,我这里用requests也写了一个:

import requests
import json
import random
recordid = str(int(random.random() * 100000000))
json_data = {'recordid':recordid, "cal_number": 5}
json_data = json.dumps(json_data)
response = requests.post("http://localhost:8081/demo", json_data)
print(response.json())

这里模拟了一个recordid和一个cal_number的参数,然后用request去请求,这里的localhost表示本地,大家可以换成自己的ip或者是域名,demo是服务下的服务名,直接请求就可以得到结果。

python check_api.py

首先可以看到check_api这打印的结果(多来几次):

{'recordid': '77492467', 'result': 5}
{'recordid': '29791408', 'result': 10}
{'recordid': '46915366', 'result': 15}

而另一侧我们去看看正在挂载着的服务打印了什么东西:

2021-12-18 19:48:57.251 | INFO     | __main__:post:17 - [77492467]request: 5
2021-12-18 19:48:57.251 | INFO     | __main__:post:21 - [77492467]response: {'recordid': '77492467', 'result': 5}
2021-12-18 19:49:12.529 | INFO     | __main__:post:17 - [29791408]request: 5
2021-12-18 19:49:12.529 | INFO     | __main__:post:21 - [29791408]response: {'recordid': '29791408', 'result': 10}
2021-12-18 19:49:27.939 | INFO     | __main__:post:17 - [46915366]request: 5
2021-12-18 19:49:27.939 | INFO     | __main__:post:21 - [46915366]response: {'recordid': '46915366', 'result': 15}

得益于日志,我们看到了每次请求过程的输入和输出。

总结

本文给大家介绍了模型部署的最基本方案,告诉大家模型是怎么上线的,同时也给了大家一个很简单的demo级别的例子方便大家理解。当然还有很多复杂的组件没有和大家展开聊,例如模型数据的维护、日志系统等,日后有机会再说(这篇文章写了巨久)。

指的强调的是,这块东西对于一名算法工程师来说非常重要,大家可以根据我里面提到的名词进行更进一步的深入学习,另外对于一些服务方面的技术细节,大家需要看看计算机方面的数据进行补充,类似计算机网络。


文章作者: 沙九
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沙九 !
  目录
s's