From d517e1b74b660f5f1627c51ec40fbf4cc5126616 Mon Sep 17 00:00:00 2001 From: tink2123 Date: Wed, 12 May 2021 06:42:15 +0000 Subject: [PATCH 1/6] update serving --- deploy/pdserving/README_CN.md | 23 +++---- deploy/pdserving/config.yml | 28 ++++---- deploy/pdserving/ocr_reader.py | 5 +- deploy/pdserving/pipeline_http_client.py | 6 +- deploy/pdserving/pipeline_rpc_client.py | 3 +- deploy/pdserving/web_service.py | 83 +++++++++++++++++------- 6 files changed, 87 insertions(+), 61 deletions(-) diff --git a/deploy/pdserving/README_CN.md b/deploy/pdserving/README_CN.md index 3e3f1bde0e..38715a4a7f 100644 --- a/deploy/pdserving/README_CN.md +++ b/deploy/pdserving/README_CN.md @@ -29,7 +29,9 @@ PaddleOCR提供2种服务部署方式: 需要准备PaddleOCR的运行环境和Paddle Serving的运行环境。 -- 准备PaddleOCR的运行环境参考[链接](../../doc/doc_ch/installation.md) +- 准备PaddleOCR的运行环境[链接](../../doc/doc_ch/installation.md) + 根据环境下载对应的paddle whl包, + 推荐2.0.1版本:https://www.paddlepaddle.org.cn/whl/mkl/stable.html - 准备PaddleServing的运行环境,步骤如下 @@ -45,25 +47,16 @@ PaddleOCR提供2种服务部署方式: ``` 2. 安装client,用于向服务发送请求 - ``` - pip3 install paddle-serving-client==0.5.0 # for CPU + 在[下载链接](https://github.com/PaddlePaddle/Serving/blob/develop/doc/LATEST_PACKAGES.md)中找到对应python版本的client安装包,这里推荐python3.7版本: - pip3 install paddle-serving-client-gpu==0.5.0 # for GPU + ``` + wget https://paddle-serving.bj.bcebos.com/whl/paddle_serving_client-0.0.0-cp37-none-any.whl + pip3 install paddle_serving_client-0.0.0-cp37-none-any.whl ``` 3. 安装serving-app ``` - pip3 install paddle-serving-app==0.3.0 - ``` - **note:** 安装0.3.0版本的serving-app后,为了能加载动态图模型,需要修改serving_app的源码,具体为: - ``` - # 找到paddle_serving_app的安装目录,找到并编辑local_predict.py文件 - vim /usr/local/lib/python3.7/site-packages/paddle_serving_app/local_predict.py - # 将local_predict.py 的第85行 config = AnalysisConfig(model_path) 替换为: - if os.path.exists(os.path.join(model_path, "__params__")): - config = AnalysisConfig(os.path.join(model_path, "__model__"), os.path.join(model_path, "__params__")) - else: - config = AnalysisConfig(model_path) + pip3 install paddle-serving-app==0.3.1 ``` **Note:** 如果要安装最新版本的PaddleServing参考[链接](https://github.com/PaddlePaddle/Serving/blob/develop/doc/LATEST_PACKAGES.md)。 diff --git a/deploy/pdserving/config.yml b/deploy/pdserving/config.yml index aef735dbfa..8ab27dbc20 100644 --- a/deploy/pdserving/config.yml +++ b/deploy/pdserving/config.yml @@ -1,32 +1,32 @@ #rpc端口, rpc_port和http_port不允许同时为空。当rpc_port为空且http_port不为空时,会自动将rpc_port设置为http_port+1 -rpc_port: 18090 +rpc_port: 18091 #http端口, rpc_port和http_port不允许同时为空。当rpc_port可用且http_port为空时,不自动生成http_port -http_port: 9999 +http_port: 9998 #worker_num, 最大并发数。当build_dag_each_worker=True时, 框架会创建worker_num个进程,每个进程内构建grpcSever和DAG ##当build_dag_each_worker=False时,框架会设置主线程grpc线程池的max_workers=worker_num -worker_num: 20 +worker_num: 5 #build_dag_each_worker, False,框架在进程内创建一条DAG;True,框架会每个进程内创建多个独立的DAG -build_dag_each_worker: false +build_dag_each_worker: False dag: #op资源类型, True, 为线程模型;False,为进程模型 is_thread_op: False #重试次数 - retry: 1 + retry: 10 #使用性能分析, True,生成Timeline性能数据,对性能有一定影响;False为不使用 - use_profile: False + use_profile: True tracer: interval_s: 10 op: det: #并发数,is_thread_op=True时,为线程并发;否则为进程并发 - concurrency: 4 + concurrency: 2 #当op配置没有server_endpoints时,从local_service_conf读取本地服务配置 local_service_conf: @@ -34,15 +34,15 @@ op: client_type: local_predictor #det模型路径 - model_config: /paddle/serving/models/det_serving_server/ #ocr_det_model + model_config: ./ppocr_det_server_2.0_serving #Fetch结果列表,以client_config中fetch_var的alias_name为准 fetch_list: ["save_infer_model/scale_0.tmp_1"] #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡 - devices: "2" + devices: "0" - ir_optim: True + ir_optim: False rec: #并发数,is_thread_op=True时,为线程并发;否则为进程并发 concurrency: 1 @@ -60,12 +60,12 @@ op: client_type: local_predictor #rec模型路径 - model_config: /paddle/serving/models/rec_serving_server/ #ocr_rec_model + model_config: ./ppocr_rec_server_2.0_serving #Fetch结果列表,以client_config中fetch_var的alias_name为准 - fetch_list: ["save_infer_model/scale_0.tmp_1"] #["ctc_greedy_decoder_0.tmp_0", "softmax_0.tmp_0"] + fetch_list: ["save_infer_model/scale_0.tmp_1"] #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡 - devices: "2" + devices: "0" - ir_optim: True + ir_optim: False diff --git a/deploy/pdserving/ocr_reader.py b/deploy/pdserving/ocr_reader.py index 95110706af..71682e6728 100644 --- a/deploy/pdserving/ocr_reader.py +++ b/deploy/pdserving/ocr_reader.py @@ -21,7 +21,6 @@ import sys import argparse import string from copy import deepcopy -import paddle class DetResizeForTest(object): @@ -227,8 +226,8 @@ class CTCLabelDecode(BaseRecLabelDecode): super(CTCLabelDecode, self).__init__(config) def __call__(self, preds, label=None, *args, **kwargs): - if isinstance(preds, paddle.Tensor): - preds = preds.numpy() + #if isinstance(preds, paddle.Tensor): + # preds = preds.numpy() preds_idx = preds.argmax(axis=2) preds_prob = preds.max(axis=2) text = self.decode(preds_idx, preds_prob, is_remove_duplicate=True) diff --git a/deploy/pdserving/pipeline_http_client.py b/deploy/pdserving/pipeline_http_client.py index 88c4a81ea8..0befe2f614 100644 --- a/deploy/pdserving/pipeline_http_client.py +++ b/deploy/pdserving/pipeline_http_client.py @@ -23,8 +23,8 @@ def cv2_to_base64(image): return base64.b64encode(image).decode('utf8') -url = "http://127.0.0.1:9999/ocr/prediction" -test_img_dir = "../doc/imgs/" +url = "http://127.0.0.1:9998/ocr/prediction" +test_img_dir = "../../doc/imgs/" for idx, img_file in enumerate(os.listdir(test_img_dir)): with open(os.path.join(test_img_dir, img_file), 'rb') as file: image_data1 = file.read() @@ -36,5 +36,5 @@ for idx, img_file in enumerate(os.listdir(test_img_dir)): r = requests.post(url=url, data=json.dumps(data)) print(r.json()) -test_img_dir = "../doc/imgs/" +test_img_dir = "../../doc/imgs/" print("==> total number of test imgs: ", len(os.listdir(test_img_dir))) diff --git a/deploy/pdserving/pipeline_rpc_client.py b/deploy/pdserving/pipeline_rpc_client.py index 7471f7ed6c..79f898faf3 100644 --- a/deploy/pdserving/pipeline_rpc_client.py +++ b/deploy/pdserving/pipeline_rpc_client.py @@ -23,7 +23,7 @@ import base64 import os client = PipelineClient() -client.connect(['127.0.0.1:18090']) +client.connect(['127.0.0.1:18091']) def cv2_to_base64(image): @@ -39,4 +39,3 @@ for img_file in os.listdir(test_img_dir): for i in range(1): ret = client.predict(feed_dict={"image": image}, fetch=["res"]) print(ret) - #print(ret) diff --git a/deploy/pdserving/web_service.py b/deploy/pdserving/web_service.py index b47ef65d09..43de8355f0 100644 --- a/deploy/pdserving/web_service.py +++ b/deploy/pdserving/web_service.py @@ -56,11 +56,9 @@ class DetOp(Op): det_img = self.det_preprocess(self.im) _, self.new_h, self.new_w = det_img.shape - print("det image shape", det_img.shape) return {"x": det_img[np.newaxis, :].copy()}, False, None, "" def postprocess(self, input_dicts, fetch_dict, log_id): - print("input_dicts: ", input_dicts) det_out = fetch_dict["save_infer_model/scale_0.tmp_1"] ratio_list = [ float(self.new_h) / self.ori_h, float(self.new_w) / self.ori_w @@ -69,7 +67,6 @@ class DetOp(Op): dt_boxes = self.filter_func(dt_boxes_list[0], [self.ori_h, self.ori_w]) out_dict = {"dt_boxes": dt_boxes, "image": self.im} - print("out dict", out_dict["dt_boxes"]) return out_dict, None, "" @@ -89,29 +86,67 @@ class RecOp(Op): feed_list = [] img_list = [] max_wh_ratio = 0 - for i, dtbox in enumerate(dt_boxes): - boximg = self.get_rotate_crop_image(im, dt_boxes[i]) - img_list.append(boximg) - h, w = boximg.shape[0:2] - wh_ratio = w * 1.0 / h - max_wh_ratio = max(max_wh_ratio, wh_ratio) - _, w, h = self.ocr_reader.resize_norm_img(img_list[0], - max_wh_ratio).shape + ## Many mini-batchs, the type of feed_data is list. + max_batch_size = 6 # len(dt_boxes) - imgs = np.zeros((len(img_list), 3, w, h)).astype('float32') - for id, img in enumerate(img_list): - norm_img = self.ocr_reader.resize_norm_img(img, max_wh_ratio) - imgs[id] = norm_img - print("rec image shape", imgs.shape) - feed = {"x": imgs.copy()} - return feed, False, None, "" + # If max_batch_size is 0, skipping predict stage + if max_batch_size == 0: + return {}, True, None, "" + boxes_size = len(dt_boxes) + batch_size = boxes_size // max_batch_size + rem = boxes_size % max_batch_size + #_LOGGER.info("max_batch_len:{}, batch_size:{}, rem:{}, boxes_size:{}".format(max_batch_size, batch_size, rem, boxes_size)) + for bt_idx in range(0, batch_size + 1): + imgs = None + boxes_num_in_one_batch = 0 + if bt_idx == batch_size: + if rem == 0: + continue + else: + boxes_num_in_one_batch = rem + elif bt_idx < batch_size: + boxes_num_in_one_batch = max_batch_size + else: + _LOGGER.error("batch_size error, bt_idx={}, batch_size={}". + format(bt_idx, batch_size)) + break - def postprocess(self, input_dicts, fetch_dict, log_id): - rec_res = self.ocr_reader.postprocess(fetch_dict, with_score=True) - res_lst = [] - for res in rec_res: - res_lst.append(res[0]) - res = {"res": str(res_lst)} + start = bt_idx * max_batch_size + end = start + boxes_num_in_one_batch + img_list = [] + for box_idx in range(start, end): + boximg = self.get_rotate_crop_image(im, dt_boxes[box_idx]) + img_list.append(boximg) + h, w = boximg.shape[0:2] + wh_ratio = w * 1.0 / h + max_wh_ratio = max(max_wh_ratio, wh_ratio) + _, w, h = self.ocr_reader.resize_norm_img(img_list[0], + max_wh_ratio).shape + + imgs = np.zeros((boxes_num_in_one_batch, 3, w, h)).astype('float32') + for id, img in enumerate(img_list): + norm_img = self.ocr_reader.resize_norm_img(img, max_wh_ratio) + imgs[id] = norm_img + feed = {"x": imgs.copy()} + feed_list.append(feed) + + return feed_list, False, None, "" + def postprocess(self, input_dicts, fetch_data, log_id): + res_list = [] + if isinstance(fetch_data, dict): + if len(fetch_data) > 0: + rec_batch_res = self.ocr_reader.postprocess( + fetch_data, with_score=True) + for res in rec_batch_res: + res_list.append(res[0]) + elif isinstance(fetch_data, list): + for one_batch in fetch_data: + one_batch_res = self.ocr_reader.postprocess( + one_batch, with_score=True) + for res in one_batch_res: + res_list.append(res[0]) + + res = {"res": str(res_list)} return res, None, "" From 24cca54f19201abc4338f1eb5496456982036fd3 Mon Sep 17 00:00:00 2001 From: tink2123 Date: Wed, 12 May 2021 14:56:12 +0800 Subject: [PATCH 2/6] update readme --- deploy/pdserving/README.md | 20 ++++++++------------ deploy/pdserving/README_CN.md | 3 +-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/deploy/pdserving/README.md b/deploy/pdserving/README.md index 88426ba9c5..3a38053ed2 100644 --- a/deploy/pdserving/README.md +++ b/deploy/pdserving/README.md @@ -30,6 +30,8 @@ The introduction and tutorial of Paddle Serving service deployment framework ref PaddleOCR operating environment and Paddle Serving operating environment are needed. 1. Please prepare PaddleOCR operating environment reference [link](../../doc/doc_ch/installation.md). + Download the corresponding paddle whl package according to the environment, it is recommended to install version 2.0.1. + 2. The steps of PaddleServing operating environment prepare are as follows: @@ -45,23 +47,17 @@ PaddleOCR operating environment and Paddle Serving operating environment are nee ``` 3. Install the client to send requests to the service - ``` - pip3 install paddle-serving-client==0.5.0 # for CPU + In [download link](https://github.com/PaddlePaddle/Serving/blob/develop/doc/LATEST_PACKAGES.md) find the client installation package corresponding to the python version. + The python3.7 version is recommended here: - pip3 install paddle-serving-client-gpu==0.5.0 # for GPU + ``` + wget https://paddle-serving.bj.bcebos.com/whl/paddle_serving_client-0.0.0-cp37-none-any.whl + pip3 install paddle_serving_client-0.0.0-cp37-none-any.whl ``` 4. Install serving-app ``` - pip3 install paddle-serving-app==0.3.0 - # fix local_predict to support load dynamic model - # find the install directoory of paddle_serving_app - vim /usr/local/lib/python3.7/site-packages/paddle_serving_app/local_predict.py - # replace line 85 of local_predict.py config = AnalysisConfig(model_path) with: - if os.path.exists(os.path.join(model_path, "__params__")): - config = AnalysisConfig(os.path.join(model_path, "__model__"), os.path.join(model_path, "__params__")) - else: - config = AnalysisConfig(model_path) + pip3 install paddle-serving-app==0.3.1 ``` **note:** If you want to install the latest version of PaddleServing, refer to [link](https://github.com/PaddlePaddle/Serving/blob/develop/doc/LATEST_PACKAGES.md). diff --git a/deploy/pdserving/README_CN.md b/deploy/pdserving/README_CN.md index 38715a4a7f..071d2947cb 100644 --- a/deploy/pdserving/README_CN.md +++ b/deploy/pdserving/README_CN.md @@ -30,8 +30,7 @@ PaddleOCR提供2种服务部署方式: 需要准备PaddleOCR的运行环境和Paddle Serving的运行环境。 - 准备PaddleOCR的运行环境[链接](../../doc/doc_ch/installation.md) - 根据环境下载对应的paddle whl包, - 推荐2.0.1版本:https://www.paddlepaddle.org.cn/whl/mkl/stable.html + 根据环境下载对应的paddle whl包,推荐安装2.0.1版本 - 准备PaddleServing的运行环境,步骤如下 From 761805a12d5a2c4ba2f039fe52c6fa33f41a9bcd Mon Sep 17 00:00:00 2001 From: tink2123 Date: Wed, 12 May 2021 20:19:40 +0800 Subject: [PATCH 3/6] add qps --- deploy/pdserving/README.md | 76 +++++++++++++++++++++++++++------ deploy/pdserving/README_CN.md | 72 ++++++++++++++++++++++++++----- deploy/pdserving/config.yml | 8 ++-- deploy/pdserving/ocr_reader.py | 10 ++--- deploy/pdserving/web_service.py | 13 +++--- 5 files changed, 140 insertions(+), 39 deletions(-) diff --git a/deploy/pdserving/README.md b/deploy/pdserving/README.md index 3a38053ed2..b5d8953d73 100644 --- a/deploy/pdserving/README.md +++ b/deploy/pdserving/README.md @@ -70,38 +70,38 @@ When using PaddleServing for service deployment, you need to convert the saved i Firstly, download the [inference model](https://github.com/PaddlePaddle/PaddleOCR#pp-ocr-20-series-model-listupdate-on-dec-15) of PPOCR ``` # Download and unzip the OCR text detection model -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar && tar xf ch_ppocr_server_v2.0_det_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_infer.tar # Download and unzip the OCR text recognition model -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar && tar xf ch_ppocr_server_v2.0_rec_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_infer.tar ``` -Then, you can use installed paddle_serving_client tool to convert inference model to server model. +Then, you can use installed paddle_serving_client tool to convert inference model to mobile model. ``` # Detection model conversion -python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_server_v2.0_det_infer/ \ +python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_det_infer/ \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ - --serving_server ./ppocr_det_server_2.0_serving/ \ - --serving_client ./ppocr_det_server_2.0_client/ + --serving_server ./ppocr_det_mobile_2.0_serving/ \ + --serving_client ./ppocr_det_mobile_2.0_client/ # Recognition model conversion -python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_server_v2.0_rec_infer/ \ +python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_rec_infer/ \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ - --serving_server ./ppocr_rec_server_2.0_serving/ \ - --serving_client ./ppocr_rec_server_2.0_client/ + --serving_server ./ppocr_rec_mobile_2.0_serving/ \ + --serving_client ./ppocr_rec_mobile_2.0_client/ ``` -After the detection model is converted, there will be additional folders of `ppocr_det_server_2.0_serving` and `ppocr_det_server_2.0_client` in the current folder, with the following format: +After the detection model is converted, there will be additional folders of `ppocr_det_mobile_2.0_serving` and `ppocr_det_mobile_2.0_client` in the current folder, with the following format: ``` -|- ppocr_det_server_2.0_serving/ +|- ppocr_det_mobile_2.0_serving/ |- __model__ |- __params__ |- serving_server_conf.prototxt |- serving_server_conf.stream.prototxt -|- ppocr_det_server_2.0_client +|- ppocr_det_mobile_2.0_client |- serving_client_conf.prototxt |- serving_client_conf.stream.prototxt @@ -143,6 +143,58 @@ The recognition model is the same. After successfully running, the predicted result of the model will be printed in the cmd window. An example of the result is: ![](./imgs/results.png) + Adjust the number of concurrency in config.yml to get the largest QPS. Generally, the number of concurrent detection and recognition is 2:1 + + ``` + det: + concurrency: 8 + ... + rec: + concurrency: 4 + ... + ``` + + Multiple service requests can be sent at the same time if necessary. + + The predicted performance data will be automatically written into the `PipelineServingLogs/pipeline.tracer` file: + + ``` + 2021-05-12 10:03:24,812 ==================== TRACER ====================== + 2021-05-12 10:03:24,904 Op(rec): + 2021-05-12 10:03:24,904 in[51.5634921875 ms] + 2021-05-12 10:03:24,904 prep[215.310859375 ms] + 2021-05-12 10:03:24,904 midp[33.1617109375 ms] + 2021-05-12 10:03:24,905 postp[10.451234375 ms] + 2021-05-12 10:03:24,905 out[9.736765625 ms] + 2021-05-12 10:03:24,905 idle[0.1914292677880819] + 2021-05-12 10:03:24,905 Op(det): + 2021-05-12 10:03:24,905 in[218.63487096774193 ms] + 2021-05-12 10:03:24,906 prep[357.35925 ms] + 2021-05-12 10:03:24,906 midp[31.47598387096774 ms] + 2021-05-12 10:03:24,906 postp[15.274870967741936 ms] + 2021-05-12 10:03:24,906 out[16.245693548387095 ms] + 2021-05-12 10:03:24,906 idle[0.3675805857279226] + 2021-05-12 10:03:24,906 DAGExecutor: + 2021-05-12 10:03:24,906 Query count[128] + 2021-05-12 10:03:24,906 QPS[12.8 q/s] + 2021-05-12 10:03:24,906 Succ[1.0] + 2021-05-12 10:03:24,907 Error req[] + 2021-05-12 10:03:24,907 Latency: + 2021-05-12 10:03:24,907 ave[798.6557734374998 ms] + 2021-05-12 10:03:24,907 .50[867.936 ms] + 2021-05-12 10:03:24,907 .60[914.507 ms] + 2021-05-12 10:03:24,907 .70[961.064 ms] + 2021-05-12 10:03:24,907 .80[1043.264 ms] + 2021-05-12 10:03:24,907 .90[1117.923 ms] + 2021-05-12 10:03:24,907 .95[1207.056 ms] + 2021-05-12 10:03:24,908 .99[1325.008 ms] + 2021-05-12 10:03:24,908 Channel (server worker num[10]): + 2021-05-12 10:03:24,909 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] + 2021-05-12 10:03:24,909 chl1(In: ['det'], Out: ['rec']) size[1/0] + 2021-05-12 10:03:24,910 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] + ``` + + ## FAQ **Q1**: No result return after sending the request. diff --git a/deploy/pdserving/README_CN.md b/deploy/pdserving/README_CN.md index 071d2947cb..37d018d0ed 100644 --- a/deploy/pdserving/README_CN.md +++ b/deploy/pdserving/README_CN.md @@ -68,38 +68,38 @@ PaddleOCR提供2种服务部署方式: 首先,下载PPOCR的[inference模型](https://github.com/PaddlePaddle/PaddleOCR#pp-ocr-20-series-model-listupdate-on-dec-15) ``` # 下载并解压 OCR 文本检测模型 -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar && tar xf ch_ppocr_server_v2.0_det_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_infer.tar # 下载并解压 OCR 文本识别模型 -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar && tar xf ch_ppocr_server_v2.0_rec_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_infer.tar ``` 接下来,用安装的paddle_serving_client把下载的inference模型转换成易于server部署的模型格式。 ``` # 转换检测模型 -python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_server_v2.0_det_infer/ \ +python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_det_infer/ \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ - --serving_server ./ppocr_det_server_2.0_serving/ \ - --serving_client ./ppocr_det_server_2.0_client/ + --serving_server ./ppocr_det_mobile_2.0_serving/ \ + --serving_client ./ppocr_det_mobile_2.0_client/ # 转换识别模型 -python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_server_v2.0_rec_infer/ \ +python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_rec_infer/ \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ - --serving_server ./ppocr_rec_server_2.0_serving/ \ - --serving_client ./ppocr_rec_server_2.0_client/ + --serving_server ./ppocr_rec_mobile_2.0_serving/ \ + --serving_client ./ppocr_rec_mobile_2.0_client/ ``` -检测模型转换完成后,会在当前文件夹多出`ppocr_det_server_2.0_serving` 和`ppocr_det_server_2.0_client`的文件夹,具备如下格式: +检测模型转换完成后,会在当前文件夹多出`ppocr_det_mobile_2.0_serving` 和`ppocr_det_mobile_2.0_client`的文件夹,具备如下格式: ``` -|- ppocr_det_server_2.0_serving/ +|- ppocr_det_mobile_2.0_serving/ |- __model__ |- __params__ |- serving_server_conf.prototxt |- serving_server_conf.stream.prototxt -|- ppocr_det_server_2.0_client +|- ppocr_det_mobile_2.0_client |- serving_client_conf.prototxt |- serving_client_conf.stream.prototxt @@ -140,6 +140,56 @@ python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_server_v2.0_rec_in 成功运行后,模型预测的结果会打印在cmd窗口中,结果示例为: ![](./imgs/results.png) + 调整 config.yml 中的并发个数获得最大的QPS, 一般检测和识别的并发数为2:1 + ``` + det: + #并发数,is_thread_op=True时,为线程并发;否则为进程并发 + concurrency: 8 + ... + rec: + #并发数,is_thread_op=True时,为线程并发;否则为进程并发 + concurrency: 4 + ... + ``` + 有需要的话可以同时发送多个服务请求 + + 预测性能数据会被自动写入 `PipelineServingLogs/pipeline.tracer` 文件中: + + ``` + 2021-05-12 10:03:24,812 ==================== TRACER ====================== + 2021-05-12 10:03:24,904 Op(rec): + 2021-05-12 10:03:24,904 in[51.5634921875 ms] + 2021-05-12 10:03:24,904 prep[215.310859375 ms] + 2021-05-12 10:03:24,904 midp[33.1617109375 ms] + 2021-05-12 10:03:24,905 postp[10.451234375 ms] + 2021-05-12 10:03:24,905 out[9.736765625 ms] + 2021-05-12 10:03:24,905 idle[0.1914292677880819] + 2021-05-12 10:03:24,905 Op(det): + 2021-05-12 10:03:24,905 in[218.63487096774193 ms] + 2021-05-12 10:03:24,906 prep[357.35925 ms] + 2021-05-12 10:03:24,906 midp[31.47598387096774 ms] + 2021-05-12 10:03:24,906 postp[15.274870967741936 ms] + 2021-05-12 10:03:24,906 out[16.245693548387095 ms] + 2021-05-12 10:03:24,906 idle[0.3675805857279226] + 2021-05-12 10:03:24,906 DAGExecutor: + 2021-05-12 10:03:24,906 Query count[128] + 2021-05-12 10:03:24,906 QPS[12.8 q/s] + 2021-05-12 10:03:24,906 Succ[1.0] + 2021-05-12 10:03:24,907 Error req[] + 2021-05-12 10:03:24,907 Latency: + 2021-05-12 10:03:24,907 ave[798.6557734374998 ms] + 2021-05-12 10:03:24,907 .50[867.936 ms] + 2021-05-12 10:03:24,907 .60[914.507 ms] + 2021-05-12 10:03:24,907 .70[961.064 ms] + 2021-05-12 10:03:24,907 .80[1043.264 ms] + 2021-05-12 10:03:24,907 .90[1117.923 ms] + 2021-05-12 10:03:24,907 .95[1207.056 ms] + 2021-05-12 10:03:24,908 .99[1325.008 ms] + 2021-05-12 10:03:24,908 Channel (server worker num[10]): + 2021-05-12 10:03:24,909 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] + 2021-05-12 10:03:24,909 chl1(In: ['det'], Out: ['rec']) size[1/0] + 2021-05-12 10:03:24,910 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] + ``` ## FAQ diff --git a/deploy/pdserving/config.yml b/deploy/pdserving/config.yml index 8ab27dbc20..6cac1b9fea 100644 --- a/deploy/pdserving/config.yml +++ b/deploy/pdserving/config.yml @@ -26,7 +26,7 @@ dag: op: det: #并发数,is_thread_op=True时,为线程并发;否则为进程并发 - concurrency: 2 + concurrency: 8 #当op配置没有server_endpoints时,从local_service_conf读取本地服务配置 local_service_conf: @@ -34,7 +34,7 @@ op: client_type: local_predictor #det模型路径 - model_config: ./ppocr_det_server_2.0_serving + model_config: ./ppocr_det_mobile_2.0_serving #Fetch结果列表,以client_config中fetch_var的alias_name为准 fetch_list: ["save_infer_model/scale_0.tmp_1"] @@ -45,7 +45,7 @@ op: ir_optim: False rec: #并发数,is_thread_op=True时,为线程并发;否则为进程并发 - concurrency: 1 + concurrency: 4 #超时时间, 单位ms timeout: -1 @@ -60,7 +60,7 @@ op: client_type: local_predictor #rec模型路径 - model_config: ./ppocr_rec_server_2.0_serving + model_config: ./ppocr_rec_mobile_2.0_serving #Fetch结果列表,以client_config中fetch_var的alias_name为准 fetch_list: ["save_infer_model/scale_0.tmp_1"] diff --git a/deploy/pdserving/ocr_reader.py b/deploy/pdserving/ocr_reader.py index 71682e6728..3f219784fc 100644 --- a/deploy/pdserving/ocr_reader.py +++ b/deploy/pdserving/ocr_reader.py @@ -33,12 +33,12 @@ class DetResizeForTest(object): elif 'limit_side_len' in kwargs: self.limit_side_len = kwargs['limit_side_len'] self.limit_type = kwargs.get('limit_type', 'min') - elif 'resize_long' in kwargs: - self.resize_type = 2 - self.resize_long = kwargs.get('resize_long', 960) - else: + elif 'resize_short' in kwargs: self.limit_side_len = 736 self.limit_type = 'min' + else: + self.resize_type = 2 + self.resize_long = kwargs.get('resize_long', 960) def __call__(self, data): img = deepcopy(data) @@ -226,8 +226,6 @@ class CTCLabelDecode(BaseRecLabelDecode): super(CTCLabelDecode, self).__init__(config) def __call__(self, preds, label=None, *args, **kwargs): - #if isinstance(preds, paddle.Tensor): - # preds = preds.numpy() preds_idx = preds.argmax(axis=2) preds_prob = preds.max(axis=2) text = self.decode(preds_idx, preds_prob, is_remove_duplicate=True) diff --git a/deploy/pdserving/web_service.py b/deploy/pdserving/web_service.py index 43de8355f0..3f77f03537 100644 --- a/deploy/pdserving/web_service.py +++ b/deploy/pdserving/web_service.py @@ -48,13 +48,12 @@ class DetOp(Op): def preprocess(self, input_dicts, data_id, log_id): (_, input_dict), = input_dicts.items() data = base64.b64decode(input_dict["image"].encode('utf8')) + self.raw_im = data data = np.fromstring(data, np.uint8) # Note: class variables(self.var) can only be used in process op mode im = cv2.imdecode(data, cv2.IMREAD_COLOR) - self.im = im self.ori_h, self.ori_w, _ = im.shape - - det_img = self.det_preprocess(self.im) + det_img = self.det_preprocess(im) _, self.new_h, self.new_w = det_img.shape return {"x": det_img[np.newaxis, :].copy()}, False, None, "" @@ -65,7 +64,7 @@ class DetOp(Op): ] dt_boxes_list = self.post_func(det_out, [ratio_list]) dt_boxes = self.filter_func(dt_boxes_list[0], [self.ori_h, self.ori_w]) - out_dict = {"dt_boxes": dt_boxes, "image": self.im} + out_dict = {"dt_boxes": dt_boxes, "image": self.raw_im} return out_dict, None, "" @@ -80,7 +79,9 @@ class RecOp(Op): def preprocess(self, input_dicts, data_id, log_id): (_, input_dict), = input_dicts.items() - im = input_dict["image"] + raw_im = input_dict["image"] + data = np.frombuffer(raw_im, np.uint8) + im = cv2.imdecode(data, cv2.IMREAD_COLOR) dt_boxes = input_dict["dt_boxes"] dt_boxes = self.sorted_boxes(dt_boxes) feed_list = [] @@ -95,7 +96,6 @@ class RecOp(Op): boxes_size = len(dt_boxes) batch_size = boxes_size // max_batch_size rem = boxes_size % max_batch_size - #_LOGGER.info("max_batch_len:{}, batch_size:{}, rem:{}, boxes_size:{}".format(max_batch_size, batch_size, rem, boxes_size)) for bt_idx in range(0, batch_size + 1): imgs = None boxes_num_in_one_batch = 0 @@ -131,6 +131,7 @@ class RecOp(Op): feed_list.append(feed) return feed_list, False, None, "" + def postprocess(self, input_dicts, fetch_data, log_id): res_list = [] if isinstance(fetch_data, dict): From 06379abf67dab1e8d52835c9748b8a0b73bf7291 Mon Sep 17 00:00:00 2001 From: tink2123 Date: Wed, 12 May 2021 20:21:13 +0800 Subject: [PATCH 4/6] add qps --- deploy/pdserving/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/pdserving/config.yml b/deploy/pdserving/config.yml index 6cac1b9fea..2aae922dfa 100644 --- a/deploy/pdserving/config.yml +++ b/deploy/pdserving/config.yml @@ -6,7 +6,7 @@ http_port: 9998 #worker_num, 最大并发数。当build_dag_each_worker=True时, 框架会创建worker_num个进程,每个进程内构建grpcSever和DAG ##当build_dag_each_worker=False时,框架会设置主线程grpc线程池的max_workers=worker_num -worker_num: 5 +worker_num: 10 #build_dag_each_worker, False,框架在进程内创建一条DAG;True,框架会每个进程内创建多个独立的DAG build_dag_each_worker: False @@ -42,7 +42,7 @@ op: #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡 devices: "0" - ir_optim: False + ir_optim: True rec: #并发数,is_thread_op=True时,为线程并发;否则为进程并发 concurrency: 4 @@ -68,4 +68,4 @@ op: #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡 devices: "0" - ir_optim: False + ir_optim: True From cca7cd1e091e39d950454019c1744acbe08d4e4d Mon Sep 17 00:00:00 2001 From: tink2123 Date: Wed, 12 May 2021 20:38:05 +0800 Subject: [PATCH 5/6] add qps --- deploy/pdserving/README.md | 4 +++- deploy/pdserving/README_CN.md | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/deploy/pdserving/README.md b/deploy/pdserving/README.md index b5d8953d73..34b525c472 100644 --- a/deploy/pdserving/README.md +++ b/deploy/pdserving/README.md @@ -156,7 +156,9 @@ The recognition model is the same. Multiple service requests can be sent at the same time if necessary. - The predicted performance data will be automatically written into the `PipelineServingLogs/pipeline.tracer` file: + The predicted performance data will be automatically written into the `PipelineServingLogs/pipeline.tracer` file. + + Tested on 200 real pictures, and limited the detection long side to 960. The average QPS on T4 GPU can reach around 13: ``` 2021-05-12 10:03:24,812 ==================== TRACER ====================== diff --git a/deploy/pdserving/README_CN.md b/deploy/pdserving/README_CN.md index 37d018d0ed..4bb3427199 100644 --- a/deploy/pdserving/README_CN.md +++ b/deploy/pdserving/README_CN.md @@ -153,7 +153,9 @@ python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_rec_in ``` 有需要的话可以同时发送多个服务请求 - 预测性能数据会被自动写入 `PipelineServingLogs/pipeline.tracer` 文件中: + 预测性能数据会被自动写入 `PipelineServingLogs/pipeline.tracer` 文件中。 + + 在200张真实图片上测试,把检测长边限制为960。T4 GPU 上 QPS 均值可达到13左右: ``` 2021-05-12 10:03:24,812 ==================== TRACER ====================== @@ -191,6 +193,8 @@ python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_rec_in 2021-05-12 10:03:24,910 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] ``` + + ## FAQ **Q1**: 发送请求后没有结果返回或者提示输出解码报错 From 4135eb0c77113fca40555cf86c137809c4602d54 Mon Sep 17 00:00:00 2001 From: tink2123 Date: Thu, 13 May 2021 14:03:48 +0800 Subject: [PATCH 6/6] add qps --- deploy/pdserving/README.md | 69 ++++++++++++++++++----------------- deploy/pdserving/README_CN.md | 68 +++++++++++++++++----------------- 2 files changed, 69 insertions(+), 68 deletions(-) diff --git a/deploy/pdserving/README.md b/deploy/pdserving/README.md index 34b525c472..cb5e4bfa01 100644 --- a/deploy/pdserving/README.md +++ b/deploy/pdserving/README.md @@ -158,42 +158,43 @@ The recognition model is the same. The predicted performance data will be automatically written into the `PipelineServingLogs/pipeline.tracer` file. - Tested on 200 real pictures, and limited the detection long side to 960. The average QPS on T4 GPU can reach around 13: + Tested on 200 real pictures, and limited the detection long side to 960. The average QPS on T4 GPU can reach around 23: ``` - 2021-05-12 10:03:24,812 ==================== TRACER ====================== - 2021-05-12 10:03:24,904 Op(rec): - 2021-05-12 10:03:24,904 in[51.5634921875 ms] - 2021-05-12 10:03:24,904 prep[215.310859375 ms] - 2021-05-12 10:03:24,904 midp[33.1617109375 ms] - 2021-05-12 10:03:24,905 postp[10.451234375 ms] - 2021-05-12 10:03:24,905 out[9.736765625 ms] - 2021-05-12 10:03:24,905 idle[0.1914292677880819] - 2021-05-12 10:03:24,905 Op(det): - 2021-05-12 10:03:24,905 in[218.63487096774193 ms] - 2021-05-12 10:03:24,906 prep[357.35925 ms] - 2021-05-12 10:03:24,906 midp[31.47598387096774 ms] - 2021-05-12 10:03:24,906 postp[15.274870967741936 ms] - 2021-05-12 10:03:24,906 out[16.245693548387095 ms] - 2021-05-12 10:03:24,906 idle[0.3675805857279226] - 2021-05-12 10:03:24,906 DAGExecutor: - 2021-05-12 10:03:24,906 Query count[128] - 2021-05-12 10:03:24,906 QPS[12.8 q/s] - 2021-05-12 10:03:24,906 Succ[1.0] - 2021-05-12 10:03:24,907 Error req[] - 2021-05-12 10:03:24,907 Latency: - 2021-05-12 10:03:24,907 ave[798.6557734374998 ms] - 2021-05-12 10:03:24,907 .50[867.936 ms] - 2021-05-12 10:03:24,907 .60[914.507 ms] - 2021-05-12 10:03:24,907 .70[961.064 ms] - 2021-05-12 10:03:24,907 .80[1043.264 ms] - 2021-05-12 10:03:24,907 .90[1117.923 ms] - 2021-05-12 10:03:24,907 .95[1207.056 ms] - 2021-05-12 10:03:24,908 .99[1325.008 ms] - 2021-05-12 10:03:24,908 Channel (server worker num[10]): - 2021-05-12 10:03:24,909 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] - 2021-05-12 10:03:24,909 chl1(In: ['det'], Out: ['rec']) size[1/0] - 2021-05-12 10:03:24,910 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] + + 2021-05-13 03:42:36,895 ==================== TRACER ====================== + 2021-05-13 03:42:36,975 Op(rec): + 2021-05-13 03:42:36,976 in[14.472382882882883 ms] + 2021-05-13 03:42:36,976 prep[9.556855855855856 ms] + 2021-05-13 03:42:36,976 midp[59.921905405405404 ms] + 2021-05-13 03:42:36,976 postp[15.345945945945946 ms] + 2021-05-13 03:42:36,976 out[1.9921216216216215 ms] + 2021-05-13 03:42:36,976 idle[0.16254943864471572] + 2021-05-13 03:42:36,976 Op(det): + 2021-05-13 03:42:36,976 in[315.4468035714286 ms] + 2021-05-13 03:42:36,976 prep[69.5980625 ms] + 2021-05-13 03:42:36,976 midp[18.989535714285715 ms] + 2021-05-13 03:42:36,976 postp[18.857803571428573 ms] + 2021-05-13 03:42:36,977 out[3.1337544642857145 ms] + 2021-05-13 03:42:36,977 idle[0.7477961159203756] + 2021-05-13 03:42:36,977 DAGExecutor: + 2021-05-13 03:42:36,977 Query count[224] + 2021-05-13 03:42:36,977 QPS[22.4 q/s] + 2021-05-13 03:42:36,977 Succ[0.9910714285714286] + 2021-05-13 03:42:36,977 Error req[169, 170] + 2021-05-13 03:42:36,977 Latency: + 2021-05-13 03:42:36,977 ave[535.1678348214285 ms] + 2021-05-13 03:42:36,977 .50[172.651 ms] + 2021-05-13 03:42:36,977 .60[187.904 ms] + 2021-05-13 03:42:36,977 .70[245.675 ms] + 2021-05-13 03:42:36,977 .80[526.684 ms] + 2021-05-13 03:42:36,977 .90[854.596 ms] + 2021-05-13 03:42:36,977 .95[1722.728 ms] + 2021-05-13 03:42:36,977 .99[3990.292 ms] + 2021-05-13 03:42:36,978 Channel (server worker num[10]): + 2021-05-13 03:42:36,978 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] + 2021-05-13 03:42:36,979 chl1(In: ['det'], Out: ['rec']) size[6/0] + 2021-05-13 03:42:36,979 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] ``` diff --git a/deploy/pdserving/README_CN.md b/deploy/pdserving/README_CN.md index 4bb3427199..1e53cb639c 100644 --- a/deploy/pdserving/README_CN.md +++ b/deploy/pdserving/README_CN.md @@ -155,42 +155,42 @@ python3 -m paddle_serving_client.convert --dirname ./ch_ppocr_mobile_v2.0_rec_in 预测性能数据会被自动写入 `PipelineServingLogs/pipeline.tracer` 文件中。 - 在200张真实图片上测试,把检测长边限制为960。T4 GPU 上 QPS 均值可达到13左右: + 在200张真实图片上测试,把检测长边限制为960。T4 GPU 上 QPS 均值可达到23左右: ``` - 2021-05-12 10:03:24,812 ==================== TRACER ====================== - 2021-05-12 10:03:24,904 Op(rec): - 2021-05-12 10:03:24,904 in[51.5634921875 ms] - 2021-05-12 10:03:24,904 prep[215.310859375 ms] - 2021-05-12 10:03:24,904 midp[33.1617109375 ms] - 2021-05-12 10:03:24,905 postp[10.451234375 ms] - 2021-05-12 10:03:24,905 out[9.736765625 ms] - 2021-05-12 10:03:24,905 idle[0.1914292677880819] - 2021-05-12 10:03:24,905 Op(det): - 2021-05-12 10:03:24,905 in[218.63487096774193 ms] - 2021-05-12 10:03:24,906 prep[357.35925 ms] - 2021-05-12 10:03:24,906 midp[31.47598387096774 ms] - 2021-05-12 10:03:24,906 postp[15.274870967741936 ms] - 2021-05-12 10:03:24,906 out[16.245693548387095 ms] - 2021-05-12 10:03:24,906 idle[0.3675805857279226] - 2021-05-12 10:03:24,906 DAGExecutor: - 2021-05-12 10:03:24,906 Query count[128] - 2021-05-12 10:03:24,906 QPS[12.8 q/s] - 2021-05-12 10:03:24,906 Succ[1.0] - 2021-05-12 10:03:24,907 Error req[] - 2021-05-12 10:03:24,907 Latency: - 2021-05-12 10:03:24,907 ave[798.6557734374998 ms] - 2021-05-12 10:03:24,907 .50[867.936 ms] - 2021-05-12 10:03:24,907 .60[914.507 ms] - 2021-05-12 10:03:24,907 .70[961.064 ms] - 2021-05-12 10:03:24,907 .80[1043.264 ms] - 2021-05-12 10:03:24,907 .90[1117.923 ms] - 2021-05-12 10:03:24,907 .95[1207.056 ms] - 2021-05-12 10:03:24,908 .99[1325.008 ms] - 2021-05-12 10:03:24,908 Channel (server worker num[10]): - 2021-05-12 10:03:24,909 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] - 2021-05-12 10:03:24,909 chl1(In: ['det'], Out: ['rec']) size[1/0] - 2021-05-12 10:03:24,910 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] + 2021-05-13 03:42:36,895 ==================== TRACER ====================== + 2021-05-13 03:42:36,975 Op(rec): + 2021-05-13 03:42:36,976 in[14.472382882882883 ms] + 2021-05-13 03:42:36,976 prep[9.556855855855856 ms] + 2021-05-13 03:42:36,976 midp[59.921905405405404 ms] + 2021-05-13 03:42:36,976 postp[15.345945945945946 ms] + 2021-05-13 03:42:36,976 out[1.9921216216216215 ms] + 2021-05-13 03:42:36,976 idle[0.16254943864471572] + 2021-05-13 03:42:36,976 Op(det): + 2021-05-13 03:42:36,976 in[315.4468035714286 ms] + 2021-05-13 03:42:36,976 prep[69.5980625 ms] + 2021-05-13 03:42:36,976 midp[18.989535714285715 ms] + 2021-05-13 03:42:36,976 postp[18.857803571428573 ms] + 2021-05-13 03:42:36,977 out[3.1337544642857145 ms] + 2021-05-13 03:42:36,977 idle[0.7477961159203756] + 2021-05-13 03:42:36,977 DAGExecutor: + 2021-05-13 03:42:36,977 Query count[224] + 2021-05-13 03:42:36,977 QPS[22.4 q/s] + 2021-05-13 03:42:36,977 Succ[0.9910714285714286] + 2021-05-13 03:42:36,977 Error req[169, 170] + 2021-05-13 03:42:36,977 Latency: + 2021-05-13 03:42:36,977 ave[535.1678348214285 ms] + 2021-05-13 03:42:36,977 .50[172.651 ms] + 2021-05-13 03:42:36,977 .60[187.904 ms] + 2021-05-13 03:42:36,977 .70[245.675 ms] + 2021-05-13 03:42:36,977 .80[526.684 ms] + 2021-05-13 03:42:36,977 .90[854.596 ms] + 2021-05-13 03:42:36,977 .95[1722.728 ms] + 2021-05-13 03:42:36,977 .99[3990.292 ms] + 2021-05-13 03:42:36,978 Channel (server worker num[10]): + 2021-05-13 03:42:36,978 chl0(In: ['@DAGExecutor'], Out: ['det']) size[0/0] + 2021-05-13 03:42:36,979 chl1(In: ['det'], Out: ['rec']) size[6/0] + 2021-05-13 03:42:36,979 chl2(In: ['rec'], Out: ['@DAGExecutor']) size[0/0] ```