From 1d8dd1948f9d7eebdcd2a288b59839379871818f Mon Sep 17 00:00:00 2001 From: MING KANG Date: Wed, 29 Mar 2023 13:05:03 -0700 Subject: [PATCH 1/3] added attribute in predict() to allow for invoking predict in local --- ads/model/generic_model.py | 6 +++ .../with_extras/model/test_generic_model.py | 51 +++++++++++-------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/ads/model/generic_model.py b/ads/model/generic_model.py index 4e92b4130..8663c4201 100644 --- a/ads/model/generic_model.py +++ b/ads/model/generic_model.py @@ -2483,6 +2483,7 @@ def predict( self, data: Any = None, auto_serialize_data: bool = False, + local: bool = False, **kwargs, ) -> Dict[str, Any]: """Returns prediction of input data run against the model deployment endpoint. @@ -2507,6 +2508,8 @@ def predict( Whether to auto serialize input data. Defauls to `False` for GenericModel, and `True` for other frameworks. `data` required to be json serializable if `auto_serialize_data=False`. If `auto_serialize_data` set to True, data will be serialized before sending to model deployment endpoint. + local: bool. + Whether to invoke the prediction locally. Default to False. kwargs: content_type: str, used to indicate the media type of the resource. image: PIL.Image Object or uri for the image. @@ -2527,6 +2530,9 @@ def predict( ValueError If `data` is empty or not JSON serializable. """ + if local: + return self.verify(data=data, auto_serialize_data=auto_serialize_data, **kwargs) + if not self.model_deployment: raise ValueError("Use `deploy()` method to start model deployment.") diff --git a/tests/unitary/with_extras/model/test_generic_model.py b/tests/unitary/with_extras/model/test_generic_model.py index ac6e45233..0103be655 100644 --- a/tests/unitary/with_extras/model/test_generic_model.py +++ b/tests/unitary/with_extras/model/test_generic_model.py @@ -168,7 +168,20 @@ "training_script": None, } - +INFERENCE_CONDA_ENV= "oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1" +TRAINING_CONDA_ENV="oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1" +DEFAULT_PYTHON_VERSION = "3.8" +MODEL_FILE_NAME = "fake_model_name" + +def _prepare(model): + model.prepare( + inference_conda_env=INFERENCE_CONDA_ENV, + inference_python_version=DEFAULT_PYTHON_VERSION, + training_conda_env=TRAINING_CONDA_ENV, + training_python_version=DEFAULT_PYTHON_VERSION, + model_file_name=MODEL_FILE_NAME, + force_overwrite=True, + ) class TestEstimator: def predict(self, x): return x**2 @@ -300,14 +313,7 @@ def test_prepare_both_conda_env(self, mock_signer): @patch("ads.common.auth.default_signer") def test_verify_without_reload(self, mock_signer): """Test verify input data without reload artifacts.""" - self.generic_model.prepare( - inference_conda_env="oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1", - inference_python_version="3.6", - training_conda_env="oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1", - training_python_version="3.7", - model_file_name="fake_model_name", - force_overwrite=True, - ) + _prepare(self.generic_model) self.generic_model.verify(self.X_test.tolist()) with patch("ads.model.artifact.ModelArtifact.reload") as mock_reload: @@ -317,20 +323,10 @@ def test_verify_without_reload(self, mock_signer): @patch("ads.common.auth.default_signer") def test_verify(self, mock_signer): """Test verify input data""" - self.generic_model.prepare( - inference_conda_env="oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1", - inference_python_version="3.6", - training_conda_env="oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1", - training_python_version="3.7", - model_file_name="fake_model_name", - force_overwrite=True, - ) + _prepare(self.generic_model) prediction_1 = self.generic_model.verify(self.X_test.tolist()) assert isinstance(prediction_1, dict), "Failed to verify json payload." - prediction_2 = self.generic_model.verify(self.X_test.tolist()) - assert isinstance(prediction_2, dict), "Failed to verify input data." - def test_reload(self): """test the reload.""" pass @@ -622,6 +618,21 @@ def test_deploy_with_default_display_name(self, mock_deploy): == random_name[:-9] ) + @pytest.mark.parametrize( + "input_data", + [(X_test.tolist())] + ) + @patch("ads.common.auth.default_signer") + def test_predict_locally(self, mock_signer, input_data): + _prepare(self.generic_model) + test_result = self.generic_model.predict(data=input_data, local=True) + expected_result = self.generic_model.estimator.predict(input_data).tolist() + assert test_result['prediction'] == expected_result, "Failed to verify input data." + + with patch("ads.model.artifact.ModelArtifact.reload") as mock_reload: + self.generic_model.predict(data=input_data, local=True, reload_artifacts=False) + mock_reload.assert_not_called() + @patch.object(ModelDeployment, "predict") @patch("ads.common.auth.default_signer") @patch("ads.common.oci_client.OCIClientFactory") From d61245afb11319f4d1b57657b1208e2bc393f6db Mon Sep 17 00:00:00 2001 From: MING KANG Date: Wed, 5 Apr 2023 13:27:13 -0700 Subject: [PATCH 2/3] fixed by comments: improved error message --- ads/model/generic_model.py | 16 ++++-- .../with_extras/model/test_generic_model.py | 50 +++++++++++++------ 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/ads/model/generic_model.py b/ads/model/generic_model.py index 8663c4201..a03e23d27 100644 --- a/ads/model/generic_model.py +++ b/ads/model/generic_model.py @@ -2528,13 +2528,21 @@ def predict( NotActiveDeploymentError If model deployment process was not started or not finished yet. ValueError - If `data` is empty or not JSON serializable. + If model is not deployed yet or the endpoint information is not available. """ if local: - return self.verify(data=data, auto_serialize_data=auto_serialize_data, **kwargs) + return self.verify( + data=data, auto_serialize_data=auto_serialize_data, **kwargs + ) - if not self.model_deployment: - raise ValueError("Use `deploy()` method to start model deployment.") + if not (self.model_deployment and self.model_deployment.url): + raise ValueError( + "Error invoking the remote endpoint as the model is not " + "deployed yet or the endpoint information is not available. " + "Use `deploy()` method to start model deployment. " + "If you intend to invoke inference using locally available " + "model artifact, set parameter `local=True`" + ) current_state = self.model_deployment.state.name.upper() if current_state != ModelDeploymentState.ACTIVE.name: diff --git a/tests/unitary/with_extras/model/test_generic_model.py b/tests/unitary/with_extras/model/test_generic_model.py index 0103be655..00048a485 100644 --- a/tests/unitary/with_extras/model/test_generic_model.py +++ b/tests/unitary/with_extras/model/test_generic_model.py @@ -168,10 +168,12 @@ "training_script": None, } -INFERENCE_CONDA_ENV= "oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1" -TRAINING_CONDA_ENV="oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1" +INFERENCE_CONDA_ENV = "oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1" +TRAINING_CONDA_ENV = "oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1" DEFAULT_PYTHON_VERSION = "3.8" MODEL_FILE_NAME = "fake_model_name" +FAKE_MD_URL = "http://" + def _prepare(model): model.prepare( @@ -182,13 +184,14 @@ def _prepare(model): model_file_name=MODEL_FILE_NAME, force_overwrite=True, ) + + class TestEstimator: def predict(self, x): return x**2 class TestGenericModel: - iris = load_iris() X, y = iris.data, iris.target X_train, X_test, y_train, y_test = train_test_split(X, y) @@ -618,26 +621,31 @@ def test_deploy_with_default_display_name(self, mock_deploy): == random_name[:-9] ) - @pytest.mark.parametrize( - "input_data", - [(X_test.tolist())] - ) + @pytest.mark.parametrize("input_data", [(X_test.tolist())]) @patch("ads.common.auth.default_signer") def test_predict_locally(self, mock_signer, input_data): _prepare(self.generic_model) test_result = self.generic_model.predict(data=input_data, local=True) expected_result = self.generic_model.estimator.predict(input_data).tolist() - assert test_result['prediction'] == expected_result, "Failed to verify input data." + assert ( + test_result["prediction"] == expected_result + ), "Failed to verify input data." with patch("ads.model.artifact.ModelArtifact.reload") as mock_reload: - self.generic_model.predict(data=input_data, local=True, reload_artifacts=False) + self.generic_model.predict( + data=input_data, local=True, reload_artifacts=False + ) mock_reload.assert_not_called() @patch.object(ModelDeployment, "predict") @patch("ads.common.auth.default_signer") @patch("ads.common.oci_client.OCIClientFactory") + @patch( + "ads.model.deployment.model_deployment.ModelDeployment.url", + return_value=FAKE_MD_URL, + ) def test_predict_with_not_active_deployment_fail( - self, mock_client, mock_signer, mock_predict + self, mock_url, mock_client, mock_signer, mock_predict ): """Ensures predict model fails in case of model deployment is not in an active state.""" with pytest.raises(NotActiveDeploymentError): @@ -657,7 +665,11 @@ def test_predict_with_not_active_deployment_fail( @patch("ads.common.auth.default_signer") @patch("ads.common.oci_client.OCIClientFactory") - def test_predict_bytes_success(self, mock_client, mock_signer): + @patch( + "ads.model.deployment.model_deployment.ModelDeployment.url", + return_value=FAKE_MD_URL, + ) + def test_predict_bytes_success(self, mock_url, mock_client, mock_signer): """Ensures predict model passes with bytes input.""" with patch.object( ModelDeployment, "state", new_callable=PropertyMock @@ -666,7 +678,7 @@ def test_predict_bytes_success(self, mock_client, mock_signer): with patch.object(ModelDeployment, "predict") as mock_predict: mock_predict.return_value = {"result": "result"} self.generic_model.model_deployment = ModelDeployment( - model_deployment_id="test" + model_deployment_id="test", ) # self.generic_model.model_deployment.current_state = ModelDeploymentState.ACTIVE self.generic_model._as_onnx = False @@ -679,7 +691,11 @@ def test_predict_bytes_success(self, mock_client, mock_signer): @patch("ads.common.auth.default_signer") @patch("ads.common.oci_client.OCIClientFactory") - def test_predict_success(self, mock_client, mock_signer): + @patch( + "ads.model.deployment.model_deployment.ModelDeployment.url", + return_value=FAKE_MD_URL, + ) + def test_predict_success(self, mock_url, mock_client, mock_signer): """Ensures predict model passes with valid input parameters.""" with patch.object( ModelDeployment, "state", new_callable=PropertyMock @@ -796,7 +812,11 @@ def test_from_model_artifact( @patch("ads.common.auth.default_signer") @patch("ads.common.oci_client.OCIClientFactory") - def test_predict_success__serialize_input(self, mock_client, mock_signer): + @patch( + "ads.model.deployment.model_deployment.ModelDeployment.url", + return_value=FAKE_MD_URL, + ) + def test_predict_success__serialize_input(self, mock_url, mock_client, mock_signer): """Ensures predict model passes with valid input parameters.""" df = pd.DataFrame([1, 2, 3]) @@ -806,7 +826,6 @@ def test_predict_success__serialize_input(self, mock_client, mock_signer): with patch.object( GenericModel, "get_data_serializer" ) as mock_get_data_serializer: - mock_get_data_serializer.return_value.data = df.to_json() mock_state.return_value = ModelDeploymentState.ACTIVE with patch.object(ModelDeployment, "predict") as mock_predict: @@ -1793,7 +1812,6 @@ def test_upload_artifact_fail(self): def test_upload_artifact_success(self): """Tests uploading model artifacts to the provided `uri`.""" with tempfile.TemporaryDirectory() as tmp_dir: - # copy test artifacts to the temp folder shutil.copytree( os.path.join(self.curr_dir, "test_files/valid_model_artifacts"), From 4206cc6c0e597c049b934f5e4578af7f8ad81a04 Mon Sep 17 00:00:00 2001 From: MING KANG Date: Wed, 5 Apr 2023 13:32:27 -0700 Subject: [PATCH 3/3] removed real paths --- tests/unitary/with_extras/model/test_generic_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unitary/with_extras/model/test_generic_model.py b/tests/unitary/with_extras/model/test_generic_model.py index 00048a485..568f01c50 100644 --- a/tests/unitary/with_extras/model/test_generic_model.py +++ b/tests/unitary/with_extras/model/test_generic_model.py @@ -168,8 +168,8 @@ "training_script": None, } -INFERENCE_CONDA_ENV = "oci://service-conda-packs@ociodscdev/service_pack/cpu/General_Machine_Learning_for_CPUs/1.0/mlcpuv1" -TRAINING_CONDA_ENV = "oci://service-conda-packs@ociodscdev/service_pack/cpu/Oracle_Database_for_CPU_Python_3.7/1.0/database_p37_cpu_v1" +INFERENCE_CONDA_ENV = "oci://bucket@namespace/" +TRAINING_CONDA_ENV = "oci://bucket@namespace/" DEFAULT_PYTHON_VERSION = "3.8" MODEL_FILE_NAME = "fake_model_name" FAKE_MD_URL = "http://"