Skip to content

Commit 3a87c9d

Browse files
committed
[HWORKS-1224] Add llm signature and openai endpoint
1 parent 864c2ad commit 3a87c9d

File tree

9 files changed

+220
-1
lines changed

9 files changed

+220
-1
lines changed

python/hopsworks_common/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class MODEL:
158158
FRAMEWORK_TORCH = "TORCH"
159159
FRAMEWORK_PYTHON = "PYTHON"
160160
FRAMEWORK_SKLEARN = "SKLEARN"
161+
FRAMEWORK_LLM = "LLM"
161162

162163

163164
class MODEL_REGISTRY:
@@ -210,6 +211,7 @@ class PREDICTOR:
210211
# model server
211212
MODEL_SERVER_PYTHON = "PYTHON"
212213
MODEL_SERVER_TF_SERVING = "TENSORFLOW_SERVING"
214+
MODEL_SERVER_VLLM = "VLLM"
213215
# serving tool
214216
SERVING_TOOL_DEFAULT = "DEFAULT"
215217
SERVING_TOOL_KSERVE = "KSERVE"

python/hsml/core/serving_api.py

+3
Original file line numberDiff line numberDiff line change
@@ -417,4 +417,7 @@ def _get_hopsworks_inference_path(self, project_id: int, deployment_instance):
417417
]
418418

419419
def _get_istio_inference_path(self, deployment_instance):
420+
if deployment_instance.model_server == "VLLM":
421+
return ["openai", "v1", "completions"]
422+
420423
return ["v1", "models", deployment_instance.name + ":predict"]

python/hsml/engine/serving_engine.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,10 @@ def predict(
493493
inputs: Union[Dict, List[Dict]],
494494
):
495495
# validate user-provided payload
496-
self._validate_inference_payload(deployment_instance.api_protocol, data, inputs)
496+
if deployment_instance.model_server != "VLLM":
497+
self._validate_inference_payload(
498+
deployment_instance.api_protocol, data, inputs
499+
)
497500

498501
# build inference payload based on API protocol
499502
payload = self._build_inference_payload(

python/hsml/llm/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#
2+
# Copyright 2024 Hopsworks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#

python/hsml/llm/model.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#
2+
# Copyright 2024 Hopsworks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
import humps
18+
from hsml.constants import MODEL
19+
from hsml.model import Model
20+
21+
22+
class Model(Model):
23+
"""Metadata object representing a LLM model in the Model Registry."""
24+
25+
def __init__(
26+
self,
27+
id,
28+
name,
29+
version=None,
30+
created=None,
31+
creator=None,
32+
environment=None,
33+
description=None,
34+
project_name=None,
35+
metrics=None,
36+
program=None,
37+
user_full_name=None,
38+
model_schema=None,
39+
training_dataset=None,
40+
input_example=None,
41+
model_registry_id=None,
42+
tags=None,
43+
href=None,
44+
feature_view=None,
45+
training_dataset_version=None,
46+
**kwargs,
47+
):
48+
super().__init__(
49+
id,
50+
name,
51+
version=version,
52+
created=created,
53+
creator=creator,
54+
environment=environment,
55+
description=description,
56+
project_name=project_name,
57+
metrics=metrics,
58+
program=program,
59+
user_full_name=user_full_name,
60+
model_schema=model_schema,
61+
training_dataset=training_dataset,
62+
input_example=input_example,
63+
framework=MODEL.FRAMEWORK_LLM,
64+
model_registry_id=model_registry_id,
65+
feature_view=feature_view,
66+
training_dataset_version=training_dataset_version,
67+
)
68+
69+
def update_from_response_json(self, json_dict):
70+
json_decamelized = humps.decamelize(json_dict)
71+
json_decamelized.pop("framework")
72+
if "type" in json_decamelized: # backwards compatibility
73+
_ = json_decamelized.pop("type")
74+
self.__init__(**json_decamelized)
75+
return self

python/hsml/llm/predictor.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#
2+
# Copyright 2024 Hopsworks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
from hsml.constants import MODEL, PREDICTOR
18+
from hsml.predictor import Predictor
19+
20+
21+
class Predictor(Predictor):
22+
"""Configuration for a predictor running with the vLLM backend"""
23+
24+
def __init__(self, **kwargs):
25+
kwargs["model_framework"] = MODEL.FRAMEWORK_LLM
26+
kwargs["model_server"] = PREDICTOR.MODEL_SERVER_VLLM
27+
28+
super().__init__(**kwargs)

python/hsml/llm/signature.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#
2+
# Copyright 2024 Hopsworks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
from typing import Optional, Union
18+
19+
import numpy
20+
import pandas
21+
from hopsworks_common import usage
22+
from hsml.model_schema import ModelSchema
23+
from hsml.llm.model import Model
24+
25+
26+
_mr = None
27+
28+
29+
@usage.method_logger
30+
def create_model(
31+
name: str,
32+
version: Optional[int] = None,
33+
metrics: Optional[dict] = None,
34+
description: Optional[str] = None,
35+
input_example: Optional[
36+
Union[pandas.DataFrame, pandas.Series, numpy.ndarray, list]
37+
] = None,
38+
model_schema: Optional[ModelSchema] = None,
39+
feature_view=None,
40+
training_dataset_version: Optional[int] = None,
41+
):
42+
"""Create an LLM model metadata object.
43+
44+
!!! note "Lazy"
45+
This method is lazy and does not persist any metadata or uploads model artifacts in the
46+
model registry on its own. To save the model object and the model artifacts, call the `save()` method with a
47+
local file path to the directory containing the model artifacts.
48+
49+
# Arguments
50+
name: Name of the model to create.
51+
version: Optionally version of the model to create, defaults to `None` and
52+
will create the model with incremented version from the last
53+
version in the model registry.
54+
metrics: Optionally a dictionary with model evaluation metrics (e.g., accuracy, MAE)
55+
description: Optionally a string describing the model, defaults to empty string
56+
`""`.
57+
input_example: Optionally an input example that represents a single input for the model, defaults to `None`.
58+
model_schema: Optionally a model schema for the model inputs and/or outputs.
59+
60+
# Returns
61+
`Model`. The model metadata object.
62+
"""
63+
model = Model(
64+
id=None,
65+
name=name,
66+
version=version,
67+
description=description,
68+
metrics=metrics,
69+
input_example=input_example,
70+
model_schema=model_schema,
71+
feature_view=feature_view,
72+
training_dataset_version=training_dataset_version,
73+
)
74+
model._shared_registry_project_name = _mr.shared_registry_project_name
75+
model._model_registry_id = _mr.model_registry_id
76+
77+
return model

python/hsml/model_registry.py

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from hsml.sklearn import signature as sklearn_signature # noqa: F401
2525
from hsml.tensorflow import signature as tensorflow_signature # noqa: F401
2626
from hsml.torch import signature as torch_signature # noqa: F401
27+
from hsml.llm import signature as llm_signature # noqa: F401
2728

2829

2930
class ModelRegistry:
@@ -49,11 +50,13 @@ def __init__(
4950
self._python = python_signature
5051
self._sklearn = sklearn_signature
5152
self._torch = torch_signature
53+
self._llm = llm_signature
5254

5355
tensorflow_signature._mr = self
5456
python_signature._mr = self
5557
sklearn_signature._mr = self
5658
torch_signature._mr = self
59+
llm_signature._mr = self
5760

5861
@classmethod
5962
def from_response_json(cls, json_dict):
@@ -191,6 +194,12 @@ def python(self):
191194

192195
return python_signature
193196

197+
@property
198+
def llm(self):
199+
"""Module for exporting a Large Language Model."""
200+
201+
return llm_signature
202+
194203
def __repr__(self):
195204
project_name = (
196205
self._shared_registry_project_name

python/hsml/util.py

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def set_model_class(model):
100100
from hsml.sklearn.model import Model as SkLearnModel
101101
from hsml.tensorflow.model import Model as TFModel
102102
from hsml.torch.model import Model as TorchModel
103+
from hsml.llm.model import Model as LLMModel
103104

104105
if "href" in model:
105106
_ = model.pop("href")
@@ -120,6 +121,8 @@ def set_model_class(model):
120121
return SkLearnModel(**model)
121122
elif framework == MODEL.FRAMEWORK_PYTHON:
122123
return PyModel(**model)
124+
elif framework == MODEL.FRAMEWORK_LLM:
125+
return LLMModel(**model)
123126
else:
124127
raise ValueError(
125128
"framework {} is not a supported framework".format(str(framework))
@@ -242,6 +245,8 @@ def get_predictor_for_model(model, **kwargs):
242245
from hsml.tensorflow.predictor import Predictor as TFPredictor
243246
from hsml.torch.model import Model as TorchModel
244247
from hsml.torch.predictor import Predictor as TorchPredictor
248+
from hsml.llm.model import Model as LLMModel
249+
from hsml.llm.predictor import Predictor as vLLMPredictor
245250

246251
if not isinstance(model, BaseModel):
247252
raise ValueError(
@@ -258,6 +263,8 @@ def get_predictor_for_model(model, **kwargs):
258263
return SkLearnPredictor(**kwargs)
259264
if type(model) is PyModel:
260265
return PyPredictor(**kwargs)
266+
if type(model) is LLMModel:
267+
return vLLMPredictor(**kwargs)
261268
if type(model) is BaseModel:
262269
return BasePredictor( # python as default framework and model server
263270
model_framework=MODEL.FRAMEWORK_PYTHON,

0 commit comments

Comments
 (0)