From dd14fafd75b74b17e7621b00faf0af7ab9135d56 Mon Sep 17 00:00:00 2001 From: ShiningZhang Date: Fri, 6 May 2022 17:08:32 +0800 Subject: [PATCH] update doc --- doc/Offical_Docs/5-4_HTTP_API_CN.md | 198 +++++++++++++++++++ doc/Offical_Docs/5-5_RPC_API_CN.md | 93 +++++++++ doc/Offical_Docs/6-3_Request_Cache_CN.md | 17 ++ doc/Offical_Docs/6-4_Encryption_CN.md | 42 ++++ doc/Offical_Docs/6-6_OP_CN.md | 140 +++++++++++++ doc/Offical_Docs/6-7_Model_Ensemble_CN.md | 228 ++++++++++++++++++++++ 6 files changed, 718 insertions(+) create mode 100644 doc/Offical_Docs/5-4_HTTP_API_CN.md create mode 100644 doc/Offical_Docs/5-5_RPC_API_CN.md create mode 100644 doc/Offical_Docs/6-3_Request_Cache_CN.md create mode 100644 doc/Offical_Docs/6-4_Encryption_CN.md create mode 100644 doc/Offical_Docs/6-6_OP_CN.md create mode 100644 doc/Offical_Docs/6-7_Model_Ensemble_CN.md diff --git a/doc/Offical_Docs/5-4_HTTP_API_CN.md b/doc/Offical_Docs/5-4_HTTP_API_CN.md new file mode 100644 index 00000000..b8ec660a --- /dev/null +++ b/doc/Offical_Docs/5-4_HTTP_API_CN.md @@ -0,0 +1,198 @@ +# HTTP 方式访问 Server + +Paddle Serving 服务端目前提供了支持 Http 直接访问的功能,本文档显示了详细信息。 + +## 基本原理 + +Server 端支持通过 Http 的方式被访问,各种语言都有实现 Http 请求的一些库,下边介绍使用 Java/Python/Go 等语言通过 Http 的方式直接访问服务端进行预测的方法。 + +**一. Http 方式:** + +基本流程和原理:客户端需要将数据按照 Proto 约定的格式(请参阅[`core/general-server/proto/general_model_service.proto`](../../core/general-server/proto/general_model_service.proto))封装在 Http 请求的请求体中。 +Server 会尝试去 JSON 字符串中再去反序列化出 Proto 格式的数据,从而进行后续的处理。 + +**二. Http + protobuf 方式:** + +各种语言都提供了对 ProtoBuf 的支持,如果您对此比较熟悉,您也可以先将数据使用 ProtoBuf 序列化,再将序列化后的数据放入 Http 请求数据体中,然后指定 Content-Type: application/proto,从而使用 http + protobuf 二进制串访问服务。 +实测随着数据量的增大,使用 JSON 方式的 Http 的数据量和反序列化的耗时会大幅度增加,推荐当您的数据量较大时,使用 Http + protobuf 方式,目前已经在 Java 和 Python 的 Client 端提供了支持。 + + +## 示例 + +我们将以 examples/C++/fit_a_line 为例,讲解如何通过 Http 访问 Server 端。 + +**一. 获取模型:** + +```shell +sh get_data.sh +``` + +**二. 开启服务端:** + +```shell +python3.6 -m paddle_serving_server.serve --model uci_housing_model --thread 10 --port 9393 +``` +服务端无须做任何改造,即可支持 BRPC 和 HTTP 两种方式。 + + +## 客户端访问 + +**一. HttpClient 方式发送 Http 请求(Python/Java):** + +为了方便用户快速的使用 Http 方式请求 Server 端预测服务,我们已经将常用的 Http 请求的数据体封装、压缩、请求加密等功能封装为一个 HttpClient 类提供给用户,方便用户使用。 + +使用 HttpClient 最简单只需要四步:1、创建一个 HttpClient 对象。2、加载 Client 端的 prototxt 配置文件(本例中为 examples/C++/fit_a_line 目录下的 uci_housing_client/serving_client_conf.prototxt)。3、调用 connect 函数。4、调用 Predict 函数,通过 Http 方式请求预测服务。 + +此外,您可以根据自己的需要配置 Server 端 IP、Port、服务名称,设置 Request 数据体压缩,设置 Response 支持压缩传输,模型加密预测(需要配置 Server 端使用模型加密)、设置响应超时时间等功能。 + +1. Python 的 HttpClient 使用示例如下: + +``` +from paddle_serving_client.httpclient import HttpClient +import sys +import numpy as np +import time + +client = HttpClient() +client.load_client_config(sys.argv[1]) + +client.connect(["127.0.0.1:9393"]) +fetch_list = client.get_fetch_names() + +new_data = np.zeros((1, 13)).astype("float32") +new_data[0] = [0.0137, -0.1136, 0.2553, -0.0692, 0.0582, -0.0727, -0.1583, -0.0584, 0.6283, 0.4919, 0.1856, 0.0795, -0.0332] +fetch_map = client.predict( + feed={"x": new_data}, fetch=fetch_list, batch=True) +print(fetch_map) +``` + +2. Java 的 HttpClient 使用示例如下: + +``` +boolean http_proto(String model_config_path) { + float[] data = {0.0137f, -0.1136f, 0.2553f, -0.0692f, + 0.0582f, -0.0727f, -0.1583f, -0.0584f, + 0.6283f, 0.4919f, 0.1856f, 0.0795f, -0.0332f}; + INDArray npdata = Nd4j.createFromArray(data); + long[] batch_shape = {1,13}; + INDArray batch_npdata = npdata.reshape(batch_shape); + HashMap feed_data + = new HashMap() {{ + put("x", batch_npdata); + }}; + List fetch = Arrays.asList("price"); + + Client client = new Client(); + client.setIP("127.0.0.1"); + client.setPort("9393"); + client.loadClientConfig(model_config_path); + String result = client.predict(feed_data, fetch, true, 0); + + System.out.println(result); + return true; +} +``` + +Java 的 HttpClient 更多使用示例详见[`java/examples/src/main/java/PaddleServingClientExample.java`](../../java/examples/src/main/java/PaddleServingClientExample.java)接口详见[`java/src/main/java/io/paddle/serving/client/Client.java`](../../java/src/main/java/io/paddle/serving/client/Client.java)。 + +如果不能满足您的需求,您也可以在此基础上添加一些功能。 + + +**二. curl方式发送Http请求:** + +```shell +curl -XPOST http://0.0.0.0:9393/GeneralModelService/inference -d ' {"tensor":[{"float_data":[0.0137,-0.1136,0.2553,-0.0692,0.0582,-0.0727,-0.1583,-0.0584,0.6283,0.4919,0.1856,0.0795,-0.0332],"elem_type":1,"name":"x","alias_name":"x","shape":[1,13]}],"fetch_var_names":["price"],"log_id":0}' +``` + +其中 `127.0.0.1:9393` 为 IP 和 Port,根据您服务端启动的 IP 和 Port 自行设定。 + +`GeneralModelService`字段和`inference`字段分别为 Service 服务名和 rpc 方法名。 + +-d 后面的是请求的数据体,json 中一定要包含上述 proto 中的 required 字段,否则转化会失败,对应请求会被拒绝。 + +需要注意的是,数据中的 shape 字段为模型实际需要的 shape 信息,包含 batch 维度在内。 + +1. message + +对应 rapidjson Object, 以花括号包围,其中的元素会被递归地解析。 + +```protobuf +// protobuf +message Foo { + required string field1 = 1; + required int32 field2 = 2; +} +message Bar { + required Foo foo = 1; + optional bool flag = 2; + required string name = 3; +} + +// rapidjson +{"foo":{"field1":"hello", "field2":3},"name":"Tom" } +``` + +2. repeated field + +对应 rapidjson Array, 以方括号包围,其中的元素会被递归地解析,和 message 不同,每个元素的类型相同。 + +```protobuf +// protobuf +repeated int32 numbers = 1; + +// rapidjson +{"numbers" : [12, 17, 1, 24] } +``` +3. elem_type + +表示数据类型,0 means int64, 1 means float32, 2 means int32, 20 means bytes(string) + +4. fetch_var_names + +表示返回结果中需要的数据名称,请参考模型文件 serving_client_conf.prototxt 中的`fetch_var`字段下的`alias_name`。 + +**三. Http压缩:** + +支持 gzip 压缩,但 gzip 并不是一个压缩解压速度非常快的方法,当数据量较小时候,使用 gzip 压缩反而会得不偿失,推荐至少数据大于 512 字节时才考虑使用 gzip 压缩,实测结果是当数据量小于 50K 时,压缩的收益都不大。 + +1. Client 请求的数据体压缩 + +以上面的 fit_a_line 为例,仍使用上文的请求数据体,但只作为示例演示用法,实际此时使用压缩得不偿失。 + +```shell +echo ' {"tensor":[{"float_data":[0.0137,-0.1136,0.2553,-0.0692,0.0582,-0.0727,-0.1583,-0.0584,0.6283,0.4919,0.1856,0.0795,-0.0332],"elem_type":1,"shape":[1,13]}],"fetch_var_names":["price"],"log_id":0}' | gzip -c > data.txt.gz +``` + +```shell +curl --data-binary @data.txt.gz -H'Content-Encoding: gzip' -XPOST http://127.0.0.1:9393/GeneralModelService/inference +``` + +**注意:当请求数据体压缩时,需要指定请求头中 Content-Encoding: gzip** + +2. Server 端 Response 压缩 + +当 Http 请求头中设置了 Accept-encoding: gzip 时,Server 端会尝试用 gzip 压缩 Response 的数据,“尝试“指的是压缩有可能不发生,条件有: + +- 请求中没有设置 Accept-encoding: gzip。 + +- body 尺寸小于 -http_body_compress_threshold 指定的字节数,默认是 512。gzip 并不是一个很快的压缩算法,当 body 较小时,压缩增加的延时可能比网络传输省下的还多。当包较小时不做压缩可能是个更好的选项。 + +这时 server 总是会返回不压缩的结果。 + +如果使用 curl,通常推荐使用 --compressed 参数来设置 Response 压缩,--compressed 参数会自动地在 http 请求中设置 Accept-encoding: gzip,并在收到压缩后的 Response 后自动解压,对于用户而言,整个压缩/解压过程就像透明的一样。 + +```shell +curl --data-binary @data.txt.gz -H'Content-Encoding: gzip' --compressed -XPOST http://127.0.0.1:9393/GeneralModelService/inference +``` + +若您只是在 Http 请求头中通过 -H'Accept-encoding: gzip' 设置了接收压缩的信息,收到的将是压缩后的 Response,此时,您需要手动解压。 + +也就是说,--compressed = -H'Content-Encoding: gzip' + 自动解压,所以推荐您使用 --compressed,以下仅作为单独设置请求头 + 手动解压的原理性示例。 + +当您想要验证返回值是否真的压缩时,您可以只添加请求头 -H'Content-Encoding: gzip',而不解压,可以看到返回信息是压缩后的数据(一般而言是看不懂的压缩码)。 + +```shell +curl --data-binary @data.txt.gz -H'Content-Encoding: gzip' -H'Accept-encoding: gzip' -XPOST http://127.0.0.1:9393/GeneralModelService/inference | gunzip +``` + + diff --git a/doc/Offical_Docs/5-5_RPC_API_CN.md b/doc/Offical_Docs/5-5_RPC_API_CN.md new file mode 100644 index 00000000..8eb99165 --- /dev/null +++ b/doc/Offical_Docs/5-5_RPC_API_CN.md @@ -0,0 +1,93 @@ +# RPC 方式访问 Server + +Paddle Serving 采用[brpc框架](https://github.com/apache/incubator-brpc)进行 Client/Server 端的通信。brpc 是百度开源的一款PRC网络框架,具有高并发、低延时等特点,已经支持了包括百度在内上百万在线预估实例、上千个在线预估服务,稳定可靠。与 gRPC 网络框架相比,具有更低的延时,更高的并发性能,且底层支持**brpc/grpc/http+json/http+proto**等多种协议。本文主要介绍如何使用 BRPC 进行通信。 + +## 示例 + +我们将以 examples/C++/fit_a_line 为例,讲解如何通过 RPC 访问 Server 端。 + +**一. 获取模型:** + +```shell +sh get_data.sh +``` + +**二. 开启服务端:** + +```shell +python3.6 -m paddle_serving_server.serve --model uci_housing_model --thread 10 --port 9393 +``` +服务端无须做任何改造,即可支持 RPC 方式。 + +## 客户端请求 + +**一. C++ 方法:** + +基础使用方法主要分为四步:1、创建一个 Client 对象。2、加载 Client 端的 prototxt 配置文件(本例中为 examples/C++/fit_a_line 目录下的 uci_housing_client/serving_client_conf.prototxt)。3、准备请求数据。4、调用 predict 函数,通过 brpc 方式请求预测服务。 +示例如下: + +``` + std::unique_ptr client; + client.reset(new ServingBrpcClient()); + + std::vector confs; + confs.push_back(conf); + if (client->init(confs, url) != 0) { + LOG(ERROR) << "Failed to init client!"; + return 0; + } + + PredictorInputs input; + PredictorOutputs output; + std::vector fetch_name; + std::vector float_feed = {0.0137f, -0.1136f, 0.2553f, -0.0692f, + 0.0582f, -0.0727f, -0.1583f, -0.0584f, + 0.6283f, 0.4919f, 0.1856f, 0.0795f, -0.0332f}; + std::vector float_shape = {1, 13}; + std::string feed_name = "x"; + fetch_name = {"price"}; + std::vector lod; + input.add_float_data(float_feed, feed_name, float_shape, lod); + + if (client->predict(input, output, fetch_name, 0) != 0) { + LOG(ERROR) << "Failed to predict!"; + } + else { + LOG(INFO) << output.print(); + } +``` + +具体使用详见[simple_client.cpp](./example/simple_client.cpp),已提供封装好的调用方法。 +```shell +./simple_client --client_conf="uci_housing_client/serving_client_conf.prototxt" --server_port="127.0.0.1:9393" --test_type="brpc" --sample_type="fit_a_line" +``` + +| Argument | Type | Default | Description | +| ---------------------------------------------- | ---- | ------------------------------------ | ----------------------------------------------------- | +| `client_conf` | str | `"serving_client_conf.prototxt"` | Path of client conf | +| `server_port` | str | `"127.0.0.1:9393"` | Exposed ip:port of server | +| `test_type` | str | `"brpc"` | Mode of request "brpc" | +| `sample_type` | str | `"fit_a_line"` | Type of sample include "fit_a_line,bert" | + +**二. Python 方法:** + +为了方便用户快速的使用 RPC 方式请求 Server 端预测服务,我们已经将常用的 RPC 请求的数据体封装、压缩、请求加密等功能封装为一个 Client 类提供给用户,方便用户使用。 + +使用 Client 最简单只需要四步:1、创建一个 Client 对象。2、加载 Client 端的 prototxt 配置文件(本例中为 examples/C++/fit_a_line 目录下的 uci_housing_client/serving_client_conf.prototxt)。3、调用 connect 函数。4、调用 Predict 函数,通过 RPC 方式请求预测服务。 + +``` +from paddle_serving_client import Client +import sys +import numpy as np + +client = Client() +client.load_client_config(sys.argv[1]) +client.connect(["127.0.0.1:9393"]) +fetch_list = client.get_fetch_names() + +new_data = np.zeros((1, 13)).astype("float32") +new_data[0] = [0.0137, -0.1136, 0.2553, -0.0692, 0.0582, -0.0727, -0.1583, -0.0584, 0.6283, 0.4919, 0.1856, 0.0795, -0.0332] +fetch_map = client.predict( + feed={"x": new_data}, fetch=fetch_list, batch=True) +print(fetch_map) +``` \ No newline at end of file diff --git a/doc/Offical_Docs/6-3_Request_Cache_CN.md b/doc/Offical_Docs/6-3_Request_Cache_CN.md new file mode 100644 index 00000000..70acfd51 --- /dev/null +++ b/doc/Offical_Docs/6-3_Request_Cache_CN.md @@ -0,0 +1,17 @@ +# 请求缓存 + +本文主要介绍请求缓存功能及实现原理。 + +## 基本原理 + +服务中请求由张量 tensor、结果名称 fetch_var_names、调试开关 profile_server、标识码 log_id 组成,预测结果包含输出张量 tensor 等。这里缓存会保存请求与结果的键值对。当请求命中缓存时,服务不会执行模型预测,而是会直接从缓存中提取结果。对于某些特定场景而言,这能显著降低请求耗时。 + +缓存可以通过设置`--request_cache_size`来开启。该标志默认为 0,即不开启缓存。当设置非零值时,服务会以设置大小为存储上限开启缓存。这里设置的内存单位为字节。注意,如果设置`--request_cache_size`为 0 是不能开启缓存的。 + +缓存中的键为 64 位整形数,是由请求中的 tensor 和 fetch_var_names 数据生成的 64 位哈希值。如果请求命中,那么对应的处理结果会提取出来用于构建响应数据。如果请求没有命中,服务则会执行模型预测,在返回结果的同时将处理结果放入缓存中。由于缓存设置了存储上限,因此需要淘汰机制来限制缓存容量。当前,服务采用了最近最少使用(LRU)机制用于淘汰缓存数据。 + +## 注意事项 + + - 只有预测成功的请求会进行缓存。如果请求失败或者在预测过程中返回错误,则处理结果不会缓存。 + - 缓存是基于请求数据的哈希值实现。因此,可能会出现两个不同的请求生成了相同的哈希值即哈希碰撞,这时服务可能会返回错误的响应数据。哈希值为 64 位数据,发生哈希碰撞的可能性较小。 + - 不论使用同步模式还是异步模式,均可以正常使用缓存功能。 diff --git a/doc/Offical_Docs/6-4_Encryption_CN.md b/doc/Offical_Docs/6-4_Encryption_CN.md new file mode 100644 index 00000000..0a0f8406 --- /dev/null +++ b/doc/Offical_Docs/6-4_Encryption_CN.md @@ -0,0 +1,42 @@ +# 加密模型预测 + +Padle Serving 提供了模型加密预测功能,本文档显示了详细信息。 + +## 原理 + +采用对称加密算法对模型进行加密。对称加密算法采用同一密钥进行加解密,它计算量小,速度快,是最常用的加密方法。 + +**一. 获得加密模型:** + +普通的模型和参数可以理解为一个字符串,通过对其使用加密算法(参数是您的密钥),普通模型和参数就变成了一个加密的模型和参数。 + +我们提供了一个简单的演示来加密模型。请参阅[examples/C++/encryption/encrypt.py](../../examples/C++/encryption/encrypt.py)。 + + +**二. 启动加密服务:** + +假设您已经有一个已经加密的模型(在`encrypt_server/`路径下),您可以通过添加一个额外的命令行参数 `--use_encryption_model`来启动加密模型服务。 + +CPU Service +``` +python -m paddle_serving_server.serve --model encrypt_server/ --port 9300 --use_encryption_model +``` +GPU Service +``` +python -m paddle_serving_server.serve --model encrypt_server/ --port 9300 --use_encryption_model --gpu_ids 0 +``` + +此时,服务器不会真正启动,而是等待密钥。 + +**三. Client Encryption Inference:** + +首先,您必须拥有模型加密过程中使用的密钥。 + +然后你可以用这个密钥配置你的客户端,当你连接服务器时,这个密钥会发送到服务器,服务器会保留它。 + +一旦服务器获得密钥,它就使用该密钥解析模型并启动模型预测服务。 + + +**四. 模型加密推理示例:** + +模型加密推理示例, 请参见[examples/C++/encryption/](../../examples/C++/encryption/)。 diff --git a/doc/Offical_Docs/6-6_OP_CN.md b/doc/Offical_Docs/6-6_OP_CN.md new file mode 100644 index 00000000..6fed3d15 --- /dev/null +++ b/doc/Offical_Docs/6-6_OP_CN.md @@ -0,0 +1,140 @@ +# 如何开发一个新的General Op? + +在本文档中,我们主要集中于如何为 Paddle Serving 开发新的服务器端运算符。在开始编写新运算符之前,让我们看一些示例代码以获得为服务器编写新运算符的基本思想。我们假设您已经知道 Paddle Serving 服务器端的基本计算逻辑。 下面的代码您可以在 Serving代码库下的 `core/general-server/op` 目录查阅。 + + +``` c++ + +#pragma once +#include +#include +#include "paddle_inference_api.h" // NOLINT +#include "core/general-server/general_model_service.pb.h" +#include "core/general-server/op/general_infer_helper.h" + +namespace baidu { +namespace paddle_serving { +namespace serving { + +class GeneralInferOp + : public baidu::paddle_serving::predictor::OpWithChannel { + public: + typedef std::vector TensorVector; + + DECLARE_OP(GeneralInferOp); + + int inference(); + +}; + +} // namespace serving +} // namespace paddle_serving +} // namespace baidu +``` + +## 定义一个Op + +上面的头文件声明了一个名为 `GeneralInferOp` 的 Paddle Serving 运算符。 在运行时,将调用函数 `int inference()`。 通常,我们将服务器端运算符定义为baidu::paddle_serving::predictor::OpWithChannel 的子类,并使用 `GeneralBlob` 数据结构。 + +## 在Op之间使用 `GeneralBlob` + +`GeneralBlob` 是一种可以在服务器端运算符之间使用的数据结构。 `tensor_vector` 是 `GeneralBlob` 中最重要的数据结构。 服务器端的操作员可以将多个 `paddle::PaddleTensor` 作为输入,并可以将多个 `paddle::PaddleTensor `作为输出。 特别是,`tensor_vector` 可以在没有内存拷贝的操作下输入到 Paddle 推理引擎中。 + +``` c++ +struct GeneralBlob { + std::vector tensor_vector; + int64_t time_stamp[20]; + int p_size = 0; + + int _batch_size; + + void Clear() { + size_t tensor_count = tensor_vector.size(); + for (size_t ti = 0; ti < tensor_count; ++ti) { + tensor_vector[ti].shape.clear(); + } + tensor_vector.clear(); + } + + int SetBatchSize(int batch_size) { _batch_size = batch_size; } + + int GetBatchSize() const { return _batch_size; } + std::string ShortDebugString() const { return "Not implemented!"; } +}; +``` + +**一. 实现 `int Inference()`** + +``` c++ +int GeneralInferOp::inference() { + VLOG(2) << "Going to run inference"; + const GeneralBlob *input_blob = get_depend_argument(pre_name()); + VLOG(2) << "Get precedent op name: " << pre_name(); + GeneralBlob *output_blob = mutable_data(); + + if (!input_blob) { + LOG(ERROR) << "Failed mutable depended argument, op:" << pre_name(); + return -1; + } + + const TensorVector *in = &input_blob->tensor_vector; + TensorVector *out = &output_blob->tensor_vector; + int batch_size = input_blob->GetBatchSize(); + VLOG(2) << "input batch size: " << batch_size; + + output_blob->SetBatchSize(batch_size); + + VLOG(2) << "infer batch size: " << batch_size; + + Timer timeline; + int64_t start = timeline.TimeStampUS(); + timeline.Start(); + + if (InferManager::instance().infer(engine_name().c_str(), in, out, batch_size)) { + LOG(ERROR) << "Failed do infer in fluid model: " << engine_name().c_str(); + return -1; + } + + int64_t end = timeline.TimeStampUS(); + CopyBlobInfo(input_blob, output_blob); + AddBlobInfo(output_blob, start); + AddBlobInfo(output_blob, end); + return 0; +} +DEFINE_OP(GeneralInferOp); +``` + +`input_blob` 和 `output_blob` 都有很多的 `paddle::PaddleTensor`, 且 Paddle 预测库会被 `InferManager::instance().infer(engine_name().c_str(), in, out, batch_size)` 调用。此函数中的其他大多数代码都与性能分析有关,将来我们也可能会删除多余的代码。 + + +基本上,以上代码可以实现一个新的运算符。如果您想访问字典资源,可以参考 `core/predictor/framework/resource.cpp` 来添加全局可见资源。资源的初始化在启动服务器的运行时执行。 + +## 定义 Python API + +在服务器端为 Paddle Serving 定义 C++ 运算符后,最后一步是在 Python API 中为 Paddle Serving 服务器 API 添加注册, `python/paddle_serving_server/dag.py` 文件里有关于 API 注册的代码如下 + + +``` python +self.op_list = [ + "GeneralInferOp", + "GeneralReaderOp", + "GeneralResponseOp", + "GeneralTextReaderOp", + "GeneralTextResponseOp", + "GeneralSingleKVOp", + "GeneralDistKVInferOp", + "GeneralDistKVOp", + "GeneralCopyOp", + "GeneralDetectionOp", + ] +``` + +在 `python/paddle_serving_server/server.py` 文件中仅添加`需要加载模型,执行推理预测的自定义的 C++ OP 类的类名`。例如 `GeneralReaderOp` 由于只是做一些简单的数据处理而不加载模型调用预测,故在上述的代码中需要添加,而不添加在下方的代码中。 +``` python +default_engine_types = [ + 'GeneralInferOp', + 'GeneralDistKVInferOp', + 'GeneralDistKVQuantInferOp', + 'GeneralDetectionOp', + ] +``` diff --git a/doc/Offical_Docs/6-7_Model_Ensemble_CN.md b/doc/Offical_Docs/6-7_Model_Ensemble_CN.md new file mode 100644 index 00000000..ccee0f01 --- /dev/null +++ b/doc/Offical_Docs/6-7_Model_Ensemble_CN.md @@ -0,0 +1,228 @@ +# 如何使用 C++ 定义模型组合 + +如果您的模型处理过程包含 2+ 的模型推理环节(例如 OCR 一般需要 det+rec 两个环节),此时有两种做法可以满足您的需求。 + +1. 启动两个 Serving 服务(例如 Serving-det, Serving-rec),在您的 Client 中,读入数据——>det 前处理——>调用 Serving-det 预测——>det 后处理——>rec 前处理——>调用 Serving-rec 预测——>rec 后处理——>输出结果。 + - 优点:无须改动 Paddle Serving 代码 + - 缺点:需要两次请求服务,请求数据量越大,效率稍差。 +2. 通过修改代码,自定义模型预测行为(自定义 OP),自定义服务处理的流程(自定义 DAG),将多个模型的组合处理过程(上述的 det 前处理——>调用 Serving-det 预测——>det 后处理——>rec 前处理——>调用 Serving-rec 预测——>rec 后处理)集成在一个 Serving 服务中。此时,在您的 Client 中,读入数据——>调用集成后的 Serving——>输出结果。 + - 优点:只需要一次请求服务,效率高。 + - 缺点:需要改动代码,且需要重新编译。 + +本文主要介绍第二种效率高的方法,该方法的基本步骤如下: +1. 自定义 OP(即定义单个模型的前处理-模型预测-模型后处理) +2. 编译 +3. 服务启动与调用 + +## 自定义 OP +一个 OP 定义了单个模型的前处理-模型预测-模型后处理,定义 OP 需要以下 2 步: +1. 定义 C++.h 头文件 +2. 定义 C++.cpp 源文件 + +**一. 定义 C++.h 头文件** +复制下方的代码,将其中`/*自定义 Class 名称*/`更换为自定义的类名即可,如 `GeneralDetectionOp` + +放置于 `core/general-server/op/` 路径下,文件名自定义即可,如 `general_detection_op.h` +``` C++ +#pragma once +#include +#include +#include "core/general-server/general_model_service.pb.h" +#include "core/general-server/op/general_infer_helper.h" +#include "paddle_inference_api.h" // NOLINT + +namespace baidu { +namespace paddle_serving { +namespace serving { + +class /*自定义Class名称*/ + : public baidu::paddle_serving::predictor::OpWithChannel { + public: + typedef std::vector TensorVector; + + DECLARE_OP(/*自定义Class名称*/); + + int inference(); +}; + +} // namespace serving +} // namespace paddle_serving +} // namespace baidu +``` +**二. 定义 C++.cpp 源文件** +复制下方的代码,将其中`/*自定义 Class 名称*/`更换为自定义的类名,如 `GeneralDetectionOp` + +将前处理和后处理的代码添加在下方的代码中注释的前处理和后处理的位置。 + +放置于 `core/general-server/op/` 路径下,文件名自定义即可,如 `general_detection_op.cpp` + +``` C++ +#include "core/general-server/op/自定义的头文件名" +#include +#include +#include +#include +#include "core/predictor/framework/infer.h" +#include "core/predictor/framework/memory.h" +#include "core/predictor/framework/resource.h" +#include "core/util/include/timer.h" + +namespace baidu { +namespace paddle_serving { +namespace serving { + +using baidu::paddle_serving::Timer; +using baidu::paddle_serving::predictor::MempoolWrapper; +using baidu::paddle_serving::predictor::general_model::Tensor; +using baidu::paddle_serving::predictor::general_model::Response; +using baidu::paddle_serving::predictor::general_model::Request; +using baidu::paddle_serving::predictor::InferManager; +using baidu::paddle_serving::predictor::PaddleGeneralModelConfig; + +int /*自定义Class名称*/::inference() { + //获取前置OP节点 + const std::vector pre_node_names = pre_names(); + if (pre_node_names.size() != 1) { + LOG(ERROR) << "This op(" << op_name() + << ") can only have one predecessor op, but received " + << pre_node_names.size(); + return -1; + } + const std::string pre_name = pre_node_names[0]; + + //将前置OP的输出,作为本OP的输入。 + GeneralBlob *input_blob = mutable_depend_argument(pre_name); + if (!input_blob) { + LOG(ERROR) << "input_blob is nullptr,error"; + return -1; + } + TensorVector *in = &input_blob->tensor_vector; + uint64_t log_id = input_blob->GetLogId(); + int batch_size = input_blob->_batch_size; + + //初始化本OP的输出。 + GeneralBlob *output_blob = mutable_data(); + output_blob->SetLogId(log_id); + output_blob->_batch_size = batch_size; + VLOG(2) << "(logid=" << log_id << ") infer batch size: " << batch_size; + TensorVector *out = &output_blob->tensor_vector; + + //前处理的代码添加在此处,前处理直接修改上文的TensorVector* in + //注意in里面的数据是前置节点的输出经过后处理后的out中的数据 + + Timer timeline; + int64_t start = timeline.TimeStampUS(); + timeline.Start(); + // 将前处理后的in,初始化的out传入,进行模型预测,模型预测的输出会直接修改out指向的内存中的数据 + // 如果您想定义一个不需要模型调用,只进行数据处理的OP,删除下面这一部分的代码即可。 + if (InferManager::instance().infer( + engine_name().c_str(), in, out, batch_size)) { + LOG(ERROR) << "(logid=" << log_id + << ") Failed do infer in fluid model: " << engine_name().c_str(); + return -1; + } + + //后处理的代码添加在此处,后处理直接修改上文的TensorVector* out + //后处理后的out会被传递给后续的节点 + + int64_t end = timeline.TimeStampUS(); + CopyBlobInfo(input_blob, output_blob); + AddBlobInfo(output_blob, start); + AddBlobInfo(output_blob, end); + return 0; +} +DEFINE_OP(/*自定义Class名称*/); + +} // namespace serving +} // namespace paddle_serving +} // namespace baidu +``` + +1. TensorVector数据结构 + +TensorVector* in 和 out 都是一个 TensorVector 类型的指针,其使用方法跟 Paddle C++ API 中的 Tensor 几乎一样,相关的数据结构如下所示 + +``` C++ +//TensorVector +typedef std::vector TensorVector; + +//paddle::PaddleTensor +struct PD_INFER_DECL PaddleTensor { + PaddleTensor() = default; + std::string name; ///< variable name. + std::vector shape; + PaddleBuf data; ///< blob of data. + PaddleDType dtype; + std::vector> lod; ///< Tensor+LoD equals LoDTensor +}; + +//PaddleBuf +class PD_INFER_DECL PaddleBuf { + public: + + explicit PaddleBuf(size_t length) + : data_(new char[length]), length_(length), memory_owned_(true) {} + + PaddleBuf(void* data, size_t length) + : data_(data), length_(length), memory_owned_{false} {} + + explicit PaddleBuf(const PaddleBuf& other); + + void Resize(size_t length); + void Reset(void* data, size_t length); + bool empty() const { return length_ == 0; } + void* data() const { return data_; } + size_t length() const { return length_; } + ~PaddleBuf() { Free(); } + PaddleBuf& operator=(const PaddleBuf&); + PaddleBuf& operator=(PaddleBuf&&); + PaddleBuf() = default; + PaddleBuf(PaddleBuf&& other); + private: + void Free(); + void* data_{nullptr}; ///< pointer to the data memory. + size_t length_{0}; ///< number of memory bytes. + bool memory_owned_{true}; +}; +``` + +2. TensorVector 代码示例 + +```C++ +/*例如,你想访问输入数据中的第1个Tensor*/ +paddle::PaddleTensor& tensor_1 = in->at(0); +/*例如,你想修改输入数据中的第1个Tensor的名称*/ +tensor_1.name = "new name"; +/*例如,你想获取输入数据中的第1个Tensor的shape信息*/ +std::vector tensor_1_shape = tensor_1.shape; +/*例如,你想修改输入数据中的第1个Tensor中的数据*/ +void* data_1 = tensor_1.data.data(); +//后续直接修改data_1指向的内存即可 +//比如,当您的数据是int类型,将void*转换为int*进行处理即可 +``` + + +## 编译 +此时,需要您重新编译生成 serving,并通过 `export SERVING_BIN` 设置环境变量来指定使用您编译生成的 serving 二进制文件,并通过 `pip3 install` 的方式安装相关 python 包,细节请参考[如何编译Serving](2-3_Compile_CN.md) + +## 服务启动与调用 + +**一. Server 端启动** + +在前面两个小节工作做好的基础上,一个服务启动两个模型串联,只需要在`--model 后依次按顺序传入模型文件夹的相对路径`,且需要在`--op 后依次传入自定义 C++OP 类名称`,其中--model 后面的模型与--op 后面的类名称的顺序需要对应,`这里假设我们已经定义好了两个 OP 分别为 GeneralDetectionOp 和 GeneralRecOp`,则脚本代码如下: +```python +#一个服务启动多模型串联 +python3 -m paddle_serving_server.serve --model ocr_det_model ocr_rec_model --op GeneralDetectionOp GeneralRecOp --port 9292 +#多模型串联 ocr_det_model 对应 GeneralDetectionOp ocr_rec_model 对应 GeneralRecOp +``` + +**二. Client 端调用** + +此时,Client 端的调用,也需要传入两个 Client 端的 proto 文件或文件夹的路径,以 OCR 为例,可以参考[ocr_cpp_client.py](../../examples/C++/PaddleOCR/ocr/ocr_cpp_client.py)来自行编写您的脚本,此时 Client 调用如下: +```python +#一个服务启动多模型串联 +python3 自定义.py ocr_det_client ocr_rec_client +#ocr_det_client为第一个模型的Client端proto文件夹的相对路径 +#ocr_rec_client为第二个模型的Client端proto文件夹的相对路径 +``` +此时,对于 Server 端而言,输入的数据的格式与`第一个模型的 Client 端 proto 格式`定义的一致,输出的数据格式与`最后一个模型的 Client 端 proto`文件一致。一般情况下您无须关注此事,当您需要了解详细的proto的定义,请参考[Serving 配置](5-3_Serving_Configure_CN.md)。 -- GitLab