def log_model(onnx_model, artifact_path, conda_env=None, registered_model_name=None, signature: ModelSignature = None, input_example: ModelInputExample = None): """ Log an ONNX model as an MLflow artifact for the current run. :param onnx_model: ONNX model to be saved. :param artifact_path: Run-relative artifact path. :param conda_env: Either a dictionary representation of a Conda environment or the path to a Conda environment yaml file. If provided, this decsribes the environment this model should be run in. At minimum, it should specify the dependencies contained in :func:`get_default_conda_env()`. If `None`, the default :func:`get_default_conda_env()` environment is added to the model. The following is an *example* dictionary representation of a Conda environment:: { 'name': 'mlflow-env', 'channels': ['defaults'], 'dependencies': [ 'python=3.6.0', 'onnx=1.4.1', 'onnxruntime=0.3.0' ] } :param registered_model_name: (Experimental) If given, create a model version under ``registered_model_name``, also creating a registered model if one with the given name does not exist. :param signature: (Experimental) :py:class:`ModelSignature <mlflow.models.ModelSignature>` describes model input and output :py:class:`Schema <mlflow.types.Schema>`. The model signature can be :py:func:`inferred <mlflow.models.infer_signature>` from datasets with valid model input (e.g. the training dataset with target column omitted) and valid model output (e.g. model predictions generated on the training dataset), for example: .. code-block:: python from mlflow.models.signature import infer_signature train = df.drop_column("target_label") predictions = ... # compute model predictions signature = infer_signature(train, predictions) :param input_example: (Experimental) Input example provides one or several instances of valid model input. The example can be used as a hint of what data to feed the model. The given example will be converted to a Pandas DataFrame and then serialized to json using the Pandas split-oriented format. Bytes are base64-encoded. """ Model.log(artifact_path=artifact_path, flavor=kiwi.onnx, onnx_model=onnx_model, conda_env=conda_env, registered_model_name=registered_model_name, signature=signature, input_example=input_example)
def test_mleap_module_model_save_with_absolute_path_and_valid_sample_input_produces_mleap_flavor( spark_model_iris, model_path): model_path = os.path.abspath(model_path) mlflow_model = Model() mleap.save_model(spark_model=spark_model_iris.model, path=model_path, sample_input=spark_model_iris.spark_df, mlflow_model=mlflow_model) assert mleap.FLAVOR_NAME in mlflow_model.flavors config_path = os.path.join(model_path, "MLmodel") assert os.path.exists(config_path) config = Model.load(config_path) assert mleap.FLAVOR_NAME in config.flavors
def test_mleap_module_model_save_with_relative_path_and_valid_sample_input_produces_mleap_flavor( spark_model_iris): with TempDir(chdr=True) as tmp: model_path = os.path.basename(tmp.path("model")) mlflow_model = Model() mleap.save_model(spark_model=spark_model_iris.model, path=model_path, sample_input=spark_model_iris.spark_df, mlflow_model=mlflow_model) assert mleap.FLAVOR_NAME in mlflow_model.flavors config_path = os.path.join(model_path, "MLmodel") assert os.path.exists(config_path) config = Model.load(config_path) assert mleap.FLAVOR_NAME in config.flavors
def test_load_pyfunc_succeeds_for_older_models_with_pyfunc_data_field( sklearn_knn_model, model_path): """ This test verifies that scikit-learn models saved in older versions of MLflow are loaded successfully by ``mlflow.pyfunc.load_model``. These older models specify a pyfunc ``data`` field referring directly to a serialized scikit-learn model file. In contrast, newer models omit the ``data`` field. """ kiwi.sklearn.save_model( sk_model=sklearn_knn_model.model, path=model_path, serialization_format=kiwi.sklearn.SERIALIZATION_FORMAT_PICKLE) model_conf_path = os.path.join(model_path, "MLmodel") model_conf = Model.load(model_conf_path) pyfunc_conf = model_conf.flavors.get(pyfunc.FLAVOR_NAME) sklearn_conf = model_conf.flavors.get(kiwi.sklearn.FLAVOR_NAME) assert sklearn_conf is not None assert pyfunc_conf is not None pyfunc_conf[pyfunc.DATA] = sklearn_conf["pickled_model"] reloaded_knn_pyfunc = pyfunc.load_pyfunc(model_uri=model_path) np.testing.assert_array_equal( sklearn_knn_model.model.predict(sklearn_knn_model.inference_data), reloaded_knn_pyfunc.predict(sklearn_knn_model.inference_data))
def test_validate_deployment_flavor_validates_python_function_flavor_successfully( pretrained_model): model_config_path = os.path.join( _download_artifact_from_uri(pretrained_model.model_uri), "MLmodel") model_config = Model.load(model_config_path) mfs._validate_deployment_flavor(model_config=model_config, flavor=kiwi.pyfunc.FLAVOR_NAME)
def test_load_model_with_differing_pytorch_version_logs_warning(sequential_model, model_path): kiwi.pytorch.save_model(pytorch_model=sequential_model, path=model_path) saver_pytorch_version = "1.0" model_config_path = os.path.join(model_path, "MLmodel") model_config = Model.load(model_config_path) model_config.flavors[kiwi.pytorch.FLAVOR_NAME]["pytorch_version"] = saver_pytorch_version model_config.save(model_config_path) log_messages = [] def custom_warn(message_text, *args, **kwargs): log_messages.append(message_text % args % kwargs) loader_pytorch_version = "0.8.2" with mock.patch("mlflow.pytorch._logger.warning") as warn_mock,\ mock.patch("torch.__version__") as torch_version_mock: torch_version_mock.__str__ = lambda *args, **kwargs: loader_pytorch_version warn_mock.side_effect = custom_warn kiwi.pytorch.load_model(model_uri=model_path) assert any([ "does not match installed PyTorch version" in log_message and saver_pytorch_version in log_message and loader_pytorch_version in log_message for log_message in log_messages ])
def _get_flavor_configuration_from_uri(model_uri, flavor_name): """ Obtains the configuration for the specified flavor from the specified MLflow model uri. If the model does not contain the specified flavor, an exception will be thrown. :param model_uri: The path to the root directory of the MLflow model for which to load the specified flavor configuration. :param flavor_name: The name of the flavor configuration to load. :return: The flavor configuration as a dictionary. """ try: ml_model_file = _download_artifact_from_uri( artifact_uri=append_to_uri_path(model_uri, MLMODEL_FILE_NAME)) except Exception as ex: raise MlflowException( "Failed to download an \"{model_file}\" model file from \"{model_uri}\": {ex}" .format(model_file=MLMODEL_FILE_NAME, model_uri=model_uri, ex=ex), RESOURCE_DOES_NOT_EXIST) model_conf = Model.load(ml_model_file) if flavor_name not in model_conf.flavors: raise MlflowException( "Model does not have the \"{flavor_name}\" flavor".format( flavor_name=flavor_name), RESOURCE_DOES_NOT_EXIST) return model_conf.flavors[flavor_name]
def test_signature_and_examples_are_saved_correctly(sklearn_knn_model, iris_data): data = iris_data signature_ = infer_signature(*data) example_ = data[0][:3, ] for signature in (None, signature_): for example in (None, example_): with TempDir() as tmp: with open(tmp.path("skmodel"), "wb") as f: pickle.dump(sklearn_knn_model, f) path = tmp.path("model") kiwi.pyfunc.save_model( path=path, data_path=tmp.path("skmodel"), loader_module=os.path.basename(__file__)[:-3], code_path=[__file__], signature=signature, input_example=example) mlflow_model = Model.load(path) assert signature == mlflow_model.signature if example is None: assert mlflow_model.saved_input_example_info is None else: assert all((_read_example(mlflow_model, path) == example).all())
def test_load_model_succeeds_when_data_is_model_file_instead_of_directory( module_scoped_subclassed_model, model_path, data): """ This test verifies that PyTorch models saved in older versions of MLflow are loaded successfully by ``mlflow.pytorch.load_model``. The ``data`` path associated with these older models is serialized PyTorch model file, as opposed to the current format: a directory containing a serialized model file and pickle module information. """ artifact_path = "pytorch_model" with kiwi.start_run(): kiwi.pytorch.log_model( artifact_path=artifact_path, pytorch_model=module_scoped_subclassed_model, conda_env=None) model_path = _download_artifact_from_uri("runs:/{run_id}/{artifact_path}".format( run_id=kiwi.active_run().info.run_id, artifact_path=artifact_path)) model_conf_path = os.path.join(model_path, "MLmodel") model_conf = Model.load(model_conf_path) pyfunc_conf = model_conf.flavors.get(pyfunc.FLAVOR_NAME) assert pyfunc_conf is not None model_data_path = os.path.join(model_path, pyfunc_conf[pyfunc.DATA]) assert os.path.exists(model_data_path) assert kiwi.pytorch._SERIALIZED_TORCH_MODEL_FILE_NAME in os.listdir(model_data_path) pyfunc_conf[pyfunc.DATA] = os.path.join( model_data_path, kiwi.pytorch._SERIALIZED_TORCH_MODEL_FILE_NAME) model_conf.save(model_conf_path) loaded_pyfunc = pyfunc.load_pyfunc(model_path) np.testing.assert_array_almost_equal( loaded_pyfunc.predict(data[0]), pd.DataFrame(_predict(model=module_scoped_subclassed_model, data=data)), decimal=4)
def _get_flavor_configuration(model_path, flavor_name): """ Obtains the configuration for the specified flavor from the specified MLflow model path. If the model does not contain the specified flavor, an exception will be thrown. :param model_path: The path to the root directory of the MLflow model for which to load the specified flavor configuration. :param flavor_name: The name of the flavor configuration to load. :return: The flavor configuration as a dictionary. """ model_configuration_path = os.path.join(model_path, MLMODEL_FILE_NAME) if not os.path.exists(model_configuration_path): raise MlflowException( "Could not find an \"{model_file}\" configuration file at \"{model_path}\"" .format(model_file=MLMODEL_FILE_NAME, model_path=model_path), RESOURCE_DOES_NOT_EXIST) model_conf = Model.load(model_configuration_path) if flavor_name not in model_conf.flavors: raise MlflowException( "Model does not have the \"{flavor_name}\" flavor".format( flavor_name=flavor_name), RESOURCE_DOES_NOT_EXIST) conf = model_conf.flavors[flavor_name] return conf
def test_load_model_with_differing_cloudpickle_version_at_micro_granularity_logs_warning( model_path): class TestModel(kiwi.pyfunc.PythonModel): def predict(self, context, model_input): return model_input kiwi.pyfunc.save_model(path=model_path, python_model=TestModel()) saver_cloudpickle_version = "0.5.8" model_config_path = os.path.join(model_path, "MLmodel") model_config = Model.load(model_config_path) model_config.flavors[kiwi.pyfunc.FLAVOR_NAME][ kiwi.pyfunc.model. CONFIG_KEY_CLOUDPICKLE_VERSION] = saver_cloudpickle_version model_config.save(model_config_path) log_messages = [] def custom_warn(message_text, *args, **kwargs): log_messages.append(message_text % args % kwargs) loader_cloudpickle_version = "0.5.7" with mock.patch("mlflow.pyfunc._logger.warning") as warn_mock, \ mock.patch("cloudpickle.__version__") as cloudpickle_version_mock: cloudpickle_version_mock.__str__ = lambda *args, **kwargs: loader_cloudpickle_version warn_mock.side_effect = custom_warn kiwi.pyfunc.load_pyfunc(model_uri=model_path) assert any([ "differs from the version of CloudPickle that is currently running" in log_message and saver_cloudpickle_version in log_message and loader_cloudpickle_version in log_message for log_message in log_messages ])
def test_signature_and_examples_are_saved_correctly(iris_data, main_scoped_model_class): def test_predict(sk_model, model_input): return sk_model.predict(model_input) * 2 data = iris_data signature_ = infer_signature(*data) example_ = data[0][:3, ] for signature in (None, signature_): for example in (None, example_): with TempDir() as tmp: path = tmp.path("model") kiwi.pyfunc.save_model( path=path, artifacts={}, python_model=main_scoped_model_class(test_predict), signature=signature, input_example=example) mlflow_model = Model.load(path) assert signature == mlflow_model.signature if example is None: assert mlflow_model.saved_input_example_info is None else: assert all((_read_example(mlflow_model, path) == example).all())
def test_model_log_load(sklearn_knn_model, main_scoped_model_class, iris_data): sklearn_artifact_path = "sk_model" with kiwi.start_run(): kiwi.sklearn.log_model(sk_model=sklearn_knn_model, artifact_path=sklearn_artifact_path) sklearn_model_uri = "runs:/{run_id}/{artifact_path}".format( run_id=kiwi.active_run().info.run_id, artifact_path=sklearn_artifact_path) def test_predict(sk_model, model_input): return sk_model.predict(model_input) * 2 pyfunc_artifact_path = "pyfunc_model" with kiwi.start_run(): kiwi.pyfunc.log_model( artifact_path=pyfunc_artifact_path, artifacts={ "sk_model": sklearn_model_uri, }, python_model=main_scoped_model_class(test_predict)) pyfunc_model_uri = "runs:/{run_id}/{artifact_path}".format( run_id=kiwi.active_run().info.run_id, artifact_path=pyfunc_artifact_path) pyfunc_model_path = _download_artifact_from_uri( "runs:/{run_id}/{artifact_path}".format( run_id=kiwi.active_run().info.run_id, artifact_path=pyfunc_artifact_path)) model_config = Model.load(os.path.join(pyfunc_model_path, "MLmodel")) loaded_pyfunc_model = kiwi.pyfunc.load_pyfunc(model_uri=pyfunc_model_uri) assert model_config.to_yaml() == loaded_pyfunc_model.metadata.to_yaml() np.testing.assert_array_equal( loaded_pyfunc_model.predict(iris_data[0]), test_predict(sk_model=sklearn_knn_model, model_input=iris_data[0]))
def test_build_image_includes_default_metadata_in_azure_image_and_model_tags( sklearn_model): artifact_path = "model" with kiwi.start_run(): kiwi.sklearn.log_model(sk_model=sklearn_model, artifact_path=artifact_path) run_id = kiwi.active_run().info.run_id model_uri = "runs:///{run_id}/{artifact_path}".format( run_id=run_id, artifact_path=artifact_path) model_config = Model.load( os.path.join(_download_artifact_from_uri(artifact_uri=model_uri), "MLmodel")) with AzureMLMocks() as aml_mocks: workspace = get_azure_workspace() kiwi.azureml.build_image(model_uri=model_uri, workspace=workspace) register_model_call_args = aml_mocks["register_model"].call_args_list assert len(register_model_call_args) == 1 _, register_model_call_kwargs = register_model_call_args[0] called_tags = register_model_call_kwargs["tags"] assert called_tags["model_uri"] == model_uri assert called_tags["python_version"] ==\ model_config.flavors[pyfunc.FLAVOR_NAME][pyfunc.PY_VERSION] create_image_call_args = aml_mocks["create_image"].call_args_list assert len(create_image_call_args) == 1 _, create_image_call_kwargs = create_image_call_args[0] image_config = create_image_call_kwargs["image_config"] assert image_config.tags["model_uri"] == model_uri assert image_config.tags["python_version"] ==\ model_config.flavors[pyfunc.FLAVOR_NAME][pyfunc.PY_VERSION]
def run_local(model_uri, port=5000, image=DEFAULT_IMAGE_NAME, flavor=None): """ Serve model locally in a SageMaker compatible Docker container. :param model_uri: The location, in URI format, of the MLflow model to serve locally, for example: - ``/Users/me/path/to/local/model`` - ``relative/path/to/local/model`` - ``s3://my_bucket/path/to/model`` - ``runs:/<mlflow_run_id>/run-relative/path/to/model`` - ``models:/<model_name>/<model_version>`` - ``models:/<model_name>/<stage>`` For more information about supported URI schemes, see `Referencing Artifacts <https://www.mlflow.org/docs/latest/concepts.html# artifact-locations>`_. :param port: Local port. :param image: Name of the Docker image to be used. :param flavor: The name of the flavor of the model to use for local serving. If ``None``, a flavor is automatically selected from the model's available flavors. If the specified flavor is not present or not supported for deployment, an exception is thrown. """ model_path = _download_artifact_from_uri(model_uri) model_config_path = os.path.join(model_path, MLMODEL_FILE_NAME) model_config = Model.load(model_config_path) if flavor is None: flavor = _get_preferred_deployment_flavor(model_config) else: _validate_deployment_flavor(model_config, flavor) print("Using the {selected_flavor} flavor for local serving!".format( selected_flavor=flavor)) deployment_config = _get_deployment_config(flavor_name=flavor) _logger.info("launching docker image with path %s", model_path) cmd = [ "docker", "run", "-v", "{}:/opt/ml/model/".format(model_path), "-p", "%d:8080" % port ] for key, value in deployment_config.items(): cmd += ["-e", "{key}={value}".format(key=key, value=value)] cmd += ["--rm", image, "serve"] _logger.info('executing: %s', ' '.join(cmd)) proc = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, universal_newlines=True) def _sigterm_handler(*_): _logger.info("received termination signal => killing docker process") proc.send_signal(signal.SIGINT) import signal signal.signal(signal.SIGTERM, _sigterm_handler) proc.wait()
def test_get_flavor_configuration_with_present_flavor_returns_expected_configuration( sklearn_knn_model, model_path): kiwi.sklearn.save_model(sk_model=sklearn_knn_model, path=model_path) sklearn_flavor_config = mlflow_model_utils._get_flavor_configuration( model_path=model_path, flavor_name=kiwi.sklearn.FLAVOR_NAME) model_config = Model.load(os.path.join(model_path, "MLmodel")) assert sklearn_flavor_config == model_config.flavors[ kiwi.sklearn.FLAVOR_NAME]
def test_model_save_load(sklearn_knn_model, iris_data, tmpdir, model_path): sk_model_path = os.path.join(str(tmpdir), "knn.pkl") with open(sk_model_path, "wb") as f: pickle.dump(sklearn_knn_model, f) model_config = Model(run_id="test", artifact_path="testtest") kiwi.pyfunc.save_model(path=model_path, data_path=sk_model_path, loader_module=os.path.basename(__file__)[:-3], code_path=[__file__], mlflow_model=model_config) reloaded_model_config = Model.load(os.path.join(model_path, "MLmodel")) assert model_config.__dict__ == reloaded_model_config.__dict__ assert kiwi.pyfunc.FLAVOR_NAME in reloaded_model_config.flavors assert kiwi.pyfunc.PY_VERSION in reloaded_model_config.flavors[ kiwi.pyfunc.FLAVOR_NAME] reloaded_model = kiwi.pyfunc.load_pyfunc(model_path) np.testing.assert_array_equal(sklearn_knn_model.predict(iris_data[0]), reloaded_model.predict(iris_data[0]))
def test_get_preferred_deployment_flavor_obtains_valid_flavor_from_model( pretrained_model): model_config_path = os.path.join( _download_artifact_from_uri(pretrained_model.model_uri), "MLmodel") model_config = Model.load(model_config_path) selected_flavor = mfs._get_preferred_deployment_flavor( model_config=model_config) assert selected_flavor in mfs.SUPPORTED_DEPLOYMENT_FLAVORS assert selected_flavor in model_config.flavors
def test_schema_enforcement_no_col_names(): class TestModel(object): @staticmethod def predict(pdf): return pdf m = Model() input_schema = Schema([ ColSpec("double"), ColSpec("double"), ColSpec("double"), ]) m.signature = ModelSignature(inputs=input_schema) pyfunc_model = PyFuncModel(model_meta=m, model_impl=TestModel()) test_data = [[1.0, 2.0, 3.0]] # Can call with just a list assert pyfunc_model.predict(test_data).equals(pd.DataFrame(test_data)) # Or can call with a DataFrame without column names assert pyfunc_model.predict(pd.DataFrame(test_data)).equals( pd.DataFrame(test_data)) # Or with column names! pdf = pd.DataFrame(data=test_data, columns=["a", "b", "c"]) assert pyfunc_model.predict(pdf).equals(pdf) # Must provide the right number of arguments with pytest.raises(MlflowException) as ex: pyfunc_model.predict([[1.0, 2.0]]) assert "the provided input only has 2 columns." in str(ex) # Must provide the right types with pytest.raises(MlflowException) as ex: pyfunc_model.predict([[1, 2, 3]]) assert "Can not safely convert int64 to float64" in str(ex) # Can only provide data frames or lists... with pytest.raises(MlflowException) as ex: pyfunc_model.predict(set([1, 2, 3])) assert "Expected input to be DataFrame or list. Found: set" in str(ex)
def test_model_log_with_pyfunc_flavor(spacy_model_with_data): artifact_path = "model" with kiwi.start_run(): kiwi.spacy.log_model(spacy_model=spacy_model_with_data.model, artifact_path=artifact_path) model_path = _download_artifact_from_uri( "runs:/{run_id}/{artifact_path}".format( run_id=kiwi.active_run().info.run_id, artifact_path=artifact_path)) loaded_model = Model.load(model_path) assert pyfunc.FLAVOR_NAME in loaded_model.flavors
def test_deploy_throws_exception_if_model_does_not_contain_pyfunc_flavor( sklearn_model, model_path): kiwi.sklearn.save_model(sk_model=sklearn_model, path=model_path) model_config_path = os.path.join(model_path, "MLmodel") model_config = Model.load(model_config_path) del model_config.flavors[pyfunc.FLAVOR_NAME] model_config.save(model_config_path) with AzureMLMocks(), pytest.raises(MlflowException) as exc: workspace = get_azure_workspace() kiwi.azureml.deploy(model_uri=model_path, workspace=workspace) assert exc.error_code == INVALID_PARAMETER_VALUE
def test_deploy_throws_exception_if_model_python_version_is_less_than_three( sklearn_model, model_path): kiwi.sklearn.save_model(sk_model=sklearn_model, path=model_path) model_config_path = os.path.join(model_path, "MLmodel") model_config = Model.load(model_config_path) model_config.flavors[pyfunc.FLAVOR_NAME][pyfunc.PY_VERSION] = "2.7.6" model_config.save(model_config_path) with AzureMLMocks(), pytest.raises(MlflowException) as exc: workspace = get_azure_workspace() kiwi.azureml.deploy(model_uri=model_path, workspace=workspace) assert exc.error_code == INVALID_PARAMETER_VALUE
def _save_model_with_loader_module_and_data_path(path, loader_module, data_path=None, code_paths=None, conda_env=None, mlflow_model=Model()): """ Export model as a generic Python function model. :param path: The path to which to save the Python model. :param loader_module: The name of the Python module that is used to load the model from ``data_path``. This module must define a method with the prototype ``_load_pyfunc(data_path)``. :param data_path: Path to a file or directory containing model data. :param code_paths: A list of local filesystem paths to Python file dependencies (or directories containing file dependencies). These files are *prepended* to the system path before the model is loaded. :param conda_env: Either a dictionary representation of a Conda environment or the path to a Conda environment yaml file. If provided, this decsribes the environment this model should be run in. :return: Model configuration containing model info. """ code = None data = None if data_path is not None: model_file = _copy_file_or_tree(src=data_path, dst=path, dst_dir="data") data = model_file if code_paths is not None: for code_path in code_paths: _copy_file_or_tree(src=code_path, dst=path, dst_dir="code") code = "code" conda_env_subpath = "mlflow_env.yml" if conda_env is None: conda_env = get_default_conda_env() elif not isinstance(conda_env, dict): with open(conda_env, "r") as f: conda_env = yaml.safe_load(f) with open(os.path.join(path, conda_env_subpath), "w") as f: yaml.safe_dump(conda_env, stream=f, default_flow_style=False) kiwi.pyfunc.add_to_model(mlflow_model, loader_module=loader_module, code=code, data=data, env=conda_env_subpath) mlflow_model.save(os.path.join(path, MLMODEL_FILE_NAME)) return mlflow_model
def test_log_model(mlflow_client, backend_store_uri): experiment_id = mlflow_client.create_experiment('Log models') with TempDir(chdr=True): kiwi.set_experiment("Log models") model_paths = ["model/path/{}".format(i) for i in range(3)] with kiwi.start_run(experiment_id=experiment_id) as run: for i, m in enumerate(model_paths): kiwi.pyfunc.log_model(m, loader_module="mlflow.pyfunc") kiwi.pyfunc.save_model(m, mlflow_model=Model( artifact_path=m, run_id=run.info.run_id), loader_module="mlflow.pyfunc") model = Model.load(os.path.join(m, "MLmodel")) run = kiwi.get_run(run.info.run_id) tag = run.data.tags["mlflow.log-model.history"] models = json.loads(tag) model.utc_time_created = models[i]["utc_time_created"] assert models[i] == model.to_dict() assert len(models) == i + 1 for j in range(0, i + 1): assert models[j]["artifact_path"] == model_paths[j]
def test_deployment_of_model_with_no_supported_flavors_raises_exception( pretrained_model): logged_model_path = _download_artifact_from_uri(pretrained_model.model_uri) model_config_path = os.path.join(logged_model_path, "MLmodel") model_config = Model.load(model_config_path) del model_config.flavors[kiwi.pyfunc.FLAVOR_NAME] model_config.save(path=model_config_path) with pytest.raises(MlflowException) as exc: mfs.deploy(app_name="missing-flavor", model_uri=logged_model_path, flavor=None) assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_model_with_no_deployable_flavors_fails_pollitely(): from kiwi.models import Model with TempDir(chdr=True) as tmp: m = Model(artifact_path=None, run_id=None, utc_time_created="now", flavors={ "some": {}, "useless": {}, "flavors": {} }) os.mkdir(tmp.path("model")) m.save(tmp.path("model", "MLmodel")) # The following should fail because there should be no suitable flavor p = subprocess.Popen( ["mlflow", "models", "predict", "-m", tmp.path("model")], stderr=subprocess.PIPE, cwd=tmp.path("")) _, stderr = p.communicate() stderr = stderr.decode("utf-8") print(stderr) assert p.wait() != 0 assert "No suitable flavor backend was found for the model." in stderr
def _save_example(mlflow_model: Model, input_example: ModelInputExample, path: str): """ Save example to a file on the given path and updates passed Model with example metadata. The metadata is a dictionary with the following fields: - 'artifact_path': example path relative to the model directory. - 'type': Type of example. Currently the only supported value is 'dataframe' - 'pandas_orient': Determines the json encoding for dataframe examples in terms of pandas orient convention. Defaults to 'split'. :param mlflow_model: Model metadata that will get updated with the example metadata. :param path: Where to store the example file. Should be model the model directory. """ example = _Example(input_example) example.save(path) mlflow_model.saved_input_example_info = example.info
def test_model_log(): with TempDir(chdr=True) as tmp: experiment_id = kiwi.create_experiment("test") sig = ModelSignature(inputs=Schema([ColSpec("integer", "x"), ColSpec("integer", "y")]), outputs=Schema([ColSpec(name=None, type="double")])) input_example = {"x": 1, "y": 2} with kiwi.start_run(experiment_id=experiment_id) as r: Model.log("some/path", TestFlavor, signature=sig, input_example=input_example) local_path = _download_artifact_from_uri("runs:/{}/some/path".format(r.info.run_id), output_path=tmp.path("")) loaded_model = Model.load(os.path.join(local_path, "MLmodel")) assert loaded_model.run_id == r.info.run_id assert loaded_model.artifact_path == "some/path" assert loaded_model.flavors == { "flavor1": {"a": 1, "b": 2}, "flavor2": {"x": 1, "y": 2}, } assert loaded_model.signature == sig path = os.path.join(local_path, loaded_model.saved_input_example_info["artifact_path"]) x = _dataframe_from_json(path) assert x.to_dict(orient="records")[0] == input_example
def _install_pyfunc_deps(model_path=None, install_mlflow=False): """ Creates a conda env for serving the model at the specified path and installs almost all serving dependencies into the environment - MLflow is not installed as it's not available via conda. """ # If model is a pyfunc model, create its conda env (even if it also has mleap flavor) has_env = False if model_path: model_config_path = os.path.join(model_path, MLMODEL_FILE_NAME) model = Model.load(model_config_path) # NOTE: this differs from _serve cause we always activate the env even if you're serving # an mleap model if pyfunc.FLAVOR_NAME not in model.flavors: return conf = model.flavors[pyfunc.FLAVOR_NAME] if pyfunc.ENV in conf: print("creating and activating custom environment") env = conf[pyfunc.ENV] env_path_dst = os.path.join("/opt/mlflow/", env) env_path_dst_dir = os.path.dirname(env_path_dst) if not os.path.exists(env_path_dst_dir): os.makedirs(env_path_dst_dir) shutil.copyfile(os.path.join(MODEL_PATH, env), env_path_dst) conda_create_model_env = "conda env create -n custom_env -f {}".format( env_path_dst) if Popen(["bash", "-c", conda_create_model_env]).wait() != 0: raise Exception("Failed to create model environment.") has_env = True activate_cmd = ["source /miniconda/bin/activate custom_env" ] if has_env else [] # NB: install gunicorn[gevent] from pip rather than from conda because gunicorn is already # dependency of mlflow on pip and we expect mlflow to be part of the environment. install_server_deps = ["pip install gunicorn[gevent]"] if Popen(["bash", "-c", " && ".join(activate_cmd + install_server_deps) ]).wait() != 0: raise Exception( "Failed to install serving dependencies into the model environment." ) if has_env and install_mlflow: install_mlflow_cmd = [ "pip install /opt/mlflow/." if _container_includes_mlflow_source() else "pip install mlflow=={}".format(MLFLOW_VERSION) ] if Popen([ "bash", "-c", " && ".join(activate_cmd + install_mlflow_cmd) ]).wait() != 0: raise Exception( "Failed to install mlflow into the model environment.")
def _load_pyfunc_conf_with_model(model_path): """ Loads the `python_function` flavor configuration for the specified model or throws an exception if the model does not contain the `python_function` flavor. :param model_path: The absolute path to the model. :return: The model's `python_function` flavor configuration and the model. """ model_path = os.path.abspath(model_path) model = Model.load(os.path.join(model_path, MLMODEL_FILE_NAME)) if pyfunc.FLAVOR_NAME not in model.flavors: raise MlflowException( message=("The specified model does not contain the `python_function` flavor. This " " flavor is required for model deployment required for model deployment."), error_code=INVALID_PARAMETER_VALUE) return model.flavors[pyfunc.FLAVOR_NAME], model