实现多子图和有状态模型的服务部署
概述
MindSpore支持一个模型导出生成多张子图,拥有多个子图的模型一般也是有状态的模型,多个子图之间共享权重,通过多个子图配合实现性能优化等目标。例如,在鹏程·盘古模型网络场景,基于一段语句,经过多次推理产生一段语句,其中每次推理产生一个词。不同输入长度将会产生两个图,第一为输入长度为1024的全量输入图,处理首次长度不定文本,只需执行一次,第二图为输入长度为1的增量输入图,处理上一次新增的字,第二个图将执行多次。相对于优化之前仅有全量图执行多次,可实现推理服务性能的5-6倍提升。为此,MindSpore Serving提供了多子图功能,实现多张图之间的调度。
下面以一个简单的单卡模型场景为例,演示多子图模型部署流程,分布式场景可以参考鹏程·盘古模型模型Serving部署。
环境准备
运行示例前,需确保已经正确安装了MindSpore Serving,并配置了环境变量。MindSpore Serving安装和配置可以参考MindSpore Serving安装页面。
下载样例
请先下载样例。
导出多图模型
在export_model
目录下,使用export_matmul.py,构造一个包含Matmul和ReduceSum的网络,基于两个不同的输入导出MindSpore推理部署模型。
import os
from shutil import copyfile
import numpy as np
import mindspore as ms
from mindspore.nn import Cell
class Net(Cell):
"""Net"""
def __init__(self, matmul_size, init_val, transpose_a=False, transpose_b=False):
"""init"""
super().__init__()
matmul_np = np.full(matmul_size, init_val, dtype=np.float32)
self.matmul_weight = ms.Parameter(ms.Tensor(matmul_np))
self.matmul = ops.MatMul(transpose_a=transpose_a, transpose_b=transpose_b)
self.sum = ops.ReduceSum()
def construct(self, inputs):
"""construct"""
x = self.matmul(inputs, self.matmul_weight)
x = self.sum(x, 0)
return x
def export_net():
"""Export matmul net , and copy output model `matmul_0.mindir` and `matmul_1.mindir` to directory ../matmul/1"""
ms.set_context(mode=ms.GRAPH_MODE)
network = Net(matmul_size=(96, 16), init_val=0.5)
# subgraph 0: 128,96 matmul 16,96 -> 128,16 reduce sum axis 0-> 16
predict_data = np.random.randn(128, 96).astype(np.float32)
# pylint: disable=protected-access
ms.export(network, ms.Tensor(predict_data), file_name="matmul_0", file_format="MINDIR")
# subgraph 1: 8,96 matmul 16,96 -> 8,16 reduce sum axis 0-> 16
predict_data = np.random.randn(8, 96).astype(np.float32)
# pylint: disable=protected-access
ms.export(network, ms.Tensor(predict_data), file_name="matmul_1", file_format="MINDIR")
dst_dir = '../matmul/1'
try:
os.mkdir(dst_dir)
except OSError:
pass
dst_file = os.path.join(dst_dir, 'matmul_0.mindir')
copyfile('matmul_0.mindir', dst_file)
print("copy matmul_0.mindir to " + dst_dir + " success")
dst_file = os.path.join(dst_dir, 'matmul_1.mindir')
copyfile('matmul_1.mindir', dst_file)
print("copy matmul_1.mindir to " + dst_dir + " success")
if __name__ == "__main__":
export_net()
使用MindSpore定义神经网络需要继承mindspore.nn.Cell
。Cell
是所有神经网络的基类。神经网络的各层需要预先在__init__
方法中定义,然后通过定义construct
方法来完成神经网络的前向构造。使用mindspore
模块的export
即可导出模型文件。
更为详细完整的示例可以参考初学入门。
执行export_matmul.py
脚本,生成matmul_0.mindir
和matmul_1.mindir
文件,输入shape分别为[128,96]和[8,96]。
部署推理服务
配置服务
启动推理服务,可以参考matmul_multi_subgraphs,需要如下文件列表:
matmul_multi_subgraphs
├── matmul/
│ │── servable_config.py
│ └── 1/
│ │── matmul_0.mindir
│ └── matmul_1.mindir
└── serving_server.py
serving_server.py
为启动服务脚本文件。matmul
为模型文件夹,文件夹名即为模型名。matmul_0.mindir
和matmul_1.mindir
为上一步网络生成的模型文件,放置在文件夹1下,1为版本号,不同的版本放置在不同的文件夹下,版本号需以纯数字串命名,默认配置下启动最大数值的版本号的模型文件。servable_config.py为模型配置文件,其定义了Servable的方法
predict
。
模型配置文件内容如下:
from mindspore_serving.server import distributed
from mindspore_serving.server import register
model = register.declare_model(model_file=["matmul_0.mindir", "matmul_1.mindir"], model_format="MindIR",
with_batch_dim=False)
def process(x, y):
z1 = model.call(x, subgraph=0) # 128,96 matmul 16,96 -> reduce sum axis 0-> 16
z2 = model.call(y, subgraph=1) # 8,96 matmul 16,96 -> reduce sum axis 0-> 16
return z1 + z2
@register.register_method(output_names=["z"])
def predict(x, y):
z = register.add_stage(process, x, y, outputs_count=1)
return z
如果模型是有状态的,则需要在一个Python 函数Stage中完成对这个模型的所需要的多次调用,避免多个实例的干扰,多子图的模型一般也是有状态的模型。
例子中,process
函数中通过Model.call
接口分别调用两个子图,其中的每个子图也可以调用多次,subgraph
参数指定图的标号,从0开始,此编号在为图加载的序号,单机场景与declare_model
接口的model_file
的参数列表序号对应,分布式场景与startup_agents
接口的model_files
的参数列表序号对应。
启动Serving服务器
使用serving_server.py启动Serving服务器。
import os
import sys
from mindspore_serving import server
def start():
servable_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
servable_config = server.ServableStartConfig(servable_directory=servable_dir, servable_name="matmul",
device_ids=(0, 1))
server.start_servables(servable_config)
server.start_grpc_server("127.0.0.1:5500")
server.start_restful_server("127.0.0.1:1500")
if __name__ == "__main__":
start()
上述启动脚本将在设备0和1上共加载和运行两个matmul
推理副本,来自客户端的推理请求将被切割分流到两个推理副本。
当服务端打印日志Serving gRPC server start success, listening on 127.0.0.1:1500
时,表示Serving gRPC服务启动成功,推理模型已成功加载。
执行推理
通过gRPC访问推理服务,client需要指定gRPC服务器的网络地址。运行serving_client.py,调用matmul Servable的predict
方法,执行推理。
import numpy as np
from mindspore_serving.client import Client
def run_matmul():
"""Run client of distributed matmul"""
client = Client("localhost:5500", "matmul", "predict")
instance = {"x": np.ones((128, 96), np.float32), "y": np.ones((8, 96), np.float32)}
result = client.infer(instance)
print("result:\n", result)
assert "z" in result[0]
if __name__ == '__main__':
run_matmul()
执行后显示如下返回值,说明Serving推理服务已正确执行推理任务:
result:
[{'z': array([6528., 6528., 6528., 6528., 6528., 6528., 6528., 6528., 6528.,
6528., 6528., 6528., 6528., 6528., 6528., 6528.], }]