|
| 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