Skip to content

Commit 2d6728e

Browse files
AQUA 1.0.5 (#971)
2 parents ef6184d + bfd32d4 commit 2d6728e

File tree

7 files changed

+270
-5
lines changed

7 files changed

+270
-5
lines changed

ads/aqua/extension/deployment_handler.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,33 @@ def get(self, id=""):
5454
else:
5555
raise HTTPError(400, f"The request {self.request.path} is invalid.")
5656

57+
@handle_exceptions
58+
def delete(self, model_deployment_id):
59+
return self.finish(AquaDeploymentApp().delete(model_deployment_id))
60+
61+
@handle_exceptions
62+
def put(self, *args, **kwargs):
63+
"""
64+
Handles put request for the activating and deactivating OCI datascience model deployments
65+
Raises
66+
------
67+
HTTPError
68+
Raises HTTPError if inputs are missing or are invalid
69+
"""
70+
url_parse = urlparse(self.request.path)
71+
paths = url_parse.path.strip("/").split("/")
72+
if len(paths) != 4 or paths[0] != "aqua" or paths[1] != "deployments":
73+
raise HTTPError(400, f"The request {self.request.path} is invalid.")
74+
75+
model_deployment_id = paths[2]
76+
action = paths[3]
77+
if action == "activate":
78+
return self.finish(AquaDeploymentApp().activate(model_deployment_id))
79+
elif action == "deactivate":
80+
return self.finish(AquaDeploymentApp().deactivate(model_deployment_id))
81+
else:
82+
raise HTTPError(400, f"The request {self.request.path} is invalid.")
83+
5784
@handle_exceptions
5885
def post(self, *args, **kwargs):
5986
"""
@@ -270,5 +297,7 @@ def post(self, *args, **kwargs):
270297
("deployments/?([^/]*)/params", AquaDeploymentParamsHandler),
271298
("deployments/config/?([^/]*)", AquaDeploymentHandler),
272299
("deployments/?([^/]*)", AquaDeploymentHandler),
300+
("deployments/?([^/]*)/activate", AquaDeploymentHandler),
301+
("deployments/?([^/]*)/deactivate", AquaDeploymentHandler),
273302
("inference", AquaDeploymentInferenceHandler),
274303
]

ads/aqua/extension/errors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ class Errors(str):
88
NO_INPUT_DATA = "No input data provided."
99
MISSING_REQUIRED_PARAMETER = "Missing required parameter: '{}'"
1010
MISSING_ONEOF_REQUIRED_PARAMETER = "Either '{}' or '{}' is required."
11+
INVALID_VALUE_OF_PARAMETER = "Invalid value of parameter: '{}'"

ads/aqua/extension/model_handler.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from ads.aqua.common.decorator import handle_exceptions
1111
from ads.aqua.common.errors import AquaRuntimeError, AquaValueError
12-
from ads.aqua.common.utils import get_hf_model_info, list_hf_models
12+
from ads.aqua.common.utils import (
13+
get_hf_model_info,
14+
list_hf_models,
15+
)
1316
from ads.aqua.extension.base_handler import AquaAPIhandler
1417
from ads.aqua.extension.errors import Errors
1518
from ads.aqua.model import AquaModelApp
@@ -73,6 +76,8 @@ def delete(self, id=""):
7376
paths = url_parse.path.strip("/")
7477
if paths.startswith("aqua/model/cache"):
7578
return self.finish(AquaModelApp().clear_model_list_cache())
79+
elif id:
80+
return self.finish(AquaModelApp().delete_model(id))
7681
else:
7782
raise HTTPError(400, f"The request {self.request.path} is invalid.")
7883

@@ -139,6 +144,36 @@ def post(self, *args, **kwargs):
139144
)
140145
)
141146

147+
@handle_exceptions
148+
def put(self, id):
149+
try:
150+
input_data = self.get_json_body()
151+
except Exception as ex:
152+
raise HTTPError(400, Errors.INVALID_INPUT_DATA_FORMAT) from ex
153+
154+
if not input_data:
155+
raise HTTPError(400, Errors.NO_INPUT_DATA)
156+
157+
inference_container = input_data.get("inference_container")
158+
inference_containers = AquaModelApp.list_valid_inference_containers()
159+
if (
160+
inference_container is not None
161+
and inference_container not in inference_containers
162+
):
163+
raise HTTPError(
164+
400, Errors.INVALID_VALUE_OF_PARAMETER.format("inference_container")
165+
)
166+
167+
enable_finetuning = input_data.get("enable_finetuning")
168+
task = input_data.get("task")
169+
app=AquaModelApp()
170+
self.finish(
171+
app.edit_registered_model(
172+
id, inference_container, enable_finetuning, task
173+
)
174+
)
175+
app.clear_model_details_cache(model_id=id)
176+
142177

143178
class AquaModelLicenseHandler(AquaAPIhandler):
144179
"""Handler for Aqua Model license REST APIs."""

ads/aqua/model/model.py

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
import oci
1111
from cachetools import TTLCache
1212
from huggingface_hub import snapshot_download
13-
from oci.data_science.models import JobRun, Model
13+
from oci.data_science.models import JobRun, Metadata, Model, UpdateModelDetails
1414

1515
from ads.aqua import ODSC_MODEL_COMPARTMENT_OCID, logger
1616
from ads.aqua.app import AquaApp
17-
from ads.aqua.common.enums import InferenceContainerTypeFamily, Tags
17+
from ads.aqua.common.enums import (
18+
FineTuningContainerTypeFamily,
19+
InferenceContainerTypeFamily,
20+
Tags,
21+
)
1822
from ads.aqua.common.errors import AquaRuntimeError, AquaValueError
1923
from ads.aqua.common.utils import (
2024
LifecycleStatus,
@@ -23,6 +27,7 @@
2327
create_word_icon,
2428
generate_tei_cmd_var,
2529
get_artifact_path,
30+
get_container_config,
2631
get_hf_model_info,
2732
list_os_files_with_extension,
2833
load_config,
@@ -78,7 +83,11 @@
7883
TENANCY_OCID,
7984
)
8085
from ads.model import DataScienceModel
81-
from ads.model.model_metadata import ModelCustomMetadata, ModelCustomMetadataItem
86+
from ads.model.model_metadata import (
87+
MetadataCustomCategory,
88+
ModelCustomMetadata,
89+
ModelCustomMetadataItem,
90+
)
8291
from ads.telemetry import telemetry
8392

8493

@@ -333,6 +342,96 @@ def get(self, model_id: str, load_model_card: Optional[bool] = True) -> "AquaMod
333342

334343
return model_details
335344

345+
@telemetry(entry_point="plugin=model&action=delete", name="aqua")
346+
def delete_model(self, model_id):
347+
ds_model = DataScienceModel.from_id(model_id)
348+
is_registered_model = ds_model.freeform_tags.get(Tags.BASE_MODEL_CUSTOM, None)
349+
is_fine_tuned_model = ds_model.freeform_tags.get(
350+
Tags.AQUA_FINE_TUNED_MODEL_TAG, None
351+
)
352+
if is_registered_model or is_fine_tuned_model:
353+
return ds_model.delete()
354+
else:
355+
raise AquaRuntimeError(
356+
f"Failed to delete model:{model_id}. Only registered models or finetuned model can be deleted."
357+
)
358+
359+
@telemetry(entry_point="plugin=model&action=delete", name="aqua")
360+
def edit_registered_model(self, id, inference_container, enable_finetuning, task):
361+
"""Edits the default config of unverified registered model.
362+
363+
Parameters
364+
----------
365+
id: str
366+
The model OCID.
367+
inference_container: str.
368+
The inference container family name
369+
enable_finetuning: str
370+
Flag to enable or disable finetuning over the model. Defaults to None
371+
task:
372+
The usecase type of the model. e.g , text-generation , text_embedding etc.
373+
374+
Returns
375+
-------
376+
Model:
377+
The instance of oci.data_science.models.Model.
378+
379+
"""
380+
ds_model = DataScienceModel.from_id(id)
381+
if ds_model.freeform_tags.get(Tags.BASE_MODEL_CUSTOM, None):
382+
if ds_model.freeform_tags.get(Tags.AQUA_SERVICE_MODEL_TAG, None):
383+
raise AquaRuntimeError(
384+
f"Failed to edit model:{id}. Only registered unverified models can be edited."
385+
)
386+
else:
387+
custom_metadata_list = ds_model.custom_metadata_list
388+
freeform_tags = ds_model.freeform_tags
389+
if inference_container:
390+
custom_metadata_list.add(
391+
key=ModelCustomMetadataFields.DEPLOYMENT_CONTAINER,
392+
value=inference_container,
393+
category=MetadataCustomCategory.OTHER,
394+
description="Deployment container mapping for SMC",
395+
replace=True,
396+
)
397+
if enable_finetuning is not None:
398+
if enable_finetuning.lower() == "true":
399+
custom_metadata_list.add(
400+
key=ModelCustomMetadataFields.FINETUNE_CONTAINER,
401+
value=FineTuningContainerTypeFamily.AQUA_FINETUNING_CONTAINER_FAMILY,
402+
category=MetadataCustomCategory.OTHER,
403+
description="Fine-tuning container mapping for SMC",
404+
replace=True,
405+
)
406+
freeform_tags.update({Tags.READY_TO_FINE_TUNE: "true"})
407+
elif enable_finetuning.lower() == "false":
408+
try:
409+
custom_metadata_list.remove(
410+
ModelCustomMetadataFields.FINETUNE_CONTAINER
411+
)
412+
freeform_tags.pop(Tags.READY_TO_FINE_TUNE)
413+
except Exception as ex:
414+
raise AquaRuntimeError(
415+
f"The given model already doesn't support finetuning: {ex}"
416+
)
417+
418+
custom_metadata_list.remove("modelDescription")
419+
if task:
420+
freeform_tags.update({Tags.TASK: task})
421+
updated_custom_metadata_list = [
422+
Metadata(**metadata)
423+
for metadata in custom_metadata_list.to_dict()["data"]
424+
]
425+
update_model_details = UpdateModelDetails(
426+
custom_metadata_list=updated_custom_metadata_list,
427+
freeform_tags=freeform_tags,
428+
)
429+
AquaApp().update_model(id, update_model_details)
430+
else:
431+
raise AquaRuntimeError(
432+
f"Failed to edit model:{id}. Only registered unverified models can be edited."
433+
)
434+
336435
def _fetch_metric_from_metadata(
337436
self,
338437
custom_metadata_list: ModelCustomMetadata,
@@ -629,6 +728,32 @@ def clear_model_list_cache(
629728
}
630729
return res
631730

731+
def clear_model_details_cache(self, model_id):
732+
"""
733+
Allows user to clear model details cache item
734+
Returns
735+
-------
736+
dict with the key used, and True if cache has the key that needs to be deleted.
737+
"""
738+
res = {}
739+
logger.info(f"Clearing _service_model_details_cache for {model_id}")
740+
with self._cache_lock:
741+
if model_id in self._service_model_details_cache:
742+
self._service_model_details_cache.pop(key=model_id)
743+
res = {"key": {"model_id": model_id}, "cache_deleted": True}
744+
745+
return res
746+
747+
@staticmethod
748+
def list_valid_inference_containers():
749+
containers = list(
750+
AquaContainerConfig.from_container_index_json(
751+
config=get_container_config(), enable_spec=True
752+
).inference.values()
753+
)
754+
family_values = [item.family for item in containers]
755+
return family_values
756+
632757
def _create_model_catalog_entry(
633758
self,
634759
os_path: str,

ads/aqua/modeldeployment/deployment.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,18 @@ def list(self, **kwargs) -> List["AquaDeployment"]:
532532

533533
return results
534534

535+
@telemetry(entry_point="plugin=deployment&action=delete", name="aqua")
536+
def delete(self,model_deployment_id:str):
537+
return self.ds_client.delete_model_deployment(model_deployment_id=model_deployment_id).data
538+
539+
@telemetry(entry_point="plugin=deployment&action=deactivate",name="aqua")
540+
def deactivate(self,model_deployment_id:str):
541+
return self.ds_client.deactivate_model_deployment(model_deployment_id=model_deployment_id).data
542+
543+
@telemetry(entry_point="plugin=deployment&action=activate",name="aqua")
544+
def activate(self,model_deployment_id:str):
545+
return self.ds_client.activate_model_deployment(model_deployment_id=model_deployment_id).data
546+
535547
@telemetry(entry_point="plugin=deployment&action=get", name="aqua")
536548
def get(self, model_deployment_id: str, **kwargs) -> "AquaDeploymentDetail":
537549
"""Gets the information of Aqua model deployment.

tests/unitary/with_extras/aqua/test_deployment_handler.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ def test_get_deployment(self, mock_get):
9292
self.deployment_handler.get(id="mock-model-id")
9393
mock_get.assert_called()
9494

95+
@patch("ads.aqua.modeldeployment.AquaDeploymentApp.delete")
96+
def test_delete_deployment(self, mock_delete):
97+
self.deployment_handler.request.path = "aqua/deployments"
98+
self.deployment_handler.delete("mock-model-id")
99+
mock_delete.assert_called()
100+
101+
@patch("ads.aqua.modeldeployment.AquaDeploymentApp.activate")
102+
def test_activate_deployment(self, mock_activate):
103+
self.deployment_handler.request.path = (
104+
"aqua/deployments/ocid1.datasciencemodeldeployment.oc1.iad.xxx/activate"
105+
)
106+
mock_activate.return_value = {"lifecycle_state": "UPDATING"}
107+
self.deployment_handler.put()
108+
mock_activate.assert_called()
109+
110+
@patch("ads.aqua.modeldeployment.AquaDeploymentApp.deactivate")
111+
def test_deactivate_deployment(self, mock_deactivate):
112+
self.deployment_handler.request.path = (
113+
"aqua/deployments/ocid1.datasciencemodeldeployment.oc1.iad.xxx/deactivate"
114+
)
115+
mock_deactivate.return_value = {"lifecycle_state": "UPDATING"}
116+
self.deployment_handler.put()
117+
mock_deactivate.assert_called()
118+
95119
@patch("ads.aqua.modeldeployment.AquaDeploymentApp.list")
96120
def test_list_deployment(self, mock_list):
97121
"""Test get method to return a list of model deployments."""

tests/unitary/with_extras/aqua/test_model_handler.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
AquaModelLicenseHandler,
2121
)
2222
from ads.aqua.model import AquaModelApp
23-
from ads.aqua.model.constants import ModelTask
2423
from ads.aqua.model.entities import AquaModel, AquaModelSummary, HFModelSummary
2524

2625

@@ -80,6 +79,46 @@ def test_delete(self, mock_urlparse, mock_clear_model_list_cache):
8079
mock_urlparse.assert_called()
8180
mock_clear_model_list_cache.assert_called()
8281

82+
@patch("ads.aqua.extension.model_handler.urlparse")
83+
@patch.object(AquaModelApp, "delete_model")
84+
def test_delete_with_id(self, mock_delete, mock_urlparse):
85+
request_path = MagicMock(path="aqua/model/ocid1.datasciencemodel.oc1.iad.xxx")
86+
mock_urlparse.return_value = request_path
87+
mock_delete.return_value = {"state": "DELETED"}
88+
with patch(
89+
"ads.aqua.extension.base_handler.AquaAPIhandler.finish"
90+
) as mock_finish:
91+
mock_finish.side_effect = lambda x: x
92+
result = self.model_handler.delete(id="ocid1.datasciencemodel.oc1.iad.xxx")
93+
assert result["state"] is "DELETED"
94+
mock_urlparse.assert_called()
95+
mock_delete.assert_called()
96+
97+
@patch.object(AquaModelApp, "list_valid_inference_containers")
98+
@patch.object(AquaModelApp, "edit_registered_model")
99+
def test_put(self, mock_edit, mock_inference_container_list):
100+
mock_edit.return_value = None
101+
mock_inference_container_list.return_value = [
102+
"odsc-vllm-serving",
103+
"odsc-tgi-serving",
104+
"odsc-llama-cpp-serving",
105+
]
106+
self.model_handler.get_json_body = MagicMock(
107+
return_value=dict(
108+
task="text_generation",
109+
enable_finetuning="true",
110+
inference_container="odsc-tgi-serving",
111+
)
112+
)
113+
with patch(
114+
"ads.aqua.extension.base_handler.AquaAPIhandler.finish"
115+
) as mock_finish:
116+
mock_finish.side_effect = lambda x: x
117+
result = self.model_handler.put(id="ocid1.datasciencemodel.oc1.iad.xxx")
118+
assert result is None
119+
mock_edit.assert_called_once()
120+
mock_inference_container_list.assert_called_once()
121+
83122
@patch.object(AquaModelApp, "list")
84123
def test_list(self, mock_list):
85124
with patch(

0 commit comments

Comments
 (0)