先声明下,本文属原创并付诸实践,过程中对我帮助最大的连接如下:
- www.cnblogs.com/techi/p/12994802.html
- www.jianshu.com/p/bd67b40e6b85
环境初始化
- 系统:ubuntu 16.04,安装完毕docker服务端以及客户端
- 显卡:P4*2,安装完毕GPU驱动、cuda、cudnn(因此不需要下载NVIDIA镜像)
-
-
名词定义
- 镜像->镜像文件
- 容器->实例化的镜像
安装tensorflow/serving:latest-gpu镜像
- 下载或者在线安装tensorflow/serving:latest-gpu:
sudo docker pull tensorflow/serving:latest-gpu
若从本地镜像文件拉取到仓库:
sudo docker load -i xxxx.tar.gz(镜像文件)
查看仓库镜像文件,比如看到刚入库的镜像id为“860c279d2fec”:
sudo docker images
修改下镜像名称与版本:
sudo docker tag 860c279d2fec(镜像ID) tensorflow/serving(镜像名):latest-gpu(版本号)
- 其他实用docker指令如下
镜像实例化 -> 容器:
sudo docker run -it tensorflow/serving:latest-gpu /bin/sh
查看容器清单:
sudo docker ps -a | grep tensorflow
查看某个容器的信息(端口等):
sudo docker inspect das2432esr32(容器ID)
进入容器:
sudo docker exec -it das2432esr32(容器ID) /bin/bash
容器化模型服务
单模型部署
- 部署
一般来说,模型部署指令如下:
sudo docker run -d -p 8501:8501 --mount type=bind,source=xxxx/pb_model/kt002/,target=/models/my_model -e MODEL_NAME=my_model -t tensor-serving:latest-gpu
- source指定宿主机模型存放的位置
- target指定容器内模型存放位置, 没有会新建
- MODEL_NAME为模型服务后的名称,注意与target的对照关系
- 容器会开放两个端口:8500(gRPC),8501(rest-api),我只需要8501端口
其中
但是考虑到我的主机被部署过K8S,且IP映射被设定,我就简单部署(坑)如下:
sudo docker run -d --network host --mount type=bind,source=xxxx/pb_model/kt002/,target=/models/my_model -e MODEL_NAME=my_model -t tensor-serving:latest-gpu
- 测试
访问http://localhost:8501/v1/models/my_model:predict,注意模型服务的输入必须在"inputs"下的json,包含模型生成时的signatures,输出是"outputs"。
多模型部署
只考虑每个模型只有一个版本的情况
-
首先看下宿主机的模型文件结构
xxxx/pb_model/ ├── kt002/ │ └── 100 │ ├── saved_model.pb │ └── variables │ ├── variables.data-00000-of-00001 │ └── variables.index │ ├── model.config # 配置文件 │ ├── xyj_bx002/ │ └── 200 │ ├── saved_model.pb │ └── variables │ ├── variables.data-00000-of-00001 │ └── variables.index
-
生成配置文件model.config(名字随意),这个文件完全使能于容器,内容编辑如下:
model_config_list:{ config:{ name:"kt", base_path:"/models/multi_model/kt002/", # 容器内模型存储的path model_platform:"tensorflow" }, config:{ name:"xyj_bx", base_path:"/models/multi_model/xyj_bx002/", # 容器内模型存储的path model_platform:"tensorflow" } }
-
启动docker服务,注意宿主机端口可用
sudo docker run -d --network host --mount type=bind,source=xxxx/pb_model/,target=/models/multi_model -t tensor-serving:latest-gpu --model_config_file=/models/multi_model/model.config
【指定固定的显卡】
需要配置容器的环境变量,上述部署指令添加:
-e NVIDIA_VISIBLE_DEVICES=0
- 测试
访问http://xxx.xxx.xxx.xxx:8501/v1/models/xyj_bx:predict即可,注意事项同上述。需要注意的一个点,POST时,矩阵数据转json需要先把np.array变量通过.tolist(),再json.dumps()即可。
再补充下吧,输入数据样式如下:
#----------------array转换为json----------------#
# 服务输入的json的key必须是inputs,模型本身的输入signatures是
# input_ids <-1,64>
# input_mask <-1,64>
send_dict = {'inputs':{'input_ids':0,'input_mask':0}}
send_dict['inputs']['input_ids'] = input_token['input_ids'].tolist()
send_dict['inputs']['input_mask'] = input_token['input_mask'].tolist()
json.dumps(send_dict)
工程化部署
- Flask部署
看实际情况了,模型部署的输入就是自己搭建神经网络的输入,本例是bert分类模型,输入是数据矩阵,我想做个中转服务把业务请求的文本信息,转换为数据矩阵,调用模型服务后,再解析成分类结果返回给业务。
因为是demo,没有并发需要,因此采用flask做个简单点的部署。
import flask,requests,json
import tokenization
import numpy as np
server = flask.Flask(__name__)
#--------------设置--------------#
# 结果解析
flag = {0:'A类',1:'B类',2:'C类'}
# 模型选择
model_names = {'kt':'kt','bx':'xyj_bx','xyj':'xyj_bx'}
def tokenizer(text_list):
"""把text_list转换为维度的模型输入格式数据:
input_ids <-1,64>
input_mask <-1,64>
"""
# 转换代码,此处省略若干行
return input_ids,input_mask
@server.route('/feeling', methods=['post'])
def feeling():
# 接收请求,判断分类是否符合要求
material = json.loads(request.get_data(as_text=True))
if material['category'] not in model_names.keys() :
return json.dumps({"result":"类型代码不符合:kt,xyj,bx"}, ensure_ascii=False)
# 模型选择
model_name = model_names[material['category']]
# 结构化数据
input_token = {}
input_token['input_ids'],input_token['input_mask'] = tokenizer(material['sentence'])
# 封装json
send_dict = {'inputs':{'input_ids':0,'input_mask':0}}
send_dict['inputs']['input_ids'] = input_token['input_ids'].tolist()
send_dict['inputs']['input_mask'] = input_token['input_mask'].tolist()
predict_request = json.dumps(send_dict)
# 模型服务调用REAT-API
url = 'http://xxx.xxx.xxx.xxx:8501/v1/models/%s:predict'%model_name
response = requests.post(url, data=predict_request)
result= response.json()
# 结果解析
result = np.array(result['outputs'])
result_predict_index = result.argmax(axis=1)
result_predict_finally = [(flag[j],str(result[i][j])) for i,j in enumerate(result_predict_index)]
return json.dumps({"result":result_predict_finally}, ensure_ascii=False)
if __name__ == '__main__':
server.run(debug=True, port=9120, host='0.0.0.0')# 指定端口9120
- 写个客户端测试下:
import requests,json
from requests.adapters import HTTPAdapter
url = 'http://xxx.xxx.xxx.xxx:9120/feeling'
s = requests.Session()
s.keep_alive =False
s.mount(url, HTTPAdapter(max_retries=3))
data = {'category':'kt','sentence':["左侧下方一个直径2厘米的破损",
"有它和净化机一起开着",
"空调已按完",
"吱吱响",
"声音很小"]}
data_json = json.dumps(data)
try:
r = s.post(url,data_json,timeout=5)
print(r.text)
输出:
{"result": [["C类", "0.8891114"], ["B类", "0.627251387"], ["B类", "0.849841714"], ["C类", "0.852092803"], ["A类", "0.99754554"]]}
至此大功告成!
赶紧走,要尿裤子了!