|
| 1 | +# flake8: noqa |
1 | 2 | # Copyright (c) 2022 VisualDL Authors. All Rights Reserve. |
2 | 3 | # |
3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
|
13 | 14 | # limitations under the License. |
14 | 15 | # ======================================================================= |
15 | 16 | import base64 |
16 | | -import glob |
17 | 17 | import hashlib |
18 | 18 | import json |
19 | 19 | import os |
20 | 20 | import shutil |
21 | 21 | import tempfile |
22 | | -from threading import Lock |
23 | 22 |
|
| 23 | +import paddle2onnx |
24 | 24 | from flask import request |
25 | | -from x2paddle.convert import caffe2paddle |
26 | 25 | from x2paddle.convert import onnx2paddle |
27 | 26 |
|
28 | 27 | from .xarfile import archive |
29 | | -from .xarfile import unarchive |
| 28 | +from visualdl.io.bfile import BosFileSystem |
30 | 29 | from visualdl.server.api import gen_result |
31 | 30 | from visualdl.server.api import result |
32 | 31 | from visualdl.utils.dir import X2PADDLE_CACHE_PATH |
|
35 | 34 |
|
36 | 35 |
|
37 | 36 | class ModelConvertApi(object): |
| 37 | + '''! |
| 38 | + Integrate multiple model convertion tools, and provide convertion service for users. |
| 39 | + When user uploads a model to this server, convert model and upload the results to VDL Bos. |
| 40 | + When user downloads the model, we get the data from Bos and send it to client. |
| 41 | + Maybe users can download from bos directy if frontend can achieve it. |
| 42 | + ''' |
| 43 | + |
38 | 44 | def __init__(self): |
39 | | - self.supported_formats = {'onnx', 'caffe'} |
40 | | - self.lock = Lock() |
41 | | - self.server_count = 0 # we use this variable to count requests handled, |
42 | | - # and check the number of files every 100 requests. |
43 | | - # If more than _max_cache_numbers files in cache, we delete the last recent used 50 files. |
| 45 | + ''' |
| 46 | + Initialize a object to provide service. Need a BosFileSystem client to write data. |
| 47 | + ''' |
| 48 | + try: |
| 49 | + self.bos_client = BosFileSystem() |
| 50 | + self.bucket_name = os.getenv("BOS_BUCKET_NAME") |
| 51 | + except Exception: |
| 52 | + # When BOS_HOST, BOS_AK, BOS_SK, BOS_STS are not set in the environment variables. |
| 53 | + # We use VDL BOS by default |
| 54 | + self.bos_client = BosFileSystem(write_flag=False) |
| 55 | + self.bos_client.renew_bos_client_from_server() |
| 56 | + self.bucket_name = 'visualdl-server' |
44 | 57 |
|
45 | 58 | @result() |
46 | | - def convert_model(self, format): |
47 | | - file_handle = request.files['file'] |
48 | | - data = file_handle.stream.read() |
49 | | - if format not in self.supported_formats: |
50 | | - raise RuntimeError('Model format {} is not supported. "\ |
51 | | - "Only onnx and caffe models are supported now.'.format(format)) |
| 59 | + def onnx2paddle_model_convert(self, convert_to_lite, lite_valid_places, |
| 60 | + lite_model_type): # noqa:C901 |
| 61 | + ''' |
| 62 | + Convert onnx model to paddle model. |
| 63 | + ''' |
| 64 | + model_handle = request.files['model'] |
| 65 | + data = model_handle.stream.read() |
52 | 66 | result = {} |
53 | | - result['from'] = format |
54 | | - result['to'] = 'paddle' |
| 67 | + # Do a simple data verification |
| 68 | + if convert_to_lite in ['true', 'True', 'yes', 'Yes', 'y']: |
| 69 | + convert_to_lite = True |
| 70 | + else: |
| 71 | + convert_to_lite = False |
| 72 | + |
| 73 | + if lite_valid_places not in [ |
| 74 | + 'arm', 'opencl', 'x86', 'metal', 'xpu', 'bm', 'mlu', |
| 75 | + 'intel_fpga', 'huawei_ascend_npu', 'imagination_nna', |
| 76 | + 'rockchip_npu', 'mediatek_apu', 'huawei_kirin_npu', |
| 77 | + 'amlogic_npu' |
| 78 | + ]: |
| 79 | + lite_valid_places = 'arm' |
| 80 | + if lite_model_type not in ['protobuf', 'naive_buffer']: |
| 81 | + lite_model_type = 'naive_buffer' |
| 82 | + |
55 | 83 | # call x2paddle to convert models |
56 | 84 | hl = hashlib.md5() |
57 | 85 | hl.update(data) |
58 | 86 | identity = hl.hexdigest() |
59 | 87 | result['request_id'] = identity |
60 | | - target_path = os.path.join(X2PADDLE_CACHE_PATH, identity) |
61 | | - if os.path.exists(target_path): |
62 | | - if os.path.exists( |
63 | | - os.path.join(target_path, 'inference_model', |
64 | | - 'model.pdmodel')): # if data in cache |
65 | | - with open( |
66 | | - os.path.join(target_path, 'inference_model', |
67 | | - 'model.pdmodel'), 'rb') as model_fp: |
68 | | - model_encoded = base64.b64encode( |
69 | | - model_fp.read()).decode('utf-8') |
70 | | - result['pdmodel'] = model_encoded |
| 88 | + # check whether model has been transfromed before |
| 89 | + # if model has been transformed before, data is stored at bos |
| 90 | + pdmodel_filename = 'bos://{}/onnx2paddle/{}/model.pdmodel'.format( |
| 91 | + self.bucket_name, identity) |
| 92 | + if self.bos_client.exists(pdmodel_filename): |
| 93 | + remote_data = self.bos_client.read_file(pdmodel_filename) |
| 94 | + if remote_data: # we should check data is not empty, |
| 95 | + # in case convertion failed but empty data is still uploaded before due to unknown reasons |
| 96 | + model_encoded = base64.b64encode(remote_data).decode('utf-8') |
| 97 | + result['model'] = model_encoded |
71 | 98 | return result |
72 | | - else: |
| 99 | + target_path = os.path.join(X2PADDLE_CACHE_PATH, 'onnx2paddle', |
| 100 | + identity) |
| 101 | + if not os.path.exists(target_path): |
73 | 102 | os.makedirs(target_path, exist_ok=True) |
74 | 103 | with tempfile.NamedTemporaryFile() as fp: |
75 | 104 | fp.write(data) |
76 | 105 | fp.flush() |
77 | 106 | try: |
78 | | - if format == 'onnx': |
79 | | - try: |
80 | | - import onnx # noqa: F401 |
81 | | - except Exception: |
82 | | - raise RuntimeError( |
83 | | - "[ERROR] onnx is not installed, use \"pip install onnx>=1.6.0\"." |
84 | | - ) |
85 | | - onnx2paddle(fp.name, target_path) |
86 | | - elif format == 'caffe': |
87 | | - with tempfile.TemporaryDirectory() as unarchivedir: |
88 | | - unarchive(fp.name, unarchivedir) |
89 | | - prototxt_path = None |
90 | | - weight_path = None |
91 | | - for dirname, subdirs, filenames in os.walk( |
92 | | - unarchivedir): |
93 | | - for filename in filenames: |
94 | | - if '.prototxt' in filename: |
95 | | - prototxt_path = os.path.join( |
96 | | - dirname, filename) |
97 | | - if '.caffemodel' in filename: |
98 | | - weight_path = os.path.join( |
99 | | - dirname, filename) |
100 | | - if prototxt_path is None or weight_path is None: |
101 | | - raise RuntimeError( |
102 | | - ".prototxt or .caffemodel file is missing in your archive file, " |
103 | | - "please check files uploaded.") |
104 | | - caffe2paddle(prototxt_path, weight_path, target_path, |
105 | | - None) |
| 107 | + import onnx # noqa: F401 |
| 108 | + except Exception: |
| 109 | + raise RuntimeError( |
| 110 | + "[ERROR] onnx is not installed, use \"pip install onnx>=1.6.0\"." |
| 111 | + ) |
| 112 | + try: |
| 113 | + if convert_to_lite is False: |
| 114 | + onnx2paddle( |
| 115 | + fp.name, target_path, convert_to_lite=convert_to_lite) |
| 116 | + else: |
| 117 | + onnx2paddle( |
| 118 | + fp.name, |
| 119 | + target_path, |
| 120 | + convert_to_lite=convert_to_lite, |
| 121 | + lite_valid_places=lite_valid_places, |
| 122 | + lite_model_type=lite_model_type) |
106 | 123 | except Exception as e: |
107 | 124 | raise RuntimeError( |
108 | 125 | "[Convertion error] {}.\n Please open an issue at " |
109 | 126 | "https://github.com/PaddlePaddle/X2Paddle/issues to report your problem." |
110 | 127 | .format(e)) |
111 | | - with self.lock: # we need to enter dirname(target_path) to archive, |
112 | | - # in case unneccessary directory added in archive. |
| 128 | + |
113 | 129 | origin_dir = os.getcwd() |
114 | 130 | os.chdir(os.path.dirname(target_path)) |
115 | 131 | archive(os.path.basename(target_path)) |
116 | 132 | os.chdir(origin_dir) |
117 | | - self.server_count += 1 |
| 133 | + with open( |
| 134 | + os.path.join(X2PADDLE_CACHE_PATH, 'onnx2paddle', |
| 135 | + '{}.tar'.format(identity)), 'rb') as f: |
| 136 | + # upload archived transformed model to vdl bos |
| 137 | + data = f.read() |
| 138 | + filename = 'bos://{}/onnx2paddle/{}.tar'.format( |
| 139 | + self.bucket_name, identity) |
| 140 | + try: |
| 141 | + self.bos_client.write(filename, data) |
| 142 | + except Exception as e: |
| 143 | + print( |
| 144 | + "Exception: Write file {}.tar to bos failed, due to {}" |
| 145 | + .format(identity, e)) |
118 | 146 | with open( |
119 | 147 | os.path.join(target_path, 'inference_model', 'model.pdmodel'), |
120 | 148 | 'rb') as model_fp: |
121 | | - model_encoded = base64.b64encode(model_fp.read()).decode('utf-8') |
122 | | - result['pdmodel'] = model_encoded |
| 149 | + # upload pdmodel file to bos, if some model has been transformed before, we can directly download from bos |
| 150 | + filename = 'bos://{}/onnx2paddle/{}/model.pdmodel'.format( |
| 151 | + self.bucket_name, identity) |
| 152 | + data = model_fp.read() |
| 153 | + try: |
| 154 | + self.bos_client.write(filename, data) |
| 155 | + except Exception as e: |
| 156 | + print( |
| 157 | + "Exception: Write file {}/model.pdmodel to bos failed, due to {}" |
| 158 | + .format(identity, e)) |
| 159 | + # return transformed pdmodel file to frontend to show model structure graph |
| 160 | + model_encoded = base64.b64encode(data).decode('utf-8') |
| 161 | + # delete target_path |
| 162 | + shutil.rmtree(target_path) |
| 163 | + result['model'] = model_encoded |
123 | 164 | return result |
124 | 165 |
|
125 | 166 | @result('application/octet-stream') |
126 | | - def download_model(self, request_id): |
127 | | - if os.path.exists( |
128 | | - os.path.join(X2PADDLE_CACHE_PATH, |
129 | | - '{}.tar'.format(request_id))): |
130 | | - with open( |
131 | | - os.path.join(X2PADDLE_CACHE_PATH, |
132 | | - '{}.tar'.format(request_id)), 'rb') as f: |
133 | | - data = f.read() |
134 | | - if self.server_count % 100 == 0: # we check number of files every 100 request |
135 | | - file_paths = glob.glob( |
136 | | - os.path.join(X2PADDLE_CACHE_PATH, '*.tar')) |
137 | | - if len(file_paths) >= _max_cache_numbers: |
138 | | - file_paths = sorted( |
139 | | - file_paths, key=os.path.getctime, reverse=True) |
140 | | - for file_path in file_paths: |
141 | | - try: |
142 | | - os.remove(file_path) |
143 | | - shutil.rmtree( |
144 | | - os.path.join( |
145 | | - os.path.dirname(file_path), |
146 | | - os.path.splitext( |
147 | | - os.path.basename(file_path))[0])) |
148 | | - except Exception: |
149 | | - pass |
150 | | - return data |
| 167 | + def onnx2paddle_model_download(self, request_id): |
| 168 | + ''' |
| 169 | + Download converted paddle model from bos. |
| 170 | + ''' |
| 171 | + filename = 'bos://{}/onnx2paddle/{}.tar'.format( |
| 172 | + self.bucket_name, request_id) |
| 173 | + data = None |
| 174 | + if self.bos_client.exists(filename): |
| 175 | + data = self.bos_client.read_file(filename) |
| 176 | + if not data: |
| 177 | + raise RuntimeError( |
| 178 | + "The requested model can not be downloaded due to not existing or convertion failed." |
| 179 | + ) |
| 180 | + return data |
| 181 | + |
| 182 | + @result() |
| 183 | + def paddle2onnx_convert(self, opset_version, deploy_backend): |
| 184 | + ''' |
| 185 | + Convert paddle model to onnx model. |
| 186 | + ''' |
| 187 | + model_handle = request.files['model'] |
| 188 | + params_handle = request.files['param'] |
| 189 | + model_data = model_handle.stream.read() |
| 190 | + param_data = params_handle.stream.read() |
| 191 | + result = {} |
| 192 | + # Do a simple data verification |
| 193 | + try: |
| 194 | + opset_version = int(opset_version) |
| 195 | + except Exception: |
| 196 | + opset_version = 11 |
| 197 | + if deploy_backend not in ['onnxruntime', 'tensorrt', 'others']: |
| 198 | + deploy_backend = 'onnxruntime' |
| 199 | + |
| 200 | + # call paddle2onnx to convert models |
| 201 | + hl = hashlib.md5() |
| 202 | + hl.update(model_data + param_data) |
| 203 | + identity = hl.hexdigest() |
| 204 | + result['request_id'] = identity |
| 205 | + # check whether model has been transfromed before |
| 206 | + # if model has been transformed before, data is stored at bos |
| 207 | + model_filename = 'bos://{}/paddle2onnx/{}/model.onnx'.format( |
| 208 | + self.bucket_name, identity) |
| 209 | + if self.bos_client.exists(model_filename): |
| 210 | + remote_data = self.bos_client.read_file(model_filename) |
| 211 | + if remote_data: # we should check data is not empty, |
| 212 | + # in case convertion failed but empty data is still uploaded before due to unknown reasons |
| 213 | + model_encoded = base64.b64encode(remote_data).decode('utf-8') |
| 214 | + result['model'] = model_encoded |
| 215 | + return result |
| 216 | + |
| 217 | + with tempfile.NamedTemporaryFile() as model_fp: |
| 218 | + with tempfile.NamedTemporaryFile() as param_fp: |
| 219 | + model_fp.write(model_data) |
| 220 | + param_fp.write(param_data) |
| 221 | + model_fp.flush() |
| 222 | + param_fp.flush() |
| 223 | + try: |
| 224 | + onnx_model = paddle2onnx.export( |
| 225 | + model_fp.name, |
| 226 | + param_fp.name, |
| 227 | + opset_version=opset_version, |
| 228 | + deploy_backend=deploy_backend) |
| 229 | + except Exception as e: |
| 230 | + raise RuntimeError( |
| 231 | + "[Convertion error] {}.\n Please open an issue at " |
| 232 | + "https://github.com/PaddlePaddle/Paddle2ONNX/issues to report your problem." |
| 233 | + .format(e)) |
| 234 | + if not onnx_model: |
| 235 | + raise RuntimeError( |
| 236 | + "[Convertion error] Please check your input model and param files." |
| 237 | + ) |
| 238 | + |
| 239 | + # upload transformed model to vdl bos |
| 240 | + filename = 'bos://{}/paddle2onnx/{}/model.onnx'.format( |
| 241 | + self.bucket_name, identity) |
| 242 | + model_encoded = None |
| 243 | + if onnx_model: |
| 244 | + try: |
| 245 | + self.bos_client.write(filename, onnx_model) |
| 246 | + except Exception as e: |
| 247 | + print( |
| 248 | + "Exception: Write file {}/model.onnx to bos failed, due to {}" |
| 249 | + .format(identity, e)) |
| 250 | + model_encoded = base64.b64encode(onnx_model).decode( |
| 251 | + 'utf-8') |
| 252 | + result['model'] = model_encoded |
| 253 | + return result |
| 254 | + |
| 255 | + @result('application/octet-stream') |
| 256 | + def paddle2onnx_download(self, request_id): |
| 257 | + ''' |
| 258 | + Download converted onnx model from bos. |
| 259 | + ''' |
| 260 | + filename = 'bos://{}/paddle2onnx/{}/model.onnx'.format( |
| 261 | + self.bucket_name, request_id) |
| 262 | + data = None |
| 263 | + if self.bos_client.exists(filename): |
| 264 | + data = self.bos_client.read_file(filename) |
| 265 | + if not data: |
| 266 | + raise RuntimeError( |
| 267 | + "The requested model can not be downloaded due to not existing or convertion failed." |
| 268 | + ) |
| 269 | + return data |
151 | 270 |
|
152 | 271 |
|
153 | 272 | def create_model_convert_api_call(): |
154 | 273 | api = ModelConvertApi() |
155 | 274 | routes = { |
156 | | - 'convert': (api.convert_model, ['format']), |
157 | | - 'download': (api.download_model, ['request_id']) |
| 275 | + 'paddle2onnx/convert': (api.paddle2onnx_convert, |
| 276 | + ['opset_version', 'deploy_backend']), |
| 277 | + 'paddle2onnx/download': (api.paddle2onnx_download, ['request_id']), |
| 278 | + 'onnx2paddle/convert': |
| 279 | + (api.onnx2paddle_model_convert, |
| 280 | + ['convert_to_lite', 'lite_valid_places', 'lite_model_type']), |
| 281 | + 'onnx2paddle/download': (api.onnx2paddle_model_download, |
| 282 | + ['request_id']) |
158 | 283 | } |
159 | 284 |
|
160 | 285 | def call(path: str, args): |
|
0 commit comments