model.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. #
  2. # Copyright 2019 The FATE Authors. All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. import os
  17. import re
  18. from datetime import datetime
  19. import click
  20. import requests
  21. from flow_client.flow_cli.utils import cli_args
  22. from contextlib import closing
  23. from flow_client.flow_cli.utils.cli_utils import preprocess, access_server, prettify, get_project_base_directory, \
  24. check_abs_path
  25. @click.group(short_help="Model Operations")
  26. @click.pass_context
  27. def model(ctx):
  28. """
  29. \b
  30. Provides numbers of model operational commands, including load, store, import and etc.
  31. For more details, please check out the help text.
  32. """
  33. pass
  34. @model.command("load", short_help="Load Model Command")
  35. @cli_args.JOBID
  36. @click.option("-c", "--conf-path", type=click.Path(exists=True),
  37. help="Configuration file path.")
  38. @click.pass_context
  39. def load(ctx, **kwargs):
  40. """
  41. \b
  42. - DESCRIPTION:
  43. Load Model Command
  44. \b
  45. - USAGE:
  46. flow model load -c fate_flow/examples/publish_load_model.json
  47. flow model load -j $JOB_ID
  48. """
  49. if not kwargs.get("conf_path") and not kwargs.get("job_id"):
  50. prettify(
  51. {
  52. "retcode": 100,
  53. "retmsg": "Load model failed. No arguments received, "
  54. "please provide one of arguments from job id and conf path."
  55. }
  56. )
  57. else:
  58. if kwargs.get("conf_path") and kwargs.get("job_id"):
  59. prettify(
  60. {
  61. "retcode": 100,
  62. "retmsg": "Load model failed. Please do not provide job id and "
  63. "conf path at the same time."
  64. }
  65. )
  66. else:
  67. config_data, dsl_data = preprocess(**kwargs)
  68. access_server('post', ctx, 'model/load', config_data)
  69. @model.command("bind", short_help="Bind Model Command")
  70. @cli_args.JOBID
  71. @cli_args.CONF_PATH
  72. @click.pass_context
  73. def bind(ctx, **kwargs):
  74. """
  75. \b
  76. - DESCRIPTION:
  77. Bind Model Command
  78. \b
  79. - USAGE:
  80. flow model bind -c fate_flow/examples/bind_model_service.json
  81. flow model bind -c fate_flow/examples/bind_model_service.json -j $JOB_ID
  82. """
  83. config_data, dsl_data = preprocess(**kwargs)
  84. access_server('post', ctx, 'model/bind', config_data)
  85. @model.command("import", short_help="Import Model Command")
  86. @cli_args.CONF_PATH
  87. @click.option('--from-database', is_flag=True, default=False,
  88. help="If specified and there is a valid database environment, fate flow will import model from database "
  89. "which you specified in configuration file.")
  90. @click.pass_context
  91. def import_model(ctx, **kwargs):
  92. """
  93. \b
  94. - DESCRIPTION:
  95. Import the model from a file or storage engine.
  96. \b
  97. - USAGE:
  98. flow model import -c fate_flow/examples/import_model.json
  99. flow model import -c fate_flow/examples/restore_model.json --from-database
  100. """
  101. config_data, dsl_data = preprocess(**kwargs)
  102. if config_data.pop('from_database'):
  103. access_server('post', ctx, 'model/restore', config_data)
  104. return
  105. file_path = config_data.get("file", None)
  106. if not file_path:
  107. prettify({
  108. 'retcode': 100,
  109. 'retmsg': "Import model failed. Please specify the valid model file path and try again."
  110. })
  111. return
  112. if not os.path.isabs(file_path):
  113. file_path = os.path.join(get_project_base_directory(), file_path)
  114. if not os.path.exists(file_path):
  115. prettify({
  116. 'retcode': 100,
  117. 'retmsg': 'Import model failed. The file is obtained from the fate flow client machine, '
  118. 'but it does not exist, please check the path: {}'.format(file_path),
  119. })
  120. config_data['force_update'] = int(config_data.get('force_update', False))
  121. files = {'file': open(file_path, 'rb')}
  122. access_server('post', ctx, 'model/import', data=config_data, files=files)
  123. @model.command("export", short_help="Export Model Command")
  124. @cli_args.CONF_PATH
  125. @click.option('--to-database', is_flag=True, default=False,
  126. help="If specified and there is a valid database environment, fate flow will export model to database "
  127. "which you specified in configuration file.")
  128. @click.pass_context
  129. def export_model(ctx, **kwargs):
  130. """
  131. \b
  132. - DESCRIPTION:
  133. Export the model to a file or storage engine.
  134. \b
  135. - USAGE:
  136. flow model export -c fate_flow/examples/export_model.json
  137. flow model export -c fate_flow/examples/store_model.json --to-database
  138. """
  139. config_data, dsl_data = preprocess(**kwargs)
  140. if not config_data.pop('to_database'):
  141. with closing(access_server('get', ctx, 'model/export', config_data, False, stream=True)) as response:
  142. if response.status_code == 200:
  143. archive_file_name = re.findall("filename=(.+)", response.headers["Content-Disposition"])[0]
  144. os.makedirs(config_data["output_path"], exist_ok=True)
  145. archive_file_path = os.path.join(config_data["output_path"], archive_file_name)
  146. with open(archive_file_path, 'wb') as fw:
  147. for chunk in response.iter_content(1024):
  148. if chunk:
  149. fw.write(chunk)
  150. response_dict = {'retcode': 0,
  151. 'file': archive_file_path,
  152. 'retmsg': 'download successfully, please check {}'.format(archive_file_path)}
  153. else:
  154. response_dict = response.json() if isinstance(response, requests.models.Response) else response.json
  155. prettify(response_dict)
  156. else:
  157. access_server('post', ctx, 'model/store', config_data)
  158. @model.command("migrate", short_help="Migrate Model Command")
  159. @cli_args.CONF_PATH
  160. @click.pass_context
  161. def migrate(ctx, **kwargs):
  162. """
  163. \b
  164. - DESCRIPTION:
  165. Migrate Model Command.
  166. \b
  167. - USAGE:
  168. flow model migrate -c fate_flow/examples/migrate_model.json
  169. """
  170. config_data, dsl_data = preprocess(**kwargs)
  171. access_server('post', ctx, 'model/migrate', config_data)
  172. @model.command("tag-model", short_help="Tag Model Command")
  173. @cli_args.JOBID_REQUIRED
  174. @cli_args.TAG_NAME_REQUIRED
  175. @click.option("--remove", is_flag=True, default=False,
  176. help="If specified, the name of specified model will be "
  177. "removed from the model name list of specified tag")
  178. @click.pass_context
  179. def tag_model(ctx, **kwargs):
  180. """
  181. \b
  182. - DESCRIPTION:
  183. Tag Model Command.
  184. By default, custom can execute this command to tag model. Or custom could
  185. specify the 'remove' flag to remove the tag from model.
  186. \b
  187. - USAGE:
  188. flow model tag-model -j $JOB_ID -t $TAG_NAME
  189. flow model tag-model -j $JOB_ID -t $TAG_NAME --remove
  190. """
  191. config_data, dsl_data = preprocess(**kwargs)
  192. if not config_data.pop('remove'):
  193. access_server('post', ctx, 'model/model_tag/create', config_data)
  194. else:
  195. access_server('post', ctx, 'model/model_tag/remove', config_data)
  196. @model.command("tag-list", short_help="List Tags of Model Command")
  197. @cli_args.JOBID_REQUIRED
  198. @click.pass_context
  199. def list_tag(ctx, **kwargs):
  200. """
  201. \b
  202. - DESCRIPTION:
  203. List Tags of Model Command.
  204. Custom can query the model by a valid job id, and get the tag list of the specified model.
  205. \b
  206. - USAGE:
  207. flow model tag-list -j $JOB_ID
  208. """
  209. config_data, dsl_data = preprocess(**kwargs)
  210. access_server('post', ctx, 'model/model_tag/retrieve', config_data)
  211. @model.command("get-predict-dsl", short_help="Get predict dsl of model")
  212. @cli_args.MODEL_ID_REQUIRED
  213. @cli_args.MODEL_VERSION_REQUIRED
  214. @cli_args.OUTPUT_PATH_REQUIRED
  215. @click.pass_context
  216. def get_predict_dsl(ctx, **kwargs):
  217. """
  218. \b
  219. - DESCRIPTION:
  220. Get predict DSL of the model.
  221. \b
  222. - USAGE:
  223. flow model get-predict-dsl --model-id $MODEL_ID --model-version $MODEL_VERSION -o ./examples/
  224. """
  225. config_data, dsl_data = preprocess(**kwargs)
  226. dsl_filename = "predict_dsl_{}.json".format(datetime.now().strftime('%Y%m%d%H%M%S'))
  227. output_path = os.path.join(check_abs_path(kwargs.get("output_path")), dsl_filename)
  228. config_data["filename"] = dsl_filename
  229. with closing(access_server('post', ctx, 'model/get/predict/dsl', config_data, False, stream=True)) as response:
  230. if response.status_code == 200:
  231. os.makedirs(os.path.dirname(output_path), exist_ok=True)
  232. with open(output_path, "wb") as fw:
  233. for chunk in response.iter_content(1024):
  234. if chunk:
  235. fw.write(chunk)
  236. res = {'retcode': 0,
  237. 'retmsg': "Query predict dsl successfully. "
  238. "File path is: {}".format(output_path)}
  239. else:
  240. try:
  241. res = response.json() if isinstance(response, requests.models.Response) else response
  242. except Exception:
  243. res = {'retcode': 100,
  244. 'retmsg': "Query predict dsl failed."
  245. "For more details, please check logs/fate_flow/fate_flow_stat.log"}
  246. prettify(res)
  247. @model.command("get-predict-conf", short_help="Get predict conf template")
  248. @cli_args.MODEL_ID_REQUIRED
  249. @cli_args.MODEL_VERSION_REQUIRED
  250. @cli_args.OUTPUT_PATH_REQUIRED
  251. @click.pass_context
  252. def get_predict_conf(ctx, **kwargs):
  253. """
  254. \b
  255. - DESCRIPTION:
  256. Get the template of predict config.
  257. \b
  258. - USAGE:
  259. flow model get-predict-conf --model-id $MODEL_ID --model-version $MODEL_VERSION -o ./examples/
  260. """
  261. config_data, dsl_data = preprocess(**kwargs)
  262. conf_filename = "predict_conf_{}.json".format(datetime.now().strftime('%Y%m%d%H%M%S'))
  263. output_path = os.path.join(check_abs_path(kwargs.get("output_path")), conf_filename)
  264. config_data["filename"] = conf_filename
  265. with closing(access_server('post', ctx, 'model/get/predict/conf', config_data, False, stream=True)) as response:
  266. if response.status_code == 200:
  267. os.makedirs(os.path.dirname(output_path), exist_ok=True)
  268. with open(output_path, "wb") as fw:
  269. for chunk in response.iter_content(1024):
  270. if chunk:
  271. fw.write(chunk)
  272. res = {'retcode': 0,
  273. 'retmsg': "Query predict conf successfully. "
  274. "File path is: {}".format(output_path)}
  275. else:
  276. try:
  277. res = response.json() if isinstance(response, requests.models.Response) else response
  278. except Exception:
  279. res = {'retcode': 100,
  280. 'retmsg': "Query predict conf failed."
  281. "For more details, please check logs/fate_flow/fate_flow_stat.log"}
  282. prettify(res)
  283. @model.command("deploy", short_help="Deploy model")
  284. @cli_args.MODEL_ID_REQUIRED
  285. @cli_args.MODEL_VERSION_REQUIRED
  286. @click.option("--cpn-list", type=click.STRING,
  287. help="User inputs a string to specify component list")
  288. @click.option("--cpn-path", type=click.Path(exists=True),
  289. help="User specifies a file path which records the component list.")
  290. @click.option("--dsl-path", type=click.Path(exists=True),
  291. help="User specified predict dsl file")
  292. @click.option("--cpn-step-index", type=click.STRING, multiple=True,
  293. help="Specify a checkpoint model to replace the pipeline model. "
  294. "Use : to separate component name and step index (E.g. --cpn-step-index cpn_a:123)")
  295. @click.option("--cpn-step-name", type=click.STRING, multiple=True,
  296. help="Specify a checkpoint model to replace the pipeline model. "
  297. "Use : to separate component name and step name (E.g. --cpn-step-name cpn_b:foobar)")
  298. @click.pass_context
  299. def deploy(ctx, **kwargs):
  300. """
  301. \b
  302. - DESCRIPTION:
  303. Deploy model.
  304. \b
  305. - USAGE:
  306. flow model deploy --model-id $MODEL_ID --model-version $MODEL_VERSION
  307. """
  308. request_data = {
  309. 'model_id': kwargs['model_id'],
  310. 'model_version': kwargs['model_version'],
  311. }
  312. if kwargs.get("cpn_list") or kwargs.get("cpn_path"):
  313. if kwargs.get("cpn_list"):
  314. cpn_str = kwargs["cpn_list"]
  315. elif kwargs.get("cpn_path"):
  316. with open(kwargs["cpn_path"], "r") as fp:
  317. cpn_str = fp.read()
  318. else:
  319. cpn_str = ""
  320. if isinstance(cpn_str, list):
  321. cpn_list = cpn_str
  322. else:
  323. if (cpn_str.find("/") and cpn_str.find("\\")) != -1:
  324. raise Exception("Component list string should not contain '/' or '\\'.")
  325. cpn_str = cpn_str.replace(" ", "").replace("\n", "").strip(",[]")
  326. cpn_list = cpn_str.split(",")
  327. request_data['cpn_list'] = cpn_list
  328. elif kwargs.get("dsl_path"):
  329. with open(kwargs["dsl_path"], "r") as ft:
  330. predict_dsl = ft.read()
  331. request_data['dsl'] = predict_dsl
  332. request_data['components_checkpoint'] = {}
  333. for i in ('cpn_step_index', 'cpn_step_name'):
  334. for j in kwargs[i]:
  335. component, checkpoint = j.rsplit(':', 1)
  336. if i == 'cpn_step_index':
  337. checkpoint = int(checkpoint)
  338. if component in request_data['components_checkpoint']:
  339. raise KeyError(f"Duplicated component name '{component}'.")
  340. request_data['components_checkpoint'][component] = {
  341. i[4:]: checkpoint,
  342. }
  343. config_data, dsl_data = preprocess(**request_data)
  344. access_server('post', ctx, 'model/deploy', config_data)
  345. @model.command("get-model-info", short_help="Get model info")
  346. @cli_args.MODEL_ID
  347. @cli_args.MODEL_VERSION_REQUIRED
  348. @cli_args.ROLE
  349. @cli_args.PARTYID
  350. @click.option('--detail', is_flag=True, default=False,
  351. help="If specified, details of model will be shown.")
  352. @click.pass_context
  353. def get_model_info(ctx, **kwargs):
  354. """
  355. \b
  356. - DESCRIPTION:
  357. Get model information.
  358. \b
  359. - USAGE:
  360. flow model model-info --model-id $MODEL_ID --model-version $MODEL_VERSION
  361. flow model model-info --model-id $MODEL_ID --model-version $MODEL_VERSION --detail
  362. """
  363. config_data, dsl_data = preprocess(**kwargs)
  364. if not config_data.pop('detail'):
  365. config_data['query_filters'] = ['create_date', 'role', 'party_id', 'roles', 'model_id',
  366. 'model_version', 'loaded_times', 'size', 'description', 'parent', 'parent_info']
  367. access_server('post', ctx, 'model/query', config_data)
  368. @model.command("homo-convert", short_help="Convert trained homogenous model")
  369. @cli_args.CONF_PATH
  370. @click.pass_context
  371. def homo_convert_model(ctx, **kwargs):
  372. """
  373. \b
  374. - DESCRIPTION:
  375. Convert trained homogenous model to the format of another ML framework. Converted model files
  376. will be saved alongside the original model and can be downloaded via model export command.
  377. The supported conversions are:
  378. HomoLR to `sklearn.linear_model.LogisticRegression`
  379. HomoNN to `tf.keras.Sequential` or `torch.nn.Sequential`, depending on the originally-used backend type.
  380. \b
  381. - USAGE:
  382. flow model homo-convert -c fate_flow/examples/homo_convert_model.json
  383. """
  384. config_data, dsl_data = preprocess(**kwargs)
  385. access_server('post', ctx, 'model/homo/convert', config_data)
  386. @model.command("homo-deploy", short_help="Deploy trained homogenous model")
  387. @cli_args.CONF_PATH
  388. @click.pass_context
  389. def homo_deploy_model(ctx, **kwargs):
  390. """
  391. \b
  392. - DESCRIPTION:
  393. Deploy trained homogenous model to a target online serving system. The model must be
  394. converted beforehand.
  395. Currently the supported target serving system is KFServing. Refer to the example json
  396. for detailed parameters.
  397. \b
  398. - USAGE:
  399. flow model homo-deploy -c fate_flow/examples/homo_deploy_model.json
  400. """
  401. config_data, dsl_data = preprocess(**kwargs)
  402. if config_data.get('deployment_type') == "kfserving":
  403. kube_config = config_data.get('deployment_parameters', {}).get('config_file')
  404. if kube_config:
  405. if check_abs_path(kube_config):
  406. with open(kube_config, 'r') as fp:
  407. config_data['deployment_parameters']['config_file_content'] = fp.read()
  408. del config_data['deployment_parameters']['config_file']
  409. else:
  410. prettify(
  411. {
  412. "retcode": 100,
  413. "retmsg": "The kube_config file is obtained from the fate flow client machine, "
  414. "but it does not exist. Please check the path: {}".format(kube_config)
  415. }
  416. )
  417. return
  418. access_server('post', ctx, 'model/homo/deploy', config_data)