def _prepare_transformer_assets(fn: Callable, assets: Dict = None): notebook_path = jputils.get_notebook_path() processor = NotebookProcessor(nb_path=notebook_path, skip_validation=True) fn_source = astutils.get_function_source(fn, strip_signature=False) missing_names = flakeutils.pyflakes_report( processor.get_imports_and_functions() + "\n" + fn_source) if not assets: assets = dict() if not isinstance(assets, dict): ValueError("Please provide preprocessing assets as a dictionary" " mapping variables *names* to their objects") missing_assets = [x not in assets.keys() for x in missing_names] if any(missing_assets): raise RuntimeError( "The following abjects are a dependency for the" " provided preprocessing function. Please add the" " to the `preprocessing_assets` dictionary: %s" % [a for a, m in zip(missing_names, missing_assets) if m]) # save function and assets utils.clean_dir(TRANSFORMER_ASSETS_DIR) marshal.set_data_dir(TRANSFORMER_ASSETS_DIR) marshal.save(fn, TRANSFORMER_FN_ASSET_NAME) for asset_name, asset_value in assets.items(): marshal.save(asset_value, asset_name) # save notebook as well shutil.copy( notebook_path, os.path.join(TRANSFORMER_ASSETS_DIR, TRANSFORMER_SRC_NOTEBOOK_NAME))
def _save(self, values): if self._introspect: # get vars from function locals for var_name in self._outs: if var_name not in self._func.locals: raise RuntimeError("Variable %s not found in function's" " locals" % var_name) marshal_utils.save(self._func.locals[var_name], var_name) else: # get vars from return value if len(self._outs) == 0: return if isinstance(values, tuple): if len(values) != len(self._outs): raise RuntimeError("There is a mismatch between the tuple" " returned by the functions and its" " expected outs. If the functions is" " returning a tuple, make sure the " " return value it is properly" " unpacked.") for name, value in dict(zip(self._outs, values)).items(): marshal_utils.save(value, name) else: # any other object? if len(self._outs) > 1: raise RuntimeError("The function returned a single object," " but there are multiple expected outs:" " %s" % str(self._outs)) marshal_utils.save(values, self._outs[0])
def serve(model: Any, name: str = None, wait: bool = True, predictor: str = None, preprocessing_fn: Callable = None, preprocessing_assets: Dict = None) -> KFServer: """Main API used to serve models from a notebook or a pipeline step. This function procedurally deploys a KFServing InferenceService, starting from a model object. A summary list of actions follows: * Autogenerate an InferenceService name, if not provided * Process transformer function (and related assets) * Dump the model, to a path under a mounted PVC * Snapshot the PVC * Hydrate a new PVC from the new snapshot * Submit an InferenceService CR * Monitor the CR until it becomes ready FIXME: Improve documentation. Provide some examples in the docstring and explain how the preprocessing function parsing works. Args: model: Model object to be used as a predictor name (optional): Name of the predictor. Will be autogenerated if not provided wait (optional): Wait for the InferenceService to become ready. Default: True predictor (optional): Predictor type to be used for the InferenceService. If not provided it will be inferred using the the matching marshalling backends. preprocessing_fn (optional): A processing function that will be deployed as a KFServing Transformer preprocessing_assets (optional): A dictionary with object required by the preprocessing function. This is needed in case the preprocessing function references global objects. Returns: A KFServer instance """ log.info("Starting serve procedure for model '%s'", model) if not name: name = "%s-%s" % (podutils.get_pod_name(), utils.random_string(5)) # Validate and process transformer if preprocessing_fn: _prepare_transformer_assets(preprocessing_fn, preprocessing_assets) # Detect predictor type predictor_type = marshal.get_backend(model).predictor_type if predictor and predictor != predictor_type: raise RuntimeError("Trying to create an InferenceService with" " predictor of type '%s' but the model is of type" " '%s'" % (predictor, predictor_type)) if not predictor_type: log.error( "Kale does not yet support serving objects with '%s'" " backend.\n\nPlease help us improve Kale by opening a new" " issue at:\n" "https://github.com/kubeflow-kale/kale/issues", marshal.get_backend(model).display_name) utils.graceful_exit(-1) predictor = predictor_type # in case `predictor` is None volume = podutils.get_volume_containing_path(PVC_ROOT) volume_name = volume[1].persistent_volume_claim.claim_name log.info("Model is contained in volume '%s'", volume_name) # Dump the model marshal.set_data_dir(PREDICTOR_MODEL_DIR) model_filepath = marshal.save(model, "model") log.info("Model saved successfully at '%s'", model_filepath) # Take snapshot task_info = rokutils.snapshot_pvc(volume_name, bucket=rokutils.SERVING_BUCKET, wait=True) task = rokutils.get_task(task_info["task"]["id"], bucket=rokutils.SERVING_BUCKET) new_pvc_name = "%s-pvc-%s" % (name, utils.random_string(5)) rokutils.hydrate_pvc_from_snapshot(task["result"]["event"]["object"], task["result"]["event"]["version"], new_pvc_name, bucket=rokutils.SERVING_BUCKET) # Cleanup: remove dumped model and transformer assets from the current PVC utils.rm_r( os.path.join(PREDICTOR_MODEL_DIR, os.path.basename(model_filepath))) utils.rm_r(TRANSFORMER_ASSETS_DIR, silent=True) # Need an absolute path from the *root* of the PVC. Add '/' if not exists. pvc_model_path = "/" + PREDICTOR_MODEL_DIR.lstrip(PVC_ROOT) # Tensorflow saves the model's files into a directory by itself if predictor == "tensorflow": pvc_model_path += "/" + os.path.basename(model_filepath).lstrip("/") kfserver = create_inference_service(name=name, predictor=predictor, pvc_name=new_pvc_name, model_path=pvc_model_path, transformer=preprocessing_fn is not None) if wait: monitor_inference_service(kfserver.name) return kfserver