Skip to content

Commit 8c1f5b1

Browse files
author
Ziqun Ye
committed
adding implementation for artifactory option
1 parent d1e68e3 commit 8c1f5b1

File tree

6 files changed

+110
-52
lines changed

6 files changed

+110
-52
lines changed

ads/opctl/backend/ads_model_deployment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def watch(self) -> None:
175175

176176
def predict(self) -> None:
177177
ocid = self.config["execution"].get("ocid")
178-
data = self.config["execution"].get("data")
178+
data = self.config["execution"].get("payload")
179179
with AuthContext(auth=self.auth_type, profile=self.profile):
180180
model_deployment = ModelDeployment.from_id(ocid)
181181
data = json.loads(data)

ads/opctl/backend/local.py

Lines changed: 91 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import copy
88
import json
99
import os
10+
import shutil
1011
import tempfile
1112
from concurrent.futures import Future, ThreadPoolExecutor
1213
from time import sleep
@@ -18,13 +19,16 @@
1819
from ads.common.decorator.runtime_dependency import (OptionalDependency,
1920
runtime_dependency)
2021
from ads.common.oci_client import OCIClientFactory
21-
from ads.model.model_metadata import ModelCustomMetadata, ModelTaxonomyMetadata
22+
from ads.model.datascience_model import DataScienceModel
23+
from ads.model.model_metadata import ModelCustomMetadata
2224
from ads.opctl import logger
2325
from ads.opctl.backend.base import Backend
26+
from ads.opctl.conda.cmds import _install
2427
from ads.opctl.config.resolver import ConfigResolver
2528
from ads.opctl.constants import (DEFAULT_IMAGE_CONDA_DIR,
2629
DEFAULT_IMAGE_HOME_DIR,
2730
DEFAULT_IMAGE_SCRIPT_DIR,
31+
DEFAULT_MODEL_FOLDER,
2832
DEFAULT_NOTEBOOK_SESSION_CONDA_DIR,
2933
DEFAULT_NOTEBOOK_SESSION_SPARK_CONF_DIR,
3034
ML_JOB_GPU_IMAGE, ML_JOB_IMAGE)
@@ -35,9 +39,7 @@
3539
is_in_notebook_session, run_command,
3640
run_container)
3741
from ads.pipeline.ads_pipeline import Pipeline, PipelineStep
38-
from ads.model.datascience_model import DataScienceModel
39-
40-
DEFAULT_MODEL_FOLDER = "~/.ads_ops/models"
42+
from ads.model.runtime.runtime_info import RuntimeInfo
4143

4244

4345
class CondaPackNotFound(Exception):
@@ -55,6 +57,17 @@ def __init__(self, config: Dict) -> None:
5557
dictionary of configurations
5658
"""
5759
self.config = config
60+
self.auth_type = config["execution"].get("auth")
61+
self.profile = config["execution"].get("oci_profile", None)
62+
self.oci_config = config["execution"].get("oci_config", None)
63+
64+
self.oci_auth = create_signer(
65+
self.auth_type,
66+
self.oci_config,
67+
self.profile ,
68+
)
69+
70+
self.client = OCIClientFactory(**self.oci_auth).data_science
5871

5972
def run(self):
6073
if self.config.get("version") == "v1.0":
@@ -179,15 +192,15 @@ def init_vscode_container(self) -> None:
179192
f.write(json.dumps(dev_container, indent=2))
180193
print(f"File {os.path.abspath('.devcontainer.json')} created.")
181194

182-
def _run_with_conda_pack(self, bind_volumes: Dict, extra_cmd: str="") -> int:
195+
def _run_with_conda_pack(self, bind_volumes: Dict, extra_cmd: str="", install: bool=False, conda_uri: str="") -> int:
183196
env_vars = self.config["execution"].get("env_vars", {})
184197
slug = self.config["execution"]["conda_slug"]
185198
image = self.config["execution"].get("image", None)
186199

187200
# bind_volumes is modified in-place and does not need to be returned
188201
# it is returned just to be explicit that it is changed during this function call
189202
bind_volumes, env_vars = self._check_conda_pack_and_install_if_applicable(
190-
slug, bind_volumes, env_vars
203+
slug, bind_volumes, env_vars, install=install, conda_uri=conda_uri
191204
)
192205
bind_volumes = self._mount_source_folder_if_exists(bind_volumes)
193206
command = self._build_command_for_conda_run(extra_cmd)
@@ -274,6 +287,7 @@ def _run_with_image(self, bind_volumes: Dict) -> int:
274287
if self.config["execution"].get("source_folder", None):
275288
bind_volumes.update(self._mount_source_folder_if_exists(bind_volumes))
276289
bind_volumes.update(self.config["execution"]["volumes"])
290+
277291
return run_container(image, bind_volumes, env_vars, command, entrypoint)
278292

279293
def _run_with_image_v1(self, bind_volumes: Dict) -> int:
@@ -296,15 +310,26 @@ def _run_with_image_v1(self, bind_volumes: Dict) -> int:
296310
)
297311

298312
def _check_conda_pack_and_install_if_applicable(
299-
self, slug: str, bind_volumes: Dict, env_vars: Dict
313+
self, slug: str, bind_volumes: Dict, env_vars: Dict, install: bool=False, conda_uri: str = None
300314
) -> Dict:
315+
conda_pack_folder = os.path.abspath(os.path.expanduser(self.config['execution']["conda_pack_folder"]))
301316
conda_pack_path = os.path.join(
302-
os.path.expanduser(self.config["execution"]["conda_pack_folder"]), slug
317+
conda_pack_folder, slug
303318
)
304319
if not os.path.exists(conda_pack_path):
305-
raise CondaPackNotFound(
306-
f"Conda pack {conda_pack_path} not found. Please run `ads opctl conda create` or `ads opctl conda install`."
307-
)
320+
if install:
321+
logger.info(f"Downloading the conda pack {slug} to this conda pack {conda_pack_folder}. If this conda pack is already installed locally in a different location, pass in `conda_pack_folder` to avoid downloading it again.")
322+
_install(
323+
conda_uri=conda_uri,
324+
conda_pack_folder=conda_pack_folder,
325+
oci_config=self.oci_config,
326+
oci_profile=self.profile,
327+
auth_type=self.auth_type,
328+
)
329+
else:
330+
raise CondaPackNotFound(
331+
f"Conda pack {conda_pack_path} not found. Please run `ads opctl conda create` or `ads opctl conda install`."
332+
)
308333
if os.path.exists(os.path.join(conda_pack_path, "spark-defaults.conf")):
309334
if not is_in_notebook_session():
310335
env_vars["SPARK_CONF_DIR"] = os.path.join(DEFAULT_IMAGE_CONDA_DIR, slug)
@@ -617,26 +642,35 @@ def _log_orchestration_message(self, str: str) -> None:
617642
class LocalModelDeploymentBackend(LocalBackend):
618643
def __init__(self, config: Dict) -> None:
619644
super().__init__(config)
620-
self.oci_auth = create_signer(
621-
config["execution"].get("auth"),
622-
config["execution"].get("oci_config", None),
623-
config["execution"].get("oci_profile", None),
624-
)
625-
self.auth_type = config["execution"].get("auth")
626-
self.profile = config["execution"].get("oci_profile", None)
627-
self.client = OCIClientFactory(**self.oci_auth).data_science
628645

629646
def predict(self) -> None:
647+
artifact_directory = self.config["execution"].get("artifact_directory")
630648
ocid = self.config["execution"].get("ocid")
631-
data = self.config["execution"].get("data")
632-
model_folder = self.config["execution"].get("model_folder", DEFAULT_MODEL_FOLDER)
633-
conda_slug, conda_path = self._get_conda_info(ocid)
649+
data = self.config["execution"].get("payload")
650+
model_folder = os.path.expanduser(self.config["execution"].get("model_save_folder", DEFAULT_MODEL_FOLDER))
651+
artifact_directory = artifact_directory or os.path.join(model_folder, str(ocid))
652+
if ocid and (not os.path.exists(artifact_directory) or len(os.listdir(artifact_directory)) == 0):
653+
654+
region = self.config["execution"].get("region", None)
655+
bucket_uri = self.config["execution"].get("bucket_uri", None)
656+
timeout = self.config["execution"].get("timeout", None)
657+
logger.info(f"No cached model found. Downloading the model {ocid} to {artifact_directory}. If you already have a copy of the model, specify `artifact_directory` instead of `ocid`. You can specify `model_save_folder` to decide where to store the model artifacts.")
658+
self._download_model(ocid=ocid, artifact_directory=artifact_directory, region=region, bucket_uri=bucket_uri, timeout=timeout)
659+
660+
if ocid:
661+
conda_slug, conda_path = self._get_conda_info_from_catalog(ocid)
662+
elif artifact_directory:
663+
if not os.path.exists(artifact_directory) or len(os.listdir(artifact_directory)) == 0:
664+
raise ValueError(f"`artifact_directory` {artifact_directory} does not exist or is empty.")
665+
conda_slug, conda_path = self._get_conda_info_from_runtime(artifact_dir=artifact_directory)
666+
else:
667+
raise ValueError("Conda information cannot be detected.")
634668
compartment_id = self.config["execution"].get("compartment_id", self.config["infrastructure"].get("compartment_id"))
635669
project_id = self.config["execution"].get("project_id", self.config["infrastructure"].get("project_id"))
636670
if not compartment_id or not project_id:
637671
raise ValueError("`compartment_id` and `project_id` must be provided.")
638672

639-
extra_cmd = ocid + " " + data + " " + compartment_id + " " + project_id
673+
extra_cmd = "/opt/ds/model/deployed_model/ " + data + " " + compartment_id + " " + project_id
640674
bind_volumes = {}
641675
if not is_in_notebook_session():
642676
bind_volumes = {
@@ -647,17 +681,18 @@ def predict(self) -> None:
647681
dir_path = os.path.dirname(os.path.realpath(__file__))
648682
script = "script.py"
649683
self.config["execution"]["source_folder"] = os.path.abspath(os.path.join(dir_path, ".."))
650-
651684
self.config["execution"]["entrypoint"] = script
652-
bind_volumes[os.path.join(model_folder)] = {"bind": script}
685+
bind_volumes[artifact_directory] = {"bind": "/opt/ds/model/deployed_model/"}
686+
653687
if self.config["execution"].get("image"):
654688
exit_code = self._run_with_image(bind_volumes)
655689
elif self.config["execution"].get("conda_slug", conda_slug):
656690
self.config["execution"]["image"] = ML_JOB_IMAGE
657691
if not self.config["execution"].get("conda_slug"):
658692
self.config["execution"]["conda_slug"] = conda_slug
693+
self.config["execution"]["slug"] = conda_slug
659694
self.config["execution"]["conda_path"] = conda_path
660-
exit_code = self._run_with_conda_pack(bind_volumes, extra_cmd)
695+
exit_code = self._run_with_conda_pack(bind_volumes, extra_cmd, install=True, conda_uri=conda_path)
661696
else:
662697
raise ValueError("Either conda pack info or image should be specified.")
663698

@@ -667,25 +702,39 @@ def predict(self) -> None:
667702
f"Run with the --debug argument to view container logs."
668703
)
669704

670-
def _download_model(self, ocid, region, bucket_uri, timeout):
671-
dsc_model = DataScienceModel.from_id(ocid)
672-
dsc_model.download_artifact(
673-
target_dir=os.path.join(self.config["execution"].get("source_folder", DEFAULT_MODEL_FOLDER), ocid),
674-
force_overwrite=True,
675-
overwrite_existing_artifact=True,
676-
remove_existing_artifact=True,
677-
auth=self.oci_auth,
678-
region=region,
679-
timeout=timeout,
680-
bucket_uri=bucket_uri,
681-
)
682-
683-
def _get_conda_info(self, ocid):
705+
def _download_model(self, ocid, artifact_directory, region, bucket_uri, timeout):
706+
os.makedirs(artifact_directory, exist_ok=True)
707+
os.chmod(artifact_directory, 777)
708+
709+
try:
710+
dsc_model = DataScienceModel.from_id(ocid)
711+
dsc_model.download_artifact(
712+
target_dir=artifact_directory,
713+
force_overwrite=True,
714+
overwrite_existing_artifact=True,
715+
remove_existing_artifact=True,
716+
auth=self.oci_auth,
717+
region=region,
718+
timeout=timeout,
719+
bucket_uri=bucket_uri,
720+
)
721+
except:
722+
shutil.rmtree(artifact_directory, ignore_errors=True)
723+
724+
def _get_conda_info_from_catalog(self, ocid):
684725
response = self.client.get_model(ocid)
685726
custom_metadata = ModelCustomMetadata._from_oci_metadata(response.data.custom_metadata_list)
686-
conda_env_path = custom_metadata['CondaEnvironmentPath'].value
727+
conda_path = custom_metadata['CondaEnvironmentPath'].value
687728
conda_slug = custom_metadata['SlugName'].value
688-
return conda_slug, conda_env_path
729+
return conda_slug, conda_path
730+
731+
def _get_conda_info_from_runtime(self, artifact_dir):
732+
runtime_yaml_file = os.path.join(artifact_dir, "runtime.yaml")
733+
runtime_info = RuntimeInfo.from_yaml(uri=runtime_yaml_file)
734+
conda_slug = runtime_info.model_deployment.inference_conda_env.inference_env_slug
735+
conda_path = runtime_info.model_deployment.inference_conda_env.inference_env_path
736+
return conda_slug, conda_path
737+
689738

690739
def _run_with_image(self, bind_volumes):
691740
ocid = self.config["execution"].get("ocid")

ads/opctl/cli.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ads.opctl.utils import suppress_traceback
2929
from ads.opctl.config.merger import ConfigMerger
3030
from ads.opctl.constants import BACKEND_NAME
31+
from ads.opctl.backend.local import DEFAULT_MODEL_FOLDER
3132

3233
import ads.opctl.conda.cli
3334
import ads.opctl.spark.cli
@@ -498,19 +499,25 @@ def deactivate(**kwargs):
498499
Deactivates a data science service.
499500
"""
500501
suppress_traceback(kwargs["debug"])(deactivate_cmd)(**kwargs)
501-
502-
502+
503+
503504
@commands.command()
504-
@click.argument("ocid", nargs=1)
505-
@click.argument("data", nargs=1)
506-
@click.argument("conda_slug", nargs=1, required=False)
507-
@add_options(_options)
505+
@click.option("--ocid", nargs=1, required=False, help="This can be either a model id or model deployment id. When model id is passed, it conducts a local predict/test. This is designed for local dev purpose in order to test whether deployment will be successful locally. When you pass in `model_save_folder`, the model artifact will be downloaded and saved to a subdirectory of `model_save_folder` where model id is the name of subdirectory. Or you can pass in a model deployment id and this will invoke the remote endpoint and conduct a prediction on the server.")
506+
@click.option("--model-save-folder", nargs=1, required=False, default=DEFAULT_MODEL_FOLDER, help="Which location to store model artifact folders. Defaults to ~/.ads_ops/models. This is only used when model id is passed to `ocid` and a local predict is conducted.")
507+
@click.option("--conda-pack-folder", nargs=1, required=False, help="Which location to store the conda pack locally. Defaults to ~/.ads_ops/conda. This is only used when model id is passed to `ocid` and a local predict is conducted.")
508+
@click.option("--bucket-uri", nargs=1, required=False, help="The OCI Object Storage URI where model artifacts will be copied to. The `bucket_uri` is only necessary for uploading large artifacts which size is greater than 2GB. Example: `oci://<bucket_name>@<namespace>/prefix/`. This is only used when the model id is passed.")
509+
@click.option("--region", nargs=1, required=False, help="The destination Object Storage bucket region. By default the value will be extracted from the `OCI_REGION_METADATA` environment variables. This is only used when the model id is passed.")
510+
@click.option("--timeout", nargs=1, required=False, help="The connection timeout in seconds for the client. This is only used when the model id is passed.")
511+
@click.option("--artifact-directory", nargs=1, required=False, default=None, help="The artifact directory where stores your models, score.py and etc. This is used when you have a model artifact locally and have not saved it to the model catalog yet. In this case, you dont need to pass in model ")
512+
@click.option("--payload", nargs=1, help="The payload sent to the model for prediction.")
513+
@click.option("--conda-slug", nargs=1, required=False, help="The conda env used to load the model and conduct the prediction. This is only used when model id is passed to `ocid` and a local predict is conducted. It should match the inference conda env specified in the runtime.yaml file which is the conda pack being used when conducting real model deployment.")
514+
@click.option("--debug", "-d", help="set debug mode", is_flag=True, default=False)
508515
def predict(**kwargs):
509516
"""
510517
Deactivates a data science service.
511518
"""
512519
suppress_traceback(kwargs["debug"])(predict_cmd)(**kwargs)
513-
520+
514521

515522
commands.add_command(ads.opctl.conda.cli.commands)
516523
commands.add_command(ads.opctl.spark.cli.commands)

ads/opctl/cmds.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,8 @@ def predict(**kwargs) -> None:
488488
p = ConfigProcessor().step(ConfigMerger, **kwargs)
489489
if "datasciencemodeldeployment" in p.config["execution"].get("ocid", ""):
490490
return ModelDeploymentBackend(p.config).predict()
491-
elif "datasciencemodel" in p.config["execution"].get("ocid", ""):
491+
else:
492+
# model ocid or artifact directory
492493
return LocalModelDeploymentBackend(p.config).predict()
493494

494495

ads/opctl/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
DEFAULT_OCI_CONFIG_FILE = "~/.oci/config"
1010
DEFAULT_PROFILE = "DEFAULT"
1111
DEFAULT_CONDA_PACK_FOLDER = "~/conda"
12+
DEFAULT_MODEL_FOLDER = "~/.ads_ops/models"
1213
CONDA_PACK_OS_PREFIX_FORMAT = "oci://<bucket>@<namespace>/<prefix>"
1314
DEFAULT_ADS_CONFIG_FOLDER = "~/.ads_ops"
1415
OPS_IMAGE_BASE = "ads-operators-base"

ads/opctl/script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
def verify(artifact_dir, data, compartment_id, project_id):
99
with tempfile.TemporaryDirectory() as td:
10-
model = GenericModel.from_model_artifact(artifact_dir=artifact_dir,
10+
model = GenericModel.from_model_artifact(uri = artifact_dir, artifact_dir=artifact_dir,
1111
force_overwrite=True,
1212
compartment_id=compartment_id,
1313
project_id=project_id)

0 commit comments

Comments
 (0)