diff --git a/ads/aqua/extension/deployment_handler.py b/ads/aqua/extension/deployment_handler.py index 4c4fc2ac5..e8225396f 100644 --- a/ads/aqua/extension/deployment_handler.py +++ b/ads/aqua/extension/deployment_handler.py @@ -88,6 +88,15 @@ def put(self, *args, **kwargs): # noqa: ARG002 return self.finish(AquaDeploymentApp().activate(model_deployment_id)) elif action == "deactivate": return self.finish(AquaDeploymentApp().deactivate(model_deployment_id)) + elif action == "update": + try: + input_data = self.get_json_body() + except Exception as ex: + raise HTTPError(400, Errors.INVALID_INPUT_DATA_FORMAT) from ex + + if not input_data: + raise HTTPError(400, Errors.NO_INPUT_DATA) + return self.finish(AquaDeploymentApp().update(**input_data)) else: raise HTTPError(400, f"The request {self.request.path} is invalid.") @@ -294,5 +303,6 @@ def post(self, *args, **kwargs): # noqa: ARG002 ("deployments/?([^/]*)", AquaDeploymentHandler), ("deployments/?([^/]*)/activate", AquaDeploymentHandler), ("deployments/?([^/]*)/deactivate", AquaDeploymentHandler), + ("deployments/?([^/]*)/update", AquaDeploymentHandler), ("inference", AquaDeploymentInferenceHandler), ] diff --git a/ads/aqua/modeldeployment/deployment.py b/ads/aqua/modeldeployment/deployment.py index cdc77da3c..3a191850c 100644 --- a/ads/aqua/modeldeployment/deployment.py +++ b/ads/aqua/modeldeployment/deployment.py @@ -17,7 +17,12 @@ ComputeShapeSummary, ContainerPath, ) -from ads.aqua.common.enums import InferenceContainerTypeFamily, ModelFormat, Tags +from ads.aqua.common.enums import ( + InferenceContainerType, + InferenceContainerTypeFamily, + ModelFormat, + Tags, +) from ads.aqua.common.errors import AquaRuntimeError, AquaValueError from ads.aqua.common.utils import ( DEFINED_METADATA_TO_FILE_MAP, @@ -55,8 +60,9 @@ AquaDeploymentDetail, ConfigurationItem, ConfigValidationError, - CreateModelDeploymentDetails, ModelDeploymentConfigSummary, + ModelDeploymentCreateSpec, + ModelDeploymentUpdateSpec, ) from ads.aqua.modeldeployment.utils import MultiModelDeploymentConfigLoader from ads.common.object_storage_details import ObjectStorageDetails @@ -75,6 +81,7 @@ ModelDeploymentInfrastructure, ModelDeploymentMode, ) +from ads.model.deployment.common.utils import State from ads.model.model_metadata import ModelCustomMetadataItem from ads.telemetry import telemetry @@ -91,6 +98,8 @@ class AquaDeploymentApp(AquaApp): Creates a model deployment for Aqua Model. get(model_deployment_id: str) -> AquaDeployment: Retrieves details of an Aqua model deployment by its unique identifier. + update(deployment_id: str, instance_shape: str, display_name: str,...) -> AquaDeployment + Updates a model deployment for Aqua Model. list(**kwargs) -> List[AquaModelSummary]: Lists all Aqua deployments within a specified compartment and/or project. get_deployment_config(self, model_id: str) -> AquaDeploymentConfig: @@ -111,7 +120,7 @@ class AquaDeploymentApp(AquaApp): @telemetry(entry_point="plugin=deployment&action=create", name="aqua") def create( self, - create_deployment_details: Optional[CreateModelDeploymentDetails] = None, + create_deployment_details: Optional[ModelDeploymentCreateSpec] = None, **kwargs, ) -> "AquaDeployment": """ @@ -119,8 +128,8 @@ def create( For detailed information about CLI flags see: https://github.com/oracle-samples/oci-data-science-ai-samples/blob/main/ai-quick-actions/cli-tips.md#create-model-deployment Args: - create_deployment_details : CreateModelDeploymentDetails, optional - An instance of CreateModelDeploymentDetails containing all required and optional + create_deployment_details : ModelDeploymentCreateSpec, optional + An instance of ModelDeploymentCreateSpec containing all required and optional fields for creating a model deployment via Aqua. kwargs: instance_shape (str): The instance shape used for deployment. @@ -157,7 +166,7 @@ def create( # Build deployment details from kwargs if not explicitly provided. if create_deployment_details is None: try: - create_deployment_details = CreateModelDeploymentDetails(**kwargs) + create_deployment_details = ModelDeploymentCreateSpec(**kwargs) except ValidationError as ex: custom_errors = build_pydantic_error_message(ex) raise AquaValueError( @@ -178,9 +187,7 @@ def create( # validate instance shape availability in compartment available_shapes = [ shape.name.lower() - for shape in self.list_shapes( - compartment_id=compartment_id - ) + for shape in self.list_shapes(compartment_id=compartment_id) ] if create_deployment_details.instance_shape.lower() not in available_shapes: @@ -309,7 +316,7 @@ def create( def _create( self, aqua_model: DataScienceModel, - create_deployment_details: CreateModelDeploymentDetails, + create_deployment_details: ModelDeploymentCreateSpec, container_config: Dict, ) -> AquaDeployment: """Builds the configurations required by single model deployment and creates the deployment. @@ -318,8 +325,8 @@ def _create( ---------- aqua_model : DataScienceModel An instance of Aqua data science model. - create_deployment_details : CreateModelDeploymentDetails - An instance of CreateModelDeploymentDetails containing all required and optional + create_deployment_details : ModelDeploymentCreateSpec + An instance of ModelDeploymentCreateSpec containing all required and optional fields for creating a model deployment via Aqua. container_config: Dict Container config dictionary. @@ -329,6 +336,56 @@ def _create( AquaDeployment An Aqua deployment instance. """ + ( + model_name, + model_type, + container_image_uri, + server_port, + health_check_port, + env_var, + tags, + cmd_var, + ) = self._get_runtime_config_from_model_deployment( + aqua_model=aqua_model, + deployment_details=create_deployment_details, + container_config=container_config, + ) + + return self._create_deployment( + create_deployment_details=create_deployment_details, + aqua_model_id=aqua_model.id, + model_name=model_name, + model_type=model_type, + container_image_uri=container_image_uri, + server_port=server_port, + health_check_port=health_check_port, + env_var=env_var, + tags=tags, + cmd_var=cmd_var, + ) + + def _get_runtime_config_from_model_deployment( + self, + aqua_model: DataScienceModel, + deployment_details: Union[ModelDeploymentCreateSpec, ModelDeploymentUpdateSpec], + container_config: Dict, + ): + """Gets required or optional configurations for building container runtime of model deployment. + + Parameters + ---------- + aqua_model: DataScienceModel + An instance of Aqua data science model. + deployment_details: Union[ModelDeploymentCreateSpec, ModelDeploymentUpdateSpec] + An instance of either ModelDeploymentCreateSpec or ModelDeploymentUpdateSpec containing all required and optional + fields for creating or updating a model deployment via Aqua. + container_config: Dict + Container config dictionary. + + Returns + ------- + A tuple of required or optional configurations for container runtime of model deployment. + """ tags = {} for tag in [ Tags.AQUA_SERVICE_MODEL_TAG, @@ -342,7 +399,7 @@ def _create( tags.update({Tags.TASK: aqua_model.freeform_tags.get(Tags.TASK, UNKNOWN)}) # Set up info to get deployment config - config_source_id = create_deployment_details.model_id + config_source_id = deployment_details.model_id model_name = aqua_model.display_name is_fine_tuned_model = Tags.AQUA_FINE_TUNED_MODEL_TAG in aqua_model.freeform_tags @@ -362,8 +419,8 @@ def _create( ) from err # set up env and cmd var - env_var = create_deployment_details.env_var or {} - cmd_var = create_deployment_details.cmd_var or [] + env_var = deployment_details.env_var or {} + cmd_var = deployment_details.cmd_var or [] try: model_path_prefix = aqua_model.custom_metadata_list.get( @@ -397,11 +454,11 @@ def _create( container_type_key = self._get_container_type_key( model=aqua_model, - container_family=create_deployment_details.container_family, + container_family=deployment_details.container_family, ) container_image_uri = ( - create_deployment_details.container_image_uri + deployment_details.container_image_uri or self.get_container_image(container_type=container_type_key) ) if not container_image_uri: @@ -448,7 +505,7 @@ def _create( and container_type_key.lower() == InferenceContainerTypeFamily.AQUA_LLAMA_CPP_CONTAINER_FAMILY ): - model_file = create_deployment_details.model_file + model_file = deployment_details.model_file if model_file is not None: logger.info( f"Overriding {model_file} as model_file for model {aqua_model.id}." @@ -476,18 +533,18 @@ def _create( container_spec = container_config.spec if container_config else UNKNOWN # these params cannot be overridden for Aqua deployments params = container_spec.cli_param if container_spec else UNKNOWN - server_port = create_deployment_details.server_port or ( + server_port = deployment_details.server_port or ( container_spec.server_port if container_spec else None ) # Give precendece to the input parameter - health_check_port = create_deployment_details.health_check_port or ( + health_check_port = deployment_details.health_check_port or ( container_spec.health_check_port if container_spec else None ) deployment_config = self.get_deployment_config(model_id=config_source_id) config_params = deployment_config.configuration.get( - create_deployment_details.instance_shape, ConfigurationItem() + deployment_details.instance_shape, ConfigurationItem() ).parameters.get(get_container_params_type(container_type_key), UNKNOWN) # validate user provided params @@ -529,29 +586,27 @@ def _create( logger.info(f"Env vars used for deploying {aqua_model.id} :{env_var}") - tags = {**tags, **(create_deployment_details.freeform_tags or {})} + tags = {**tags, **(deployment_details.freeform_tags or {})} model_type = ( AQUA_MODEL_TYPE_CUSTOM if is_fine_tuned_model else AQUA_MODEL_TYPE_SERVICE ) - return self._create_deployment( - create_deployment_details=create_deployment_details, - aqua_model_id=aqua_model.id, - model_name=model_name, - model_type=model_type, - container_image_uri=container_image_uri, - server_port=server_port, - health_check_port=health_check_port, - env_var=env_var, - tags=tags, - cmd_var=cmd_var, + return ( + model_name, + model_type, + container_image_uri, + server_port, + health_check_port, + env_var, + tags, + cmd_var, ) def _create_multi( self, aqua_model: DataScienceModel, model_config_summary: ModelDeploymentConfigSummary, - create_deployment_details: CreateModelDeploymentDetails, + create_deployment_details: ModelDeploymentCreateSpec, container_config: AquaContainerConfig, ) -> AquaDeployment: """Builds the environment variables required by multi deployment container and creates the deployment. @@ -562,8 +617,8 @@ def _create_multi( Summary Model Deployment configuration for the group of models. aqua_model : DataScienceModel An instance of Aqua data science model. - create_deployment_details : CreateModelDeploymentDetails - An instance of CreateModelDeploymentDetails containing all required and optional + create_deployment_details : ModelDeploymentCreateSpec + An instance of ModelDeploymentCreateSpec containing all required and optional fields for creating a model deployment via Aqua. container_config: Dict Container config dictionary. @@ -695,7 +750,7 @@ def _create_multi( def _create_deployment( self, - create_deployment_details: CreateModelDeploymentDetails, + create_deployment_details: ModelDeploymentCreateSpec, aqua_model_id: str, model_name: str, model_type: str, @@ -710,8 +765,8 @@ def _create_deployment( Parameters ---------- - create_deployment_details : CreateModelDeploymentDetails - An instance of CreateModelDeploymentDetails containing all required and optional + create_deployment_details : ModelDeploymentCreateSpec + An instance of ModelDeploymentCreateSpec containing all required and optional fields for creating a model deployment via Aqua. aqua_model_id: str The id of the aqua model to be deployed. @@ -903,6 +958,256 @@ def list(self, **kwargs) -> List["AquaDeployment"]: return results + @telemetry(entry_point="plugin=deployment&action=update", name="aqua") + def update( + self, + update_deployment_details: Optional[ModelDeploymentUpdateSpec] = None, + **kwargs, + ) -> "AquaDeployment": + """ + Updates an Aqua model deployment.\n + For detailed information about CLI flags see: https://github.com/oracle-samples/oci-data-science-ai-samples/blob/main/ai-quick-actions/cli-tips.md#update-model-deployment + + Args: + update_deployment_details : ModelDeploymentUpdateSpec, optional + An instance of ModelDeploymentUpdateSpec containing all required and optional + fields for updating a model deployment via Aqua. + kwargs: + deployment_id (str): The OCID of model deployment to be update. + instance_shape (Optional[str]): The instance shape used for deployment. + display_name (Optional[str]): The name of the model deployment. + description (Optional[str]): The description of the deployment. + model_id (Optional[str]): The model OCID to deploy. + models (Optional[List[AquaMultiModelRef]]): List of models for multimodel deployment. + instance_count (Optional[int]): Number of instances used for deployment. + log_group_id (Optional[str]): OCI logging group ID for logs. + access_log_id (Optional[str]): OCID for access logs. + predict_log_id (Optional[str]): OCID for prediction logs. + bandwidth_mbps (Optional[int]): Bandwidth limit on the load balancer in Mbps. + web_concurrency (Optional[int]): Number of worker processes/threads for handling requests. + server_port (Optional[int]): Server port for the Docker container image. + health_check_port (Optional[int]): Health check port for the Docker container image. + env_var (Optional[Dict[str, str]]): Environment variables for deployment. + container_family (Optional[str]): Image family of the model deployment container runtime. + memory_in_gbs (Optional[float]): Memory (in GB) for the selected shape. + ocpus (Optional[float]): OCPU count for the selected shape. + model_file (Optional[str]): File used for model deployment. + private_endpoint_id (Optional[str]): Private endpoint ID for model deployment. + container_image_uri (Optional[str]): Image URI for model deployment container runtime. + cmd_var (Optional[List[str]]): Command variables for the container runtime. + freeform_tags (Optional[Dict]): Freeform tags for model deployment. + defined_tags (Optional[Dict]): Defined tags for model deployment. + + Returns + ------- + AquaDeployment + An Aqua deployment instance. + """ + # Build update deployment details from kwargs if not explicitly provided. + if update_deployment_details is None: + try: + update_deployment_details = ModelDeploymentUpdateSpec(**kwargs) + except ValidationError as ex: + custom_errors = build_pydantic_error_message(ex) + raise AquaValueError( + f"Invalid parameters for updating model deployment. Error details: {custom_errors}." + ) from ex + + if ( + update_deployment_details.container_family + or update_deployment_details.container_image_uri + ): + raise AquaValueError( + "Updating inference container is not supported at this moment for Aqau Deployment." + ) + + aqua_deployment = ModelDeployment.from_id( + update_deployment_details.deployment_id + ) + + if aqua_deployment.lifecycle_state != State.ACTIVE.name: + raise AquaValueError( + f"Invalid parameter `deployment_id`. Model deployment has to be {State.ACTIVE.name} to be updated." + f"Wait for the status to become {State.ACTIVE.name} or specify a different `deployment_id`." + ) + + aqua_model = DataScienceModel.from_id(aqua_deployment.runtime.model_uri) + + oci_aqua = ( + ( + Tags.AQUA_TAG in aqua_deployment.freeform_tags + or Tags.AQUA_TAG.lower() in aqua_deployment.freeform_tags + ) + if aqua_deployment.freeform_tags + else False + ) + + if not oci_aqua: + raise AquaRuntimeError( + f"Target deployment {aqua_deployment.id} is not Aqua deployment as it does not contain " + f"{Tags.AQUA_TAG} tag." + ) + + model_name = UNKNOWN + is_fine_tuned_model = Tags.AQUA_FINE_TUNED_MODEL_TAG in aqua_model.freeform_tags + model_type = ( + AQUA_MODEL_TYPE_CUSTOM if is_fine_tuned_model else AQUA_MODEL_TYPE_SERVICE + ) + if Tags.MULTIMODEL_TYPE_TAG not in aqua_deployment.freeform_tags: + self._update( + model=aqua_model, + model_deployment=aqua_deployment, + update_deployment_details=update_deployment_details, + ) + model_name = aqua_model.display_name + else: + # TODO: update multi-model deployment + pass + + logger.info( + f"Aqua model deployment {aqua_deployment.id} updated for model {aqua_model.id}." + ) + + # we arbitrarily choose last 8 characters of OCID to identify MD in telemetry + telemetry_kwargs = {"ocid": get_ocid_substring(aqua_deployment.id, key_len=8)} + + # tracks unique deployments that were created in the user compartment + self.telemetry.record_event_async( + category=f"aqua/{model_type}/deployment", + action="update", + detail=model_name, + **telemetry_kwargs, + ) + # tracks the shape used for deploying the custom or service models by name + self.telemetry.record_event_async( + category=f"aqua/{model_type}/deployment/update", + action="shape", + detail=update_deployment_details.instance_shape, + value=model_name, + ) + + return AquaDeployment.from_oci_model_deployment( + aqua_deployment.dsc_model_deployment, self.region + ) + + def _update( + self, + model: DataScienceModel, + model_deployment: ModelDeployment, + update_deployment_details: ModelDeploymentUpdateSpec, + ): + """Builds the configurations required by single model deployment and updates the deployment. + + Parameters + ---------- + model : DataScienceModel + An instance of Aqua data science model. + model_deployment : ModelDeployment + An instance of Aqua model deployment. + update_deployment_details : ModelDeploymentUpdateSpec + An instance of ModelDeploymentUpdateSpec containing all required and optional + fields for updating a model deployment via Aqua. + """ + infrastructure = model_deployment.infrastructure + runtime = model_deployment.runtime + # updates model deployment infrastructure + ( + infrastructure.with_shape_name( + update_deployment_details.instance_shape or infrastructure.shape_name + ) + .with_bandwidth_mbps( + update_deployment_details.bandwidth_mbps + or infrastructure.bandwidth_mbps + ) + .with_replica( + update_deployment_details.instance_count or infrastructure.replica + ) + .with_web_concurrency( + update_deployment_details.web_concurrency + or infrastructure.web_concurrency + ) + .with_private_endpoint_id( + update_deployment_details.private_endpoint_id + or infrastructure.private_endpoint_id + ) + ) + if ( + update_deployment_details.log_group_id + and update_deployment_details.access_log_id + ): + ( + infrastructure.with_access_log( + log_group_id=update_deployment_details.log_group_id, + log_id=update_deployment_details.access_log_id, + ) + ) + if ( + update_deployment_details.log_group_id + and update_deployment_details.predict_log_id + ): + ( + infrastructure.with_predict_log( + log_group_id=update_deployment_details.log_group_id, + log_id=update_deployment_details.predict_log_id, + ) + ) + if ( + update_deployment_details.memory_in_gbs + and update_deployment_details.ocpus + and infrastructure.shape_name.endswith("Flex") + ): + infrastructure.with_shape_config_details( + ocpus=update_deployment_details.ocpus, + memory_in_gbs=update_deployment_details.memory_in_gbs, + ) + + # updates model deployment runtime + update_deployment_details.model_id = model.id + ( + _, + _, + container_image_uri, + server_port, + health_check_port, + env_var, + tags, + cmd_var, + ) = self._get_runtime_config_from_model_deployment( + aqua_model=model, + deployment_details=update_deployment_details, + container_config=self.get_container_config(), + ) + + if ( + update_deployment_details.env_var + or update_deployment_details.instance_shape + ): + runtime.with_env(env_var) + + if update_deployment_details.cmd_var: + runtime.with_cmd(cmd_var) + + # updates model deployment + ( + model_deployment.with_display_name( + update_deployment_details.display_name or model_deployment.display_name + ) + .with_description( + update_deployment_details.description or model_deployment.description + ) + .with_defined_tags( + **( + update_deployment_details.defined_tags + or model_deployment.defined_tags + ) + ) + ) + + if update_deployment_details.freeform_tags: + model_deployment.with_freeform_tags(**tags) + + model_deployment.update(wait_for_completion=False) + @telemetry(entry_point="plugin=deployment&action=delete", name="aqua") def delete(self, model_deployment_id: str): logger.info(f"Deleting model deployment {model_deployment_id}.") @@ -987,6 +1292,29 @@ def get(self, model_deployment_id: str, **kwargs) -> "AquaDeploymentDetail": compartment_id=model_deployment.compartment_id, source_id=model_deployment.id, ) + bandwidth_mbps = model_deployment.model_deployment_configuration_details.model_configuration_details.bandwidth_mbps + environment_configuration_details = model_deployment.model_deployment_configuration_details.environment_configuration_details + inference_mode = environment_configuration_details.environment_variables.get( + "MODEL_DEPLOY_PREDICT_ENDPOINT", None + ) + + image = environment_configuration_details.image + container_family = image[image.rfind("/") + 1 : image.rfind(":")] + container_type = UNKNOWN + if container_family == InferenceContainerTypeFamily.AQUA_VLLM_CONTAINER_FAMILY: + container_type = InferenceContainerType.CONTAINER_TYPE_VLLM.upper() + elif container_family == InferenceContainerTypeFamily.AQUA_TGI_CONTAINER_FAMILY: + container_type = InferenceContainerType.CONTAINER_TYPE_TGI.upper() + elif ( + container_family + == InferenceContainerTypeFamily.AQUA_LLAMA_CPP_CONTAINER_FAMILY + ): + container_type = InferenceContainerType.CONTAINER_TYPE_LLAMA_CPP.upper() + + inference_container = f"{container_type}:{image[image.rfind(':')+1:]}" + + defined_tags = model_deployment.defined_tags + freeform_tags = model_deployment.freeform_tags aqua_deployment = AquaDeployment.from_oci_model_deployment( model_deployment, self.region @@ -1029,6 +1357,11 @@ def get(self, model_deployment_id: str, **kwargs) -> "AquaDeploymentDetail": log_group_id, log_group_name, log_group_url ), log=AquaResourceIdentifier(log_id, log_name, log_url), + bandwidth_mbps=bandwidth_mbps, + inference_mode=inference_mode, + inference_container=inference_container, + defined_tags=defined_tags, + freeform_tags=freeform_tags, ) @telemetry( diff --git a/ads/aqua/modeldeployment/entities.py b/ads/aqua/modeldeployment/entities.py index 5899e5b2f..bf3e54b03 100644 --- a/ads/aqua/modeldeployment/entities.py +++ b/ads/aqua/modeldeployment/entities.py @@ -208,6 +208,21 @@ class AquaDeploymentDetail(AquaDeployment, DataClassSerializable): log_group: AquaResourceIdentifier = Field(default_factory=AquaResourceIdentifier) log: AquaResourceIdentifier = Field(default_factory=AquaResourceIdentifier) + bandwidth_mbps: Optional[int] = Field( + None, description="Bandwidth limit on the load balancer in Mbps." + ) + inference_container: Optional[str] = Field( + None, description="Inference container of the model deployment." + ) + inference_mode: Optional[str] = Field( + None, description="Inference mode of the model deployment." + ) + defined_tags: Optional[Dict] = Field( + None, description="Defined tags for model deployment." + ) + freeform_tags: Optional[Dict] = Field( + None, description="Freeform tags for model deployment." + ) class Config: extra = "allow" @@ -391,15 +406,9 @@ class Config: extra = "allow" -class CreateModelDeploymentDetails(BaseModel): - """Class for creating Aqua model deployments.""" +class ModelDeploymentSpec(BaseModel): + """Class for Aqua model deployments basic details.""" - instance_shape: str = Field( - ..., description="The instance shape used for deployment." - ) - display_name: str = Field(..., description="The name of the model deployment.") - compartment_id: Optional[str] = Field(None, description="The compartment OCID.") - project_id: Optional[str] = Field(None, description="The project OCID.") description: Optional[str] = Field( None, description="The description of the deployment." ) @@ -470,6 +479,20 @@ class CreateModelDeploymentDetails(BaseModel): None, description="Defined tags for model deployment." ) + class Config: + extra = "allow" + + +class ModelDeploymentCreateSpec(ModelDeploymentSpec): + """Class for creating Aqua model deployments.""" + + instance_shape: str = Field( + ..., description="The instance shape used for deployment." + ) + display_name: str = Field(..., description="The name of the model deployment.") + compartment_id: Optional[str] = Field(None, description="The compartment OCID.") + project_id: Optional[str] = Field(None, description="The project OCID.") + @model_validator(mode="before") @classmethod def validate(cls, values: Any) -> Any: @@ -651,3 +674,18 @@ def validate_multimodel_deployment_feasibility( class Config: extra = "allow" protected_namespaces = () + + +class ModelDeploymentUpdateSpec(ModelDeploymentSpec): + """Class for updating Aqua model deployments.""" + + deployment_id: str = Field( + ..., description="The id of model deployment to be updated." + ) + instance_shape: str = Field( + None, description="The instance shape used for deployment." + ) + display_name: str = Field(None, description="The name of the model deployment.") + + class Config: + extra = "allow" diff --git a/tests/unitary/with_extras/aqua/test_data/deployment/aqua_update_deployment.yaml b/tests/unitary/with_extras/aqua/test_data/deployment/aqua_update_deployment.yaml new file mode 100644 index 000000000..0c3b9e792 --- /dev/null +++ b/tests/unitary/with_extras/aqua/test_data/deployment/aqua_update_deployment.yaml @@ -0,0 +1,36 @@ +kind: deployment +spec: + createdBy: ocid1.user.oc1.. + displayName: model-deployment-name + freeformTags: + OCI_AQUA: active + aqua_model_name: model-name + id: "ocid1.datasciencemodeldeployment.oc1.." + infrastructure: + kind: infrastructure + spec: + bandwidthMbps: 10 + compartmentId: ocid1.compartment.oc1.. + deploymentType: SINGLE_MODEL + policyType: FIXED_SIZE + projectId: ocid1.datascienceproject.oc1.iad. + replica: 1 + shapeName: "VM.GPU.A10.1" + type: datascienceModelDeployment + lifecycleState: ACTIVE + modelDeploymentUrl: "https://modeldeployment.customer-oci.com/ocid1.datasciencemodeldeployment.oc1.." + runtime: + kind: runtime + spec: + env: + BASE_MODEL: service_models/model-name/artifact + MODEL_DEPLOY_ENABLE_STREAMING: 'true' + MODEL_DEPLOY_PREDICT_ENDPOINT: /v1/completions + PARAMS: --served-model-name odsc-llm --seed 42 --trust-remote-code --max-model-len 4096 + healthCheckPort: 8080 + image: "dsmc://image-name:1.0.0.0" + modelUri: "ocid1.datasciencemodeldeployment.oc1.." + serverPort: 8080 + type: container + timeCreated: 2024-01-01T00:00:00.000000+00:00 +type: modelDeployment diff --git a/tests/unitary/with_extras/aqua/test_deployment.py b/tests/unitary/with_extras/aqua/test_deployment.py index b0bc1d5fe..7f5738714 100644 --- a/tests/unitary/with_extras/aqua/test_deployment.py +++ b/tests/unitary/with_extras/aqua/test_deployment.py @@ -41,7 +41,7 @@ AquaDeploymentConfig, AquaDeploymentDetail, ConfigValidationError, - CreateModelDeploymentDetails, + ModelDeploymentCreateSpec, ModelDeploymentConfigSummary, ModelParams, ) @@ -551,6 +551,14 @@ class TestDataset: "name": "log-name", "url": "https://cloud.oracle.com/logging/search?searchQuery=search \"ocid1.compartment.oc1../ocid1.loggroup.oc1../ocid1.log.oc1..\" | source='ocid1.datasciencemodeldeployment.oc1..' | sort by datetime desc®ions=region-name", }, + "bandwidth_mbps": 10, + "inference_container": ":1.0.0.0", + "inference_mode": "/v1/completions", + "defined_tags": {}, + "freeform_tags": { + "OCI_AQUA": "active", + "aqua_model_name": "model-name", + }, } model_params = { @@ -1711,7 +1719,7 @@ def test_create_deployment_for_tei_byoc_embedding_model( @patch("ads.model.deployment.model_deployment.ModelDeployment.deploy") @patch("ads.aqua.modeldeployment.AquaDeploymentApp.get_deployment_config") @patch( - "ads.aqua.modeldeployment.entities.CreateModelDeploymentDetails.validate_multimodel_deployment_feasibility" + "ads.aqua.modeldeployment.entities.ModelDeploymentCreateSpec.validate_multimodel_deployment_feasibility" ) def test_create_deployment_for_multi_model( self, @@ -1831,6 +1839,76 @@ def test_create_deployment_for_multi_model( expected_result["state"] = "CREATING" assert actual_attributes == expected_result + @patch("ads.model.deployment.model_deployment.ModelDeployment.update") + @patch( + "ads.aqua.modeldeployment.AquaDeploymentApp._get_runtime_config_from_model_deployment" + ) + @patch.object(AquaApp, "get_container_config") + @patch("ads.model.datascience_model.DataScienceModel.from_id") + @patch("ads.model.deployment.model_deployment.ModelDeployment.from_id") + def test_update_single_model_deployment( + self, + mock_deployment, + mock_model, + mock_get_container_config, + mock_get_runtime_config_from_model_deployment, + mock_update, + ): + mock_get_container_config.return_value = ( + AquaContainerConfig.from_service_config( + service_containers=TestDataset.CONTAINER_LIST + ) + ) + freeform_tags = {"ftag1": "fvalue1", "ftag2": "fvalue2"} + defined_tags = {"dtag1": "dvalue1", "dtag2": "dvalue2"} + model_deployment_obj = ModelDeployment.from_yaml( + uri=os.path.join( + self.curr_dir, "test_data/deployment/aqua_update_deployment.yaml" + ) + ) + model_deployment_dsc_obj = copy.deepcopy(TestDataset.model_deployment_object[0]) + model_deployment_dsc_obj["lifecycle_state"] = "ACTIVE" + model_deployment_dsc_obj["defined_tags"] = defined_tags + model_deployment_dsc_obj["freeform_tags"].update(freeform_tags) + model_deployment_obj.dsc_model_deployment = ( + oci.data_science.models.ModelDeploymentSummary(**model_deployment_dsc_obj) + ) + + mock_deployment.return_value = model_deployment_obj + + mock_model.return_value = DataScienceModel.from_yaml( + uri=os.path.join( + self.curr_dir, "test_data/deployment/aqua_foundation_model.yaml" + ) + ) + + mock_get_runtime_config_from_model_deployment.return_value = ( + None, + None, + "dsmc://image-name:1.0.0.0", + "8080", + "8080", + { + "BASE_MODEL": "service_models/model-name/artifact", + "MODEL_DEPLOY_ENABLE_STREAMING": "true", + "MODEL_DEPLOY_PREDICT_ENDPOINT": "/v1/completions", + "PARAMS": "--served-model-name odsc-llm --seed 42", + }, + {**freeform_tags, **defined_tags}, + [], + ) + + mock_update.return_value = model_deployment_obj + + self.app.update( + **{ + "deployment_id": TestDataset.MODEL_DEPLOYMENT_ID, + "display_name": "updated single model deployment", + } + ) + + mock_update.assert_called_with(wait_for_completion=False) + @parameterized.expand( [ ( @@ -2041,7 +2119,7 @@ def validate_multimodel_deployment_feasibility_helper( for x in models ] - mock_create_deployment_details = CreateModelDeploymentDetails( + mock_create_deployment_details = ModelDeploymentCreateSpec( models=aqua_models, instance_shape=instance_shape, display_name=display_name, @@ -2049,7 +2127,7 @@ def validate_multimodel_deployment_feasibility_helper( ) else: model_id = "model_a" - mock_create_deployment_details = CreateModelDeploymentDetails( + mock_create_deployment_details = ModelDeploymentCreateSpec( model_id=model_id, instance_shape=instance_shape, display_name=display_name,