Ejemplo n.º 1
0
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))
Ejemplo n.º 2
0
 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])
Ejemplo n.º 3
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