diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index b23107003cc..7c954e38c86 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -37,23 +37,20 @@ jobs: python -m pip install --upgrade pip - name: Test with pytest run: | - pip install pytest - pip install pytest-cov - pip install --upgrade pytest-tornasync cd python - pip install -e ./kserve - pytest ./kserve + pip install -e ./kserve[test] + pytest --cov=kserve ./kserve pip install -e ./sklearnserver - pytest ./sklearnserver + pytest --cov=sklearnserver ./sklearnserver pip install -e ./xgbserver - pytest ./xgbserver + pytest --cov=xgbserver ./xgbserver pip install -e ./pytorchserver - pytest ./pytorchserver + pytest --cov=pytorchserver ./pytorchserver pip install -e ./pmmlserver - pytest ./pmmlserver + pytest --cov=pmmlserver ./pmmlserver pip install -e ./lgbserver - pytest ./lgbserver + pytest --cov=lgbserver ./lgbserver pip install -e ./paddleserver[test] - pytest ./paddleserver + pytest --cov=paddleserver ./paddleserver pip install -e ./alibiexplainer - pytest ./alibiexplainer + pytest --cov=alibiexplainer ./alibiexplainer diff --git a/config/runtimes/kserve-sklearnserver.yaml b/config/runtimes/kserve-sklearnserver.yaml index bd7a215f51b..249cfa37a2a 100644 --- a/config/runtimes/kserve-sklearnserver.yaml +++ b/config/runtimes/kserve-sklearnserver.yaml @@ -5,7 +5,7 @@ metadata: spec: supportedModelFormats: - name: sklearn - version: "0" + version: "1" autoSelect: true containers: - name: kserve-container diff --git a/config/runtimes/kserve-xgbserver.yaml b/config/runtimes/kserve-xgbserver.yaml index bedc11e4310..114eafdf625 100644 --- a/config/runtimes/kserve-xgbserver.yaml +++ b/config/runtimes/kserve-xgbserver.yaml @@ -5,7 +5,7 @@ metadata: spec: supportedModelFormats: - name: xgboost - version: "0" + version: "1" autoSelect: true containers: - name: kserve-container diff --git a/docs/samples/explanation/alibi/income/README.md b/docs/samples/explanation/alibi/income/README.md index 664fe902fb9..4d079fb10bc 100644 --- a/docs/samples/explanation/alibi/income/README.md +++ b/docs/samples/explanation/alibi/income/README.md @@ -1,7 +1,5 @@ # Example Anchors Tabular Explaination for Income Prediction -For users of KFServing v0.3.0 please follow [the README and notebook for v0.3.0 branch](https://github.com/kubeflow/kfserving/tree/v0.3.0/docs/samples/explanation/alibi/income). - This example uses a [US income dataset](https://archive.ics.uci.edu/ml/datasets/adult) You can also try out the [Jupyter notebook](income_explanations.ipynb). @@ -11,7 +9,7 @@ We can create a InferenceService with a trained sklearn predictor for this datas The InferenceService is shown below: ```yaml -apiVersion: "serving.kserve.io/v1alpha2" +apiVersion: "serving.kserve.io/v1beta1" kind: "InferenceService" metadata: name: "income" @@ -30,15 +28,13 @@ spec: minReplicas: 1 alibi: type: AnchorTabular - storageUri: "gs://seldon-models/sklearn/income/explainer-py36-0.5.2" + storageUri: "gs://seldon-models/sklearn/income/explainer-py37-0.6.0" resources: requests: cpu: 0.1 limits: cpu: 1 ``` -For KFS 0.4 the explainer storageUri is `gs://seldon-models/sklearn/income/alibi/0.4.0` - Create this InferenceService: ``` diff --git a/docs/samples/explanation/alibi/income/train.py b/docs/samples/explanation/alibi/income/train.py index 1f95079dc96..7b14e14ce80 100644 --- a/docs/samples/explanation/alibi/income/train.py +++ b/docs/samples/explanation/alibi/income/train.py @@ -16,24 +16,28 @@ from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder -from alibi.datasets import adult +from alibi.datasets import fetch_adult import joblib import dill from sklearn.pipeline import Pipeline import alibi # load data -data, labels, feature_names, category_map = adult() +adult = fetch_adult() +data = adult.data +targets = adult.target +feature_names = adult.feature_names +category_map = adult.category_map # define train and test set np.random.seed(0) -data_perm = np.random.permutation(np.c_[data, labels]) +data_perm = np.random.permutation(np.c_[data, targets]) data = data_perm[:, :-1] labels = data_perm[:, -1] idx = 30000 -X_train, Y_train = data[:idx, :], labels[:idx] -X_test, Y_test = data[idx + 1:, :], labels[idx + 1:] +X_train, Y_train = data[:idx, :], targets[:idx] +X_test, Y_test = data[idx + 1:, :], targets[idx + 1:] # feature transformation pipeline ordinal_features = [x for x in range(len(feature_names)) if x not in list(category_map.keys())] @@ -56,7 +60,7 @@ pipeline.fit(X_train, Y_train) print("Creating an explainer") -explainer = alibi.explainers.AnchorTabular(predict_fn=lambda x: clf.predict(preprocessor.transform(x)), +explainer = alibi.explainers.AnchorTabular(predictor=lambda x: clf.predict(preprocessor.transform(x)), feature_names=feature_names, categorical_names=category_map) explainer.fit(X_train) diff --git a/docs/samples/explanation/alibi/moviesentiment/README.md b/docs/samples/explanation/alibi/moviesentiment/README.md index e4550f21f27..d37534c4554 100644 --- a/docs/samples/explanation/alibi/moviesentiment/README.md +++ b/docs/samples/explanation/alibi/moviesentiment/README.md @@ -9,7 +9,7 @@ We can create a InferenceService with a trained sklearn predictor for this datas The InferenceService is shown below: ``` -apiVersion: "serving.kserve.io/v1alpha2" +apiVersion: "serving.kserve.io/v1beta1" kind: "InferenceService" metadata: name: "moviesentiment" diff --git a/docs/samples/explanation/alibi/moviesentiment/train.py b/docs/samples/explanation/alibi/moviesentiment/train.py index aaf8c4f4b92..801652f8f92 100644 --- a/docs/samples/explanation/alibi/moviesentiment/train.py +++ b/docs/samples/explanation/alibi/moviesentiment/train.py @@ -48,7 +48,7 @@ spacy_language_model = 'en_core_web_md' spacy_model(model=spacy_language_model) nlp = spacy.load(spacy_language_model) -anchors_text = AnchorText(nlp, lambda x: pipeline.predict(x)) +anchors_text = AnchorText(nlp=nlp, predictor=lambda x: pipeline.predict(x)) # Test explanations locally expl = anchors_text.explain("the actors are very bad") diff --git a/python/alibiexplainer/setup.py b/python/alibiexplainer/setup.py index 357d225ffee..ed328e6bc8c 100644 --- a/python/alibiexplainer/setup.py +++ b/python/alibiexplainer/setup.py @@ -32,21 +32,12 @@ python_requires='>=3.6', packages=find_packages("alibiexplainer"), install_requires=[ - "tensorflow==2.3.2", "kserve>=0.7.0", - "pandas>=0.24.2", "nest_asyncio>=1.4.0", - "alibi==0.6.0", - "scikit-learn == 0.20.3", - "argparse>=1.4.0", - "requests>=2.22.0", + "alibi==0.6.2", "joblib>=0.13.2", - "dill>=0.3.0", - "grpcio>=1.22.0", - "xgboost==1.0.2", + "xgboost==1.5.0", "shap==0.39.0", - "numpy<1.19.0", - 'spacy[lookups]>=2.0.0, <4.0.0' ], tests_require=tests_require, extras_require={'test': tests_require} diff --git a/python/alibiexplainer/tests/test_anchor_tabular.py b/python/alibiexplainer/tests/test_anchor_tabular.py index ebd748f52f4..a779f299373 100644 --- a/python/alibiexplainer/tests/test_anchor_tabular.py +++ b/python/alibiexplainer/tests/test_anchor_tabular.py @@ -22,8 +22,8 @@ import json from .utils import Predictor -ADULT_EXPLAINER_URI = "gs://seldon-models/sklearn/income/explainer-py37-0.6.0" -ADULT_MODEL_URI = "gs://seldon-models/sklearn/income/model" +ADULT_EXPLAINER_URI = "gs://kfserving-examples/models/sklearn/1.0/income/explainer-py37-0.6.2" +ADULT_MODEL_URI = "gs://kfserving-examples/models/sklearn/1.0/income/model" EXPLAINER_FILENAME = "explainer.dill" @@ -43,5 +43,5 @@ def test_anchor_tabular(): np.random.seed(0) explanation = anchor_tabular.explain(X_test[0:1].tolist()) exp_json = json.loads(explanation.to_json()) - assert exp_json["data"]["anchor"][0] == "Marital Status = Never-Married" or \ + assert exp_json["data"]["anchor"][0] == "Relationship = Own-child" or \ exp_json["data"]["anchor"][0] == "Age <= 28.00" diff --git a/python/alibiexplainer/tests/test_anchor_text.py b/python/alibiexplainer/tests/test_anchor_text.py index 6b55a1aa224..b5b7aedf58b 100644 --- a/python/alibiexplainer/tests/test_anchor_text.py +++ b/python/alibiexplainer/tests/test_anchor_text.py @@ -20,12 +20,12 @@ import json import numpy as np -MOVIE_MODEL_URI = "gs://seldon-models/sklearn/moviesentiment" +MOVIE_MODEL_URI = "gs://kfserving-examples/models/sklearn/1.0/moviesentiment/model" def test_anchor_text(): os.environ.clear() - skmodel = SKLearnModel("adult", MOVIE_MODEL_URI) + skmodel = SKLearnModel("movie", MOVIE_MODEL_URI) skmodel.load() predictor = Predictor(skmodel) anchor_text = AnchorText(predictor.predict_fn, None) diff --git a/python/kserve/kserve/handlers/http.py b/python/kserve/kserve/handlers/http.py index 80d321b0c28..cb97f519333 100644 --- a/python/kserve/kserve/handlers/http.py +++ b/python/kserve/kserve/handlers/http.py @@ -37,6 +37,8 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: if exc_info is not None: if hasattr(exc_info[1], "reason"): reason = exc_info[1].reason + else: + reason = str(exc_info[1]) self.write({"error": reason}) diff --git a/python/kserve/kserve/kfmodel.py b/python/kserve/kserve/kfmodel.py index c6aadb59000..2974df0f274 100644 --- a/python/kserve/kserve/kfmodel.py +++ b/python/kserve/kserve/kfmodel.py @@ -44,6 +44,22 @@ class PredictorProtocol(Enum): GRPC_V2 = "grpc-v2" +class ModelMissingError(Exception): + def __init__(self, path): + self.path = path + + def __str__(self): + return self.path + + +class InferenceError(RuntimeError): + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return self.reason + + # KFModel is intended to be subclassed by various components within KFServing. class KFModel: diff --git a/python/kserve/kserve/kfmodel_repository.py b/python/kserve/kserve/kfmodel_repository.py index d7844afb249..00f85b6f9ec 100644 --- a/python/kserve/kserve/kfmodel_repository.py +++ b/python/kserve/kserve/kfmodel_repository.py @@ -15,6 +15,7 @@ from typing import Dict, Optional, Union from kserve import KFModel from ray.serve.api import RayServeHandle +import os MODEL_MOUNT_DIRS = "/mnt/models" @@ -29,6 +30,12 @@ def __init__(self, models_dir: str = MODEL_MOUNT_DIRS): self.models = {} self.models_dir = models_dir + def load_models(self): + for name in os.listdir(self.models_dir): + d = os.path.join(self.models_dir, name) + if os.path.isdir(d): + self.load_model(name) + def set_models_dir(self, models_dir): # used for unit tests self.models_dir = models_dir @@ -45,7 +52,7 @@ def is_model_ready(self, name: str): if isinstance(model, KFModel): return False if model is None else model.ready else: - # For Ray Serve, the models are guaranteed to be ready after the deploy call. + # For Ray Serve, the models are guaranteed to be ready after deploying the model. return True def update(self, model: KFModel): @@ -57,6 +64,9 @@ def update_handle(self, name: str, model_handle: RayServeHandle): def load(self, name: str) -> bool: pass + def load_model(self, name: str) -> bool: + pass + def unload(self, name: str): if name in self.models: del self.models[name] diff --git a/python/kserve/kserve/kfserver.py b/python/kserve/kserve/kfserver.py index 2288322d57d..430f0c68ef3 100644 --- a/python/kserve/kserve/kfserver.py +++ b/python/kserve/kserve/kfserver.py @@ -46,6 +46,7 @@ help='The number of works to fork') parser.add_argument('--max_asyncio_workers', default=None, type=int, help='Max number of asyncio workers to spawn') + args, _ = parser.parse_known_args() tornado.log.enable_pretty_logging() diff --git a/python/kserve/requirements.txt b/python/kserve/requirements.txt index 5bc26f5c1b0..70ff6de03c8 100644 --- a/python/kserve/requirements.txt +++ b/python/kserve/requirements.txt @@ -10,7 +10,7 @@ minio>=4.0.9,<7.0.0 google-cloud-storage==1.41.1 adal>=1.2.2 table_logger>=0.3.5 -numpy>=1.17.3 +numpy~=1.19.2 azure-storage-blob==12.8.1 azure-identity>=1.6.0 cloudevents>=1.2.0 @@ -19,7 +19,7 @@ boto3==1.18.18 botocore==1.21.18 psutil>=5.0 ray[serve]==1.9.0 -grpcio==1.38.1 +grpcio>=1.34.0 google_api_core==1.29.0 jmespath==0.10.0 googleapis_common_protos==1.53.0 diff --git a/python/kserve/setup.py b/python/kserve/setup.py index 89e1eaaa2f2..2bbf4b0d537 100644 --- a/python/kserve/setup.py +++ b/python/kserve/setup.py @@ -16,6 +16,8 @@ TESTS_REQUIRES = [ 'pytest', + 'pytest-cov', + 'pytest-asyncio', 'pytest-tornasync', 'mypy' ] diff --git a/python/lgbserver/lgbserver/__main__.py b/python/lgbserver/lgbserver/__main__.py index 6e5201d1e85..0fccda8636f 100644 --- a/python/lgbserver/lgbserver/__main__.py +++ b/python/lgbserver/lgbserver/__main__.py @@ -14,10 +14,9 @@ import argparse import logging -import sys import kserve - +from kserve.kfmodel import ModelMissingError from lgbserver.lightgbm_model_repository import LightGBMModelRepository from lgbserver.model import LightGBMModel @@ -39,10 +38,9 @@ model = LightGBMModel(args.model_name, args.model_dir, args.nthread) try: model.load() - except Exception: - ex_type, ex_value = sys.exc_info()[:2] - logging.error(f"fail to load model {args.model_name} from dir {args.model_dir}. " - f"exception type {ex_type}, exception msg: {ex_value}") + except ModelMissingError: + logging.error(f"fail to load model {args.model_name} from dir {args.model_dir}," + f"trying to load from model repository.") model_repository = LightGBMModelRepository(args.model_dir, args.nthread) # LightGBM doesn't support multi-process, so the number of http server workers should be 1. kfserver = kserve.KFServer(workers=1, registered_models=model_repository) # pylint:disable=c-extension-no-member diff --git a/python/lgbserver/lgbserver/lightgbm_model_repository.py b/python/lgbserver/lgbserver/lightgbm_model_repository.py index 544a0cd7fe9..181d78935e0 100644 --- a/python/lgbserver/lgbserver/lightgbm_model_repository.py +++ b/python/lgbserver/lgbserver/lightgbm_model_repository.py @@ -21,8 +21,12 @@ class LightGBMModelRepository(KFModelRepository): def __init__(self, model_dir: str = MODEL_MOUNT_DIRS, nthread: int = 1): super().__init__(model_dir) self.nthread = nthread + self.load_models() async def load(self, name: str) -> bool: + return self.load_model(name) + + def load_model(self, name: str) -> bool: model = LightGBMModel(name, os.path.join(self.models_dir, name), self.nthread) if model.load(): self.update(model) diff --git a/python/lgbserver/lgbserver/model.py b/python/lgbserver/lgbserver/model.py index 9f13785dd42..ce46758fcbd 100644 --- a/python/lgbserver/lgbserver/model.py +++ b/python/lgbserver/lgbserver/model.py @@ -18,6 +18,8 @@ import os from typing import Dict import pandas as pd +from kserve.kfmodel import ModelMissingError, InferenceError + BOOSTER_FILE = "model.bst" @@ -36,6 +38,8 @@ def __init__(self, name: str, model_dir: str, nthread: int, def load(self) -> bool: model_file = os.path.join( kserve.Storage.download(self.model_dir), BOOSTER_FILE) + if not os.path.exists(model_file): + raise ModelMissingError(model_file) self._booster = lgb.Booster(params={"nthread": self.nthread}, model_file=model_file) self.ready = True @@ -51,4 +55,4 @@ def predict(self, request: Dict) -> Dict: result = self._booster.predict(inputs) return {"predictions": result.tolist()} except Exception as e: - raise Exception("Failed to predict %s" % e) + raise InferenceError(str(e)) diff --git a/python/lgbserver/lgbserver/test_lightgbm_model_repository.py b/python/lgbserver/lgbserver/test_lightgbm_model_repository.py index 4114ba702b8..9e5d782a251 100644 --- a/python/lgbserver/lgbserver/test_lightgbm_model_repository.py +++ b/python/lgbserver/lgbserver/test_lightgbm_model_repository.py @@ -31,9 +31,8 @@ async def test_load(): @pytest.mark.asyncio async def test_load_fail(): - repo = LightGBMModelRepository(model_dir=model_dir, nthread=1) - model_name = "model" with pytest.raises(Exception): - await repo.load(model_name) + repo = LightGBMModelRepository(model_dir=invalid_model_dir, nthread=1) + model_name = "model" assert repo.get_model(model_name) is None assert not repo.is_model_ready(model_name) diff --git a/python/sklearnserver/setup.py b/python/sklearnserver/setup.py index eb665a80a8d..82ca6ba0d38 100644 --- a/python/sklearnserver/setup.py +++ b/python/sklearnserver/setup.py @@ -33,7 +33,7 @@ packages=find_packages("sklearnserver"), install_requires=[ "kserve>=0.7.0", - "scikit-learn == 0.20.3", + "scikit-learn == 1.0.1", "joblib >= 0.13.0" ], tests_require=tests_require, diff --git a/python/sklearnserver/sklearnserver/__main__.py b/python/sklearnserver/sklearnserver/__main__.py index 2b5b7101c20..a4ca58e0351 100644 --- a/python/sklearnserver/sklearnserver/__main__.py +++ b/python/sklearnserver/sklearnserver/__main__.py @@ -14,10 +14,10 @@ import argparse import logging -import sys import kserve from sklearnserver import SKLearnModel, SKLearnModelRepository +from kserve.kfmodel import ModelMissingError DEFAULT_MODEL_NAME = "model" DEFAULT_LOCAL_MODEL_DIR = "/tmp/model" @@ -33,9 +33,8 @@ model = SKLearnModel(args.model_name, args.model_dir) try: model.load() - except Exception: - exc_info = sys.exc_info() - logging.error(f"fail to load model {args.model_name} from dir {args.model_dir}. " - f"exception type {exc_info[0]}, exception msg: {exc_info[1]}") - model.ready = False + except ModelMissingError: + logging.error(f"fail to locate model file for model {args.model_name} under dir {args.model_dir}," + f"trying loading from model repository.") + kserve.KFServer(registered_models=SKLearnModelRepository(args.model_dir)).start([model] if model.ready else []) diff --git a/python/sklearnserver/sklearnserver/example_models/joblib/model/model.joblib b/python/sklearnserver/sklearnserver/example_models/joblib/model/model.joblib index 0abb6062355..ae8171b5bb6 100644 Binary files a/python/sklearnserver/sklearnserver/example_models/joblib/model/model.joblib and b/python/sklearnserver/sklearnserver/example_models/joblib/model/model.joblib differ diff --git a/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model1/model.pickle b/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model1/model.pickle new file mode 100644 index 00000000000..7ab09abab0d Binary files /dev/null and b/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model1/model.pickle differ diff --git a/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model2/model.pkl b/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model2/model.pkl new file mode 100644 index 00000000000..7ab09abab0d Binary files /dev/null and b/python/sklearnserver/sklearnserver/example_models/multi/model_repository/model2/model.pkl differ diff --git a/python/sklearnserver/sklearnserver/example_models/pickle/model/model.pickle b/python/sklearnserver/sklearnserver/example_models/pickle/model/model.pickle index 6b3b7c2a213..c5c391e5ac1 100644 Binary files a/python/sklearnserver/sklearnserver/example_models/pickle/model/model.pickle and b/python/sklearnserver/sklearnserver/example_models/pickle/model/model.pickle differ diff --git a/python/sklearnserver/sklearnserver/example_models/pkl/model/model.pkl b/python/sklearnserver/sklearnserver/example_models/pkl/model/model.pkl index 6b3b7c2a213..546d09ae4b4 100644 Binary files a/python/sklearnserver/sklearnserver/example_models/pkl/model/model.pkl and b/python/sklearnserver/sklearnserver/example_models/pkl/model/model.pkl differ diff --git a/python/sklearnserver/sklearnserver/model.py b/python/sklearnserver/sklearnserver/model.py index 2e698e09cc4..95015bada05 100644 --- a/python/sklearnserver/sklearnserver/model.py +++ b/python/sklearnserver/sklearnserver/model.py @@ -11,15 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os import kserve import joblib import numpy as np import pathlib from typing import Dict +from kserve.kfmodel import ModelMissingError, InferenceError MODEL_BASENAME = "model" MODEL_EXTENSIONS = [".joblib", ".pkl", ".pickle"] +ENV_PREDICT_PROBA = "PREDICT_PROBA" class SKLearnModel(kserve.KFModel): # pylint:disable=c-extension-no-member @@ -34,7 +37,7 @@ def load(self) -> bool: paths = [model_path / (MODEL_BASENAME + model_extension) for model_extension in MODEL_EXTENSIONS] existing_paths = [path for path in paths if path.exists()] if len(existing_paths) == 0: - raise RuntimeError('Missing Model File.') + raise ModelMissingError(model_path) elif len(existing_paths) > 1: raise RuntimeError('More than one model file is detected, ' f'Only one is allowed within model_dir: {existing_paths}') @@ -47,10 +50,14 @@ def predict(self, request: Dict) -> Dict: try: inputs = np.array(instances) except Exception as e: - raise Exception( - "Failed to initialize NumPy array from inputs: %s, %s" % (e, instances)) + raise ValueError( + f"Failed to initialize NumPy array from inputs: {e}") try: - result = self._model.predict(inputs).tolist() + if os.environ.get(ENV_PREDICT_PROBA, "false").lower() == "true" and \ + hasattr(self._model, "predict_proba"): + result = self._model.predict_proba(inputs).tolist() + else: + result = self._model.predict(inputs).tolist() return {"predictions": result} except Exception as e: - raise Exception("Failed to predict %s" % e) + raise InferenceError(str(e)) diff --git a/python/sklearnserver/sklearnserver/sklearn_model_repository.py b/python/sklearnserver/sklearnserver/sklearn_model_repository.py index 617b1c608aa..b5117928740 100644 --- a/python/sklearnserver/sklearnserver/sklearn_model_repository.py +++ b/python/sklearnserver/sklearnserver/sklearn_model_repository.py @@ -21,8 +21,12 @@ class SKLearnModelRepository(KFModelRepository): def __init__(self, model_dir: str = MODEL_MOUNT_DIRS): super().__init__(model_dir) + self.load_models() async def load(self, name: str) -> bool: + return self.load_model(name) + + def load_model(self, name: str) -> bool: model = SKLearnModel(name, os.path.join(self.models_dir, name)) if model.load(): self.update(model) diff --git a/python/sklearnserver/sklearnserver/test_model.py b/python/sklearnserver/sklearnserver/test_model.py index 5751afaba69..782ad173d3e 100644 --- a/python/sklearnserver/sklearnserver/test_model.py +++ b/python/sklearnserver/sklearnserver/test_model.py @@ -18,6 +18,7 @@ import joblib import pickle import os +from kserve.kfmodel import ModelMissingError import pytest @@ -31,7 +32,7 @@ def _train_sample_model(): iris = datasets.load_iris() X, y = iris.data, iris.target - sklearn_model = svm.SVC(gamma='scale') + sklearn_model = svm.SVC(gamma='scale', probability=True) sklearn_model.fit(X, y) return sklearn_model, X @@ -65,9 +66,15 @@ def test_model_pickle(): def test_dir_with_no_model(): model = SKLearnModel("model", _MODEL_DIR) - with pytest.raises(RuntimeError) as e: + with pytest.raises(ModelMissingError): + model.load() + + +def test_dir_with_incompatible_model(): + model = SKLearnModel("model", _MODEL_DIR + "/pkl") + with pytest.raises(ModuleNotFoundError) as e: model.load() - assert 'Missing Model File' in str(e.value) + assert 'No module named' in str(e.value) def test_dir_with_two_models(): diff --git a/python/sklearnserver/sklearnserver/test_sklearn_model_repository.py b/python/sklearnserver/sklearnserver/test_sklearn_model_repository.py index 7be675de316..a7984ab2fb8 100644 --- a/python/sklearnserver/sklearnserver/test_sklearn_model_repository.py +++ b/python/sklearnserver/sklearnserver/test_sklearn_model_repository.py @@ -41,11 +41,18 @@ async def test_load_joblib(): assert repo.is_model_ready(model_name) +@pytest.mark.asyncio +async def test_load_multiple(): + repo = SKLearnModelRepository(_MODEL_DIR + "/multi/model_repository") + for model in ["model1", "model2"]: + assert repo.get_model(model) is not None + assert repo.is_model_ready(model) + + @pytest.mark.asyncio async def test_load_fail(): - repo = SKLearnModelRepository(INVALID_MODEL_DIR) - model_name = "model" - with pytest.raises(Exception): - await repo.load(model_name) - assert repo.get_model(model_name) is None - assert not repo.is_model_ready(model_name) + with pytest.raises(FileNotFoundError): + repo = SKLearnModelRepository(INVALID_MODEL_DIR) + model_name = "model" + assert repo.get_model(model_name) is None + assert not repo.is_model_ready(model_name) diff --git a/python/xgbserver/setup.py b/python/xgbserver/setup.py index 7d3b8c9d6ee..b21e446cd2e 100644 --- a/python/xgbserver/setup.py +++ b/python/xgbserver/setup.py @@ -34,8 +34,8 @@ packages=find_packages("xgbserver"), install_requires=[ "kserve>=0.7.0", - "xgboost == 0.82", - "scikit-learn == 0.20.3", + "xgboost == 1.5.0", + "scikit-learn == 1.0.1", ], tests_require=tests_require, extras_require={'test': tests_require} diff --git a/python/xgbserver/xgbserver/__main__.py b/python/xgbserver/xgbserver/__main__.py index fa3dbcf1e96..afebf99fed3 100644 --- a/python/xgbserver/xgbserver/__main__.py +++ b/python/xgbserver/xgbserver/__main__.py @@ -14,8 +14,8 @@ import argparse import logging -import sys import kserve +from kserve.kfmodel import ModelMissingError from xgbserver import XGBoostModel, XGBoostModelRepository @@ -37,11 +37,9 @@ model = XGBoostModel(args.model_name, args.model_dir, args.nthread) try: model.load() - except Exception: - ex_type, ex_value, _ = sys.exc_info() - logging.error(f"fail to load model {args.model_name} from dir {args.model_dir}. " - f"exception type {ex_type}, exception msg: {ex_value}") - model.ready = False + except ModelMissingError: + logging.error(f"fail to locate model file for model {args.model_name} under dir {args.model_dir}," + f"trying loading from model repository.") kserve.KFServer(registered_models=XGBoostModelRepository(args.model_dir, args.nthread))\ - .start([model] if model.ready else []) # pylint:disable=c-extension-no-member + .start([model] if model.ready else []) diff --git a/python/xgbserver/xgbserver/example_model/model/model.bst b/python/xgbserver/xgbserver/example_model/model/model.bst index cebe9408374..a81a42100d4 100644 Binary files a/python/xgbserver/xgbserver/example_model/model/model.bst and b/python/xgbserver/xgbserver/example_model/model/model.bst differ diff --git a/python/xgbserver/xgbserver/model.py b/python/xgbserver/xgbserver/model.py index e1ff90a26cf..7a4f379bc57 100644 --- a/python/xgbserver/xgbserver/model.py +++ b/python/xgbserver/xgbserver/model.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import kserve +from kserve import KFModel, Storage +from kserve.kfmodel import ModelMissingError, InferenceError import xgboost as xgb import numpy as np from xgboost import XGBModel @@ -22,7 +23,7 @@ BOOSTER_FILE = "model.bst" -class XGBoostModel(kserve.KFModel): +class XGBoostModel(KFModel): def __init__(self, name: str, model_dir: str, nthread: int, booster: XGBModel = None): super().__init__(name) @@ -35,7 +36,9 @@ def __init__(self, name: str, model_dir: str, nthread: int, def load(self) -> bool: model_file = os.path.join( - kserve.Storage.download(self.model_dir), BOOSTER_FILE) + Storage.download(self.model_dir), BOOSTER_FILE) + if not os.path.exists(model_file): + raise ModelMissingError(model_file) self._booster = xgb.Booster(params={"nthread": self.nthread}, model_file=model_file) self.ready = True @@ -48,4 +51,4 @@ def predict(self, request: Dict) -> Dict: result: xgb.DMatrix = self._booster.predict(dmatrix) return {"predictions": result.tolist()} except Exception as e: - raise Exception("Failed to predict %s" % e) + raise InferenceError(str(e)) diff --git a/python/xgbserver/xgbserver/test_xgboost_model_repository.py b/python/xgbserver/xgbserver/test_xgboost_model_repository.py index 919fa9c088a..bc2d93b946c 100644 --- a/python/xgbserver/xgbserver/test_xgboost_model_repository.py +++ b/python/xgbserver/xgbserver/test_xgboost_model_repository.py @@ -31,9 +31,8 @@ async def test_load(): @pytest.mark.asyncio async def test_load_fail(): - repo = XGBoostModelRepository(model_dir=model_dir, nthread=1) - model_name = "model" with pytest.raises(Exception): - await repo.load(model_name) + repo = XGBoostModelRepository(model_dir=invalid_model_dir, nthread=1) + model_name = "model" assert repo.get_model(model_name) is None assert not repo.is_model_ready(model_name) diff --git a/python/xgbserver/xgbserver/xgboost_model_repository.py b/python/xgbserver/xgbserver/xgboost_model_repository.py index 9983f66701b..538440def06 100644 --- a/python/xgbserver/xgbserver/xgboost_model_repository.py +++ b/python/xgbserver/xgbserver/xgboost_model_repository.py @@ -21,8 +21,12 @@ class XGBoostModelRepository(KFModelRepository): def __init__(self, model_dir: str = MODEL_MOUNT_DIRS, nthread: int = 1): super().__init__(model_dir) self.nthread = nthread + self.load_models() - async def load(self, name: str, ) -> bool: + async def load(self, name: str) -> bool: + return self.load_model(name) + + def load_model(self, name: str) -> bool: model = XGBoostModel(name, os.path.join(self.models_dir, name), self.nthread) if model.load(): self.update(model) diff --git a/test/e2e/explainer/test_tabular_explainer.py b/test/e2e/explainer/test_tabular_explainer.py index d80fa2d686e..267b39def77 100644 --- a/test/e2e/explainer/test_tabular_explainer.py +++ b/test/e2e/explainer/test_tabular_explainer.py @@ -37,7 +37,7 @@ def test_tabular_explainer(): service_name = 'isvc-explainer-tabular' predictor = V1beta1PredictorSpec( sklearn=V1beta1SKLearnSpec( - storage_uri='gs://seldon-models/sklearn/income/model', + storage_uri='gs://kfserving-examples/models/sklearn/1.0/income/model', resources=V1ResourceRequirements( requests={'cpu': '100m', 'memory': '1Gi'}, limits={'cpu': '100m', 'memory': '1Gi'} @@ -49,7 +49,7 @@ def test_tabular_explainer(): alibi=V1beta1AlibiExplainerSpec( name='kserve-container', type='AnchorTabular', - storage_uri='gs://seldon-models/sklearn/income/explainer-py37-0.6.0', + storage_uri='gs://kfserving-examples/models/sklearn/1.0/income/explainer-py37-0.6.2', resources=V1ResourceRequirements( requests={'cpu': '100m', 'memory': '1Gi'}, limits={'cpu': '100m', 'memory': '1Gi'} diff --git a/test/e2e/logger/test_logger.py b/test/e2e/logger/test_logger.py index 12ab8a2fa9c..3c8038b89b6 100644 --- a/test/e2e/logger/test_logger.py +++ b/test/e2e/logger/test_logger.py @@ -55,7 +55,7 @@ def test_kserve_logger(): url="http://message-dumper."+KSERVE_TEST_NAMESPACE+".svc.cluster.local" ), sklearn=V1beta1SKLearnSpec( - storage_uri='gs://kfserving-samples/models/sklearn/iris', + storage_uri='gs://kfserving-examples/models/sklearn/1.0/model', resources=V1ResourceRequirements( requests={'cpu': '100m', 'memory': '256Mi'}, limits={'cpu': '100m', 'memory': '256Mi'} diff --git a/test/e2e/predictor/test_multi_model_serving.py b/test/e2e/predictor/test_multi_model_serving.py index a7387e18b76..5630f89cdcd 100644 --- a/test/e2e/predictor/test_multi_model_serving.py +++ b/test/e2e/predictor/test_multi_model_serving.py @@ -39,7 +39,7 @@ [ ( "v1", - "gs://kfserving-samples/models/sklearn/iris", + "gs://kfserving-examples/models/sklearn/1.0/model", ), ( "v2", @@ -143,7 +143,7 @@ def test_mms_sklearn_kserve(protocol_version: str, storage_uri: str): [ ( "v1", - "gs://kfserving-samples/models/xgboost/iris", + "gs://kfserving-examples/models/xgboost/1.5/model", ), ( "v2", diff --git a/test/e2e/predictor/test_raw_deployment.py b/test/e2e/predictor/test_raw_deployment.py index 7e3b0494285..9ee53e3efaf 100644 --- a/test/e2e/predictor/test_raw_deployment.py +++ b/test/e2e/predictor/test_raw_deployment.py @@ -41,7 +41,7 @@ def test_raw_deployment_kserve(): predictor = V1beta1PredictorSpec( min_replicas=1, sklearn=V1beta1SKLearnSpec( - storage_uri="gs://kfserving-samples/models/sklearn/iris", + storage_uri="gs://kfserving-examples/models/sklearn/1.0/model", resources=V1ResourceRequirements( requests={"cpu": "100m", "memory": "256Mi"}, limits={"cpu": "100m", "memory": "256Mi"}, diff --git a/test/e2e/predictor/test_sklearn.py b/test/e2e/predictor/test_sklearn.py index bc4a3b80870..4fe94171cad 100644 --- a/test/e2e/predictor/test_sklearn.py +++ b/test/e2e/predictor/test_sklearn.py @@ -34,7 +34,7 @@ def test_sklearn_kserve(): predictor = V1beta1PredictorSpec( min_replicas=1, sklearn=V1beta1SKLearnSpec( - storage_uri="gs://kfserving-samples/models/sklearn/iris", + storage_uri="gs://kfserving-examples/models/sklearn/1.0/model", resources=V1ResourceRequirements( requests={"cpu": "100m", "memory": "256Mi"}, limits={"cpu": "100m", "memory": "256Mi"}, diff --git a/test/e2e/predictor/test_xgboost.py b/test/e2e/predictor/test_xgboost.py index 6c32df7816e..5b4b92c29b7 100644 --- a/test/e2e/predictor/test_xgboost.py +++ b/test/e2e/predictor/test_xgboost.py @@ -33,7 +33,7 @@ def test_xgboost_kserve(): predictor = V1beta1PredictorSpec( min_replicas=1, xgboost=V1beta1XGBoostSpec( - storage_uri="gs://kfserving-samples/models/xgboost/iris", + storage_uri="gs://kfserving-examples/models/xgboost/1.5/model", resources=V1ResourceRequirements( requests={"cpu": "100m", "memory": "256Mi"}, limits={"cpu": "100m", "memory": "256Mi"}, diff --git a/test/workflows/components/workflows.libsonnet b/test/workflows/components/workflows.libsonnet index 3e345b8fb19..b86f4598fc3 100644 --- a/test/workflows/components/workflows.libsonnet +++ b/test/workflows/components/workflows.libsonnet @@ -256,14 +256,6 @@ name: "build-custom-image-transformer", template: "build-custom-image-transformer", }, - { - name: "build-pytorchserver", - template: "build-pytorchserver", - }, - { - name: "build-pytorchserver-gpu", - template: "build-pytorchserver-gpu", - }, { name: "build-paddleserver", template: "build-paddleserver",