Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XGBoost runtime fixes #5938

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ jobs:
env:
VERSION: ${{ steps.docker-tag.outputs.value }}
run: |
make docker-build docker-push PYTHON_VERSION=3.8.10
make docker-tag-base-python docker-push-base-python PYTHON_VERSION=3.8.10
docker save -o /tmp/base-wrapper.tar seldonio/seldon-core-s2i-python38:${VERSION}
make docker-build docker-push PYTHON_VERSION=3.10.15
make docker-tag-base-python docker-push-base-python PYTHON_VERSION=3.10.15
docker save -o /tmp/base-wrapper.tar seldonio/seldon-core-s2i-python310:${VERSION}

- name: Upload artifact
uses: actions/upload-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion python-builder/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ RUN apt-get upgrade -y && \


# This is to install desired version of Python without updating conda version
ENV PYTHON_VERSION "3.7.10"
ENV PYTHON_VERSION "3.10.15"
ENV CONDA_VERSION "4.7.12"
RUN conda install --yes -c conda-forge python=$PYTHON_VERSION conda=$CONDA_VERSION

Expand Down
2 changes: 1 addition & 1 deletion servers/xgboostserver/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ docker-build:
s2i build \
-E environment \
./xgboostserver \
${DOCKER_REGISTRY}/seldon-core-s2i-python38:${VERSION} \
seldonio/seldon-core-s2i-python3:${VERSION} \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
seldonio/seldon-core-s2i-python3:${VERSION} \
${DOCKER_REGISTRY}/seldon-core-s2i-python3:${VERSION} \

${IMAGE_NAME}:${VERSION}

docker-push:
Expand Down
1 change: 1 addition & 0 deletions servers/xgboostserver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# XGBoost Runtime
39 changes: 39 additions & 0 deletions servers/xgboostserver/test/test_metadata_and_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import tempfile
import numpy as np
import pytest
import xgboost as xgb
from XGBoostServer import XGBoostServer

@pytest.fixture
def model_uri():
with tempfile.TemporaryDirectory() as temp_dir:
X = np.random.rand(100, 5)
y = np.random.randint(2, size=100)
dtrain = xgb.DMatrix(X, label=y)
params = {'objective': 'binary:logistic', 'eval_metric': 'error'}
booster = xgb.train(params, dtrain, num_boost_round=10)
model_path = os.path.join(temp_dir, "model.json")
booster.save_model(model_path)
yield temp_dir

def test_init_metadata(model_uri):
metadata = {"key": "value"}
metadata_path = os.path.join(model_uri, "metadata.yaml")
with open(metadata_path, "w") as f:
yaml.dump(metadata, f)

server = XGBoostServer(model_uri)

loaded_metadata = server.init_metadata()

# Assert that the loaded metadata matches the original metadata
assert loaded_metadata == metadata

def test_predict_invalid_input(model_uri):
# Create an instance of XGBoostServer with the model URI
server = XGBoostServer(model_uri)
server.load()
X_test = np.random.rand(10, 3) # Incorrect number of features
with pytest.raises(ValueError):
server.predict(X_test, names=[])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add new line

50 changes: 50 additions & 0 deletions servers/xgboostserver/test/test_model_and_predict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os
import tempfile
import numpy as np
from unittest import mock
import xgboost as xgb
from XGBoostServer import XGBoostServer

def test_load_json_model():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a test for the old format as well (if this test doesnt exist elsewhere?)


with tempfile.TemporaryDirectory() as temp_dir:
X = np.random.rand(100, 5)
y = np.random.randint(2, size=100)
dtrain = xgb.DMatrix(X, label=y)
params = {'objective': 'binary:logistic', 'eval_metric': 'error'}
booster = xgb.train(params, dtrain, num_boost_round=10)
model_path = os.path.join(temp_dir, "model.json")
booster.save_model(model_path)

server = XGBoostServer(temp_dir)
server.load()

assert server.ready
assert isinstance(server._booster, xgb.Booster)

def test_predict():
# Create a temporary directory for the model file
with tempfile.TemporaryDirectory() as temp_dir:
# Train a dummy XGBoost model and save it in .json format
X = np.random.rand(100, 5)
y = np.random.randint(2, size=100)
dtrain = xgb.DMatrix(X, label=y)
params = {'objective': 'binary:logistic', 'eval_metric': 'error'}
booster = xgb.train(params, dtrain, num_boost_round=10)
model_path = os.path.join(temp_dir, "model.json")
booster.save_model(model_path)

# Create an instance of XGBoostServer with the model URI
server = XGBoostServer(temp_dir)

server.load()

# Prepare test data
X_test = np.random.rand(10, 5)

with mock.patch("seldon_core.Storage.download", return_value=temp_dir):
predictions = server.predict(X_test, names=[])

# Assert the expected shape and type of predictions
assert isinstance(predictions, np.ndarray)
assert predictions.shape == (10,)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add new line

58 changes: 47 additions & 11 deletions servers/xgboostserver/test/xgboost_iris.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,55 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"import xgboost as xgb\n",
"from sklearn.datasets import load_iris\n",
"\n",
"from sklearn.datasets import load_iris"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"model_dir = \".\"\n",
"BST_FILE = \"model.bst\"\n",
"\n",
"BOOSTER_FILE = \"model.json\"\n",
"BOOSTER_FILE_DEPRECATED = \"model.bst\""
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"iris = load_iris()\n",
"y = iris[\"target\"]\n",
"X = iris[\"data\"]\n",
"X = iris[\"data\"]"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/ramonpzg/micromamba/envs/mlserver-quick/lib/python3.11/site-packages/xgboost/core.py:158: UserWarning: [18:32:52] WARNING: /workspace/src/learner.cc:740: \n",
"Parameters: { \"silent\" } are not used.\n",
"\n",
" warnings.warn(smsg, UserWarning)\n"
]
}
],
"source": [
"\n",
"dtrain = xgb.DMatrix(X, label=y)\n",
"param = {\n",
" \"max_depth\": 6,\n",
Expand All @@ -34,8 +68,10 @@
" \"objective\": \"multi:softmax\",\n",
"}\n",
"xgb_model = xgb.train(params=param, dtrain=dtrain)\n",
"model_file = os.path.join((model_dir), BST_FILE)\n",
"xgb_model.save_model(model_file)"
"model_file_json = os.path.join((model_dir), BOOSTER_FILE)\n",
"model_file_bst= os.path.join((model_dir), BOOSTER_FILE_DEPRECATED)\n",
"xgb_model.save_model(model_file_json)\n",
"xgb_model.save_model(model_file_bst)"
]
},
{
Expand Down Expand Up @@ -319,7 +355,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
Expand All @@ -333,7 +369,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.6"
"version": "3.11.9"
},
"varInspector": {
"cols": {
Expand Down
37 changes: 31 additions & 6 deletions servers/xgboostserver/xgboostserver/XGBoostServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import yaml
import logging
import xgboost as xgb
import json
from packaging import version

logger = logging.getLogger(__name__)

BOOSTER_FILE = "model.bst"
BOOSTER_FILE = "model.json"
BOOSTER_FILE_DEPRECATED = "model.bst"


class XGBoostServer(SeldonComponent):
Expand All @@ -22,7 +25,27 @@ def load(self):
model_file = os.path.join(
seldon_core.Storage.download(self.model_uri), BOOSTER_FILE
)
self._booster = xgb.Booster(model_file=model_file)
if not os.path.exists(model_file):
# Fallback to deprecated .bst format
model_file = os.path.join(
seldon_core.Storage.download(self.model_uri), BOOSTER_FILE_DEPRECATED
)
if os.path.exists(model_file):
logger.warning(
"Using deprecated .bst format for XGBoost model. "
"Please update to the .json format in the future."
)
else:
raise FileNotFoundError(f"Model file not found: {BOOSTER_FILE} or {BOOSTER_FILE_DEPRECATED}")

if version.parse(xgb.__version__) < version.parse("1.7.0"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this code path possible given that we build the server with a specific xgboost >= 1.7.0, <=2.2.0?

# Load model using deprecated method for older XGBoost versions
self._booster = xgb.Booster(model_file=model_file)
else:
# Load model using the new .json format for XGBoost >= 1.7.0
self._booster = xgb.Booster()
self._booster.load_model(model_file)

self.ready = True

def predict(
Expand All @@ -39,12 +62,14 @@ def init_metadata(self):

try:
with open(file_path, "r") as f:
return yaml.safe_load(f.read())
metadata = yaml.safe_load(f.read())
# Validate and sanitize the loaded metadata if needed
return metadata
Comment on lines +65 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change really required, this looks not to be adding anything compared to the existing code.

except FileNotFoundError:
logger.debug(f"metadata file {file_path} does not exist")
logger.debug(f"Metadata file {file_path} does not exist")
return {}
except yaml.YAMLError:
logger.error(
f"metadata file {file_path} present but does not contain valid yaml"
f"Metadata file {file_path} present but does not contain valid YAML"
)
return {}
return {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add new line

Binary file added servers/xgboostserver/xgboostserver/model.bst
Binary file not shown.
1 change: 1 addition & 0 deletions servers/xgboostserver/xgboostserver/model.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions servers/xgboostserver/xgboostserver/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
scikit-learn == 1.0.2
scikit-learn >= 1.0.2, <=1.5.0
numpy >= 1.8.2
xgboost == 1.4.2
xgboost >= 1.7.0, <=2.2.0
4 changes: 2 additions & 2 deletions wrappers/s2i/python/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ VERSION ?= $(shell cat ../../../version.txt)

DOCKER_REGISTRY ?= seldonio

CONDA_DOWNLOAD_VERSION ?= py38_23.3.1-0
CONDA_DOWNLOAD_VERSION ?= py310
CONDA_VERSION ?= 23.5.0
PYTHON_VERSION ?= 3.8.10
PYTHON_VERSION ?= 3.10.15

IMAGE_PYTHON_VERSION = $(shell echo -n $(PYTHON_VERSION) | cut -d. -f1-2 | sed 's/\.//g')
DEFAULT_IMAGE_PYTHON_VERSION = $(shell echo -n $(PYTHON_VERSION) | cut -d. -f1)
Expand Down
6 changes: 3 additions & 3 deletions wrappers/s2i/python/build_scripts/build_all.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
make -C ../ docker-build-conda-base

# Build standard wrapper images
make -C ../ docker-build PYTHON_VERSION=3.8.10
make -C ../ docker-build PYTHON_VERSION=3.10.15

# Push default tag image
make -C ../ docker-tag-base-python PYTHON_VERSION=3.8.10
make -C ../ docker-tag-base-python PYTHON_VERSION=3.10.15

# Build GPU images
make -C ../ docker-build-gpu PYTHON_VERSION=3.8.10
make -C ../ docker-build-gpu PYTHON_VERSION=3.10.15
6 changes: 3 additions & 3 deletions wrappers/s2i/python/build_scripts/push_all.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
make -C ../ docker-push-conda-base

# Push standard wrapper images
make -C ../ push_to_dockerhub PYTHON_VERSION=3.8.10
make -C ../ push_to_dockerhub PYTHON_VERSION=3.10.15

# Push default tag image
make -C ../ docker-push-base-python PYTHON_VERSION=3.8.10
make -C ../ docker-push-base-python PYTHON_VERSION=3.10.15

# Push GPU images
make -C ../ docker-push-gpu PYTHON_VERSION=3.8.10
make -C ../ docker-push-gpu PYTHON_VERSION=3.10.15
Loading