diff --git a/modelscope/hub/api.py b/modelscope/hub/api.py index f8ca683a..fd40abdf 100644 --- a/modelscope/hub/api.py +++ b/modelscope/hub/api.py @@ -12,6 +12,7 @@ from http.cookiejar import CookieJar from os.path import expanduser from typing import List, Optional, Tuple, Union +import attrs import requests from modelscope.hub.constants import (API_RESPONSE_FIELD_DATA, @@ -21,9 +22,14 @@ from modelscope.hub.constants import (API_RESPONSE_FIELD_DATA, API_RESPONSE_FIELD_USERNAME, DEFAULT_CREDENTIALS_PATH, Licenses, ModelVisibility) +from modelscope.hub.deploy import (DeleteServiceParameters, + DeployServiceParameters, + GetServiceParameters, ListServiceParameters, + ServiceParameters, ServiceResourceConfig, + Vendor) from modelscope.hub.errors import (InvalidParameter, NotExistError, - NotLoginException, RequestError, - datahub_raise_on_error, + NotLoginException, NotSupportError, + RequestError, datahub_raise_on_error, handle_http_post_error, handle_http_response, is_ok, raise_on_error) from modelscope.hub.git import GitCommandWrapper @@ -306,6 +312,169 @@ class HubApi: r.raise_for_status() return None + def deploy_model(self, model_id: str, revision: str, instance_name: str, + resource: ServiceResourceConfig, + provider: ServiceParameters): + """Deploy model to cloud, current we only support PAI EAS, this is asynchronous + call , please check instance status through the console or query the instance status. + At the same time, this call may take a long time. + + Args: + model_id (str): The deployed model id + revision (str): The model revision + instance_name (str): The deployed model instance name. + resource (DeployResource): The resource information. + provider (CreateParameter): The cloud service provider parameter + + Raises: + NotLoginException: To use this api, you need login first. + NotSupportError: Not supported platform. + RequestError: The server return error. + + Returns: + InstanceInfo: The instance information. + """ + cookies = ModelScopeConfig.get_cookies() + if cookies is None: + raise NotLoginException( + 'Token does not exist, please login first.') + if provider.vendor != Vendor.EAS: + raise NotSupportError( + 'Not support vendor: %s ,only support EAS current.' % + (provider.vendor)) + create_params = DeployServiceParameters( + instance_name=instance_name, + model_id=model_id, + revision=revision, + resource=resource, + provider=provider) + path = f'{self.endpoint}/api/v1/deployer/endpoint' + body = attrs.asdict(create_params) + r = requests.post( + path, + json=body, + cookies=cookies, + ) + handle_http_response(r, logger, cookies, 'create_eas_instance') + if r.status_code >= HTTPStatus.OK and r.status_code < HTTPStatus.MULTIPLE_CHOICES: + if is_ok(r.json()): + data = r.json()[API_RESPONSE_FIELD_DATA] + return data + else: + raise RequestError(r.json()[API_RESPONSE_FIELD_MESSAGE]) + else: + r.raise_for_status() + return None + + def list_deployed_model_instances(self, + provider: ServiceParameters, + skip: int = 0, + limit: int = 100): + """List deployed model instances. + + Args: + provider (ListServiceParameter): The cloud service provider parameter, + for eas, need access_key_id and access_key_secret. + skip: start of the list, current not support. + limit: maximum number of instances return, current not support + Raises: + NotLoginException: To use this api, you need login first. + RequestError: The request is failed from server. + + Returns: + List: List of instance information + """ + cookies = ModelScopeConfig.get_cookies() + if cookies is None: + raise NotLoginException( + 'Token does not exist, please login first.') + params = ListServiceParameters( + provider=provider, skip=skip, limit=limit) + path = '%s/api/v1/deployer/endpoint?%s' % (self.endpoint, + params.to_query_str()) + r = requests.get(path, cookies=cookies) + handle_http_response(r, logger, cookies, 'list_deployed_model') + if r.status_code == HTTPStatus.OK: + if is_ok(r.json()): + data = r.json()[API_RESPONSE_FIELD_DATA] + return data + else: + raise RequestError(r.json()[API_RESPONSE_FIELD_MESSAGE]) + else: + r.raise_for_status() + return None + + def get_deployed_model_instance(self, instance_name: str, + provider: ServiceParameters): + """Query the specified instance information. + + Args: + instance_name (str): The deployed instance name. + provider (GetParameter): The cloud provider information, for eas + need region(eg: ch-hangzhou), access_key_id and access_key_secret. + + Raises: + NotLoginException: To use this api, you need login first. + RequestError: The request is failed from server. + + Returns: + Dict: The request instance information + """ + cookies = ModelScopeConfig.get_cookies() + if cookies is None: + raise NotLoginException( + 'Token does not exist, please login first.') + params = GetServiceParameters(provider=provider) + path = '%s/api/v1/deployer/endpoint/%s?%s' % ( + self.endpoint, instance_name, params.to_query_str()) + r = requests.get(path, cookies=cookies) + handle_http_response(r, logger, cookies, 'get_deployed_model') + if r.status_code == HTTPStatus.OK: + if is_ok(r.json()): + data = r.json()[API_RESPONSE_FIELD_DATA] + return data + else: + raise RequestError(r.json()[API_RESPONSE_FIELD_MESSAGE]) + else: + r.raise_for_status() + return None + + def delete_deployed_model_instance(self, instance_name: str, + provider: ServiceParameters): + """Delete deployed model, this api send delete command and return, it will take + some to delete, please check through the cloud console. + + Args: + instance_name (str): The instance name you want to delete. + provider (DeleteParameter): The cloud provider information, for eas + need region(eg: ch-hangzhou), access_key_id and access_key_secret. + + Raises: + NotLoginException: To call this api, you need login first. + RequestError: The request is failed. + + Returns: + Dict: The deleted instance information. + """ + cookies = ModelScopeConfig.get_cookies() + if cookies is None: + raise NotLoginException( + 'Token does not exist, please login first.') + params = DeleteServiceParameters(provider=provider) + path = '%s/api/v1/deployer/endpoint/%s?%s' % ( + self.endpoint, instance_name, params.to_query_str()) + r = requests.delete(path, cookies=cookies) + handle_http_response(r, logger, cookies, 'delete_deployed_model') + if r.status_code == HTTPStatus.OK: + if is_ok(r.json()): + data = r.json()[API_RESPONSE_FIELD_DATA] + return data + else: + raise RequestError(r.json()[API_RESPONSE_FIELD_MESSAGE]) + else: + r.raise_for_status() + return None + def _check_cookie(self, use_cookies: Union[bool, CookieJar] = False) -> CookieJar: diff --git a/modelscope/hub/deploy.py b/modelscope/hub/deploy.py new file mode 100644 index 00000000..64594e0d --- /dev/null +++ b/modelscope/hub/deploy.py @@ -0,0 +1,189 @@ +import urllib +from abc import ABC, abstractmethod +from typing import Optional, Union + +import json +from attr import fields +from attrs import asdict, define, field, validators + + +class Accelerator(object): + CPU = 'cpu' + GPU = 'gpu' + + +class Vendor(object): + EAS = 'eas' + + +class EASRegion(object): + beijing = 'cn-beijing' + hangzhou = 'cn-hangzhou' + + +class EASCpuInstanceType(object): + """EAS Cpu Instance TYpe, ref(https://help.aliyun.com/document_detail/144261.html) + """ + tiny = 'ecs.c6.2xlarge' + small = 'ecs.c6.4xlarge' + medium = 'ecs.c6.6xlarge' + large = 'ecs.c6.8xlarge' + + +class EASGpuInstanceType(object): + """EAS Cpu Instance TYpe, ref(https://help.aliyun.com/document_detail/144261.html) + """ + tiny = 'ecs.gn5-c28g1.7xlarge' + small = 'ecs.gn5-c8g1.4xlarge' + medium = 'ecs.gn6i-c24g1.12xlarge' + large = 'ecs.gn6e-c12g1.3xlarge' + + +def min_smaller_than_max(instance, attribute, value): + if value > instance.max_replica: + raise ValueError( + "'min_replica' value: %s has to be smaller than 'max_replica' value: %s!" + % (value, instance.max_replica)) + + +@define +class ServiceScalingConfig(object): + """Resource scaling config + Currently we ignore max_replica + Args: + max_replica: maximum replica + min_replica: minimum replica + """ + max_replica: int = field(default=1, validator=validators.ge(1)) + min_replica: int = field( + default=1, validator=[validators.ge(1), min_smaller_than_max]) + + +@define +class ServiceResourceConfig(object): + """Eas Resource request. + + Args: + accelerator: the accelerator(cpu|gpu) + instance_type: the instance type. + scaling: The instance scaling config. + """ + instance_type: str + scaling: ServiceScalingConfig + accelerator: str = field( + default=Accelerator.CPU, + validator=validators.in_([Accelerator.CPU, Accelerator.GPU])) + + +@define +class ServiceParameters(ABC): + pass + + +@define +class EASDeployParameters(ServiceParameters): + """Parameters for EAS Deployment. + + Args: + resource_group: the resource group to deploy, current default. + region: The eas instance region(eg: cn-hangzhou). + access_key_id: The eas account access key id. + access_key_secret: The eas account access key secret. + vendor: must be 'eas' + """ + region: str + access_key_id: str + access_key_secret: str + resource_group: Optional[str] = None + vendor: str = field( + default=Vendor.EAS, validator=validators.in_([Vendor.EAS])) + """ + def __init__(self, + instance_name: str, + access_key_id: str, + access_key_secret: str, + region = EASRegion.beijing, + instance_type: str = EASCpuInstances.small, + accelerator: str = Accelerator.CPU, + resource_group: Optional[str] = None, + scaling: Optional[str] = None): + self.instance_name=instance_name + self.access_key_id=self.access_key_id + self.access_key_secret = access_key_secret + self.region = region + self.instance_type = instance_type + self.accelerator = accelerator + self.resource_group = resource_group + self.scaling = scaling + """ + + +@define +class EASListParameters(ServiceParameters): + """EAS instance list parameters. + + Args: + resource_group: the resource group to deploy, current default. + region: The eas instance region(eg: cn-hangzhou). + access_key_id: The eas account access key id. + access_key_secret: The eas account access key secret. + vendor: must be 'eas' + """ + access_key_id: str + access_key_secret: str + region: str = None + resource_group: str = None + vendor: str = field( + default=Vendor.EAS, validator=validators.in_([Vendor.EAS])) + + +@define +class DeployServiceParameters(object): + """Deploy service parameters + + Args: + instance_name: the name of the service. + model_id: the modelscope model_id + revision: the modelscope model revision + resource: the resource requirement. + provider: the cloud service provider. + """ + instance_name: str + model_id: str + revision: str + resource: ServiceResourceConfig + provider: ServiceParameters + + +class AttrsToQueryString(ABC): + """Convert the attrs class to json string. + + Args: + """ + + def to_query_str(self): + self_dict = asdict( + self.provider, filter=lambda attr, value: value is not None) + json_str = json.dumps(self_dict) + print(json_str) + safe_str = urllib.parse.quote_plus(json_str) + print(safe_str) + query_param = 'provider=%s' % safe_str + return query_param + + +@define +class ListServiceParameters(AttrsToQueryString): + provider: ServiceParameters + skip: int = 0 + limit: int = 100 + + +@define +class GetServiceParameters(AttrsToQueryString): + provider: ServiceParameters + + +@define +class DeleteServiceParameters(AttrsToQueryString): + provider: ServiceParameters diff --git a/modelscope/hub/errors.py b/modelscope/hub/errors.py index bd7a20ac..47994bc6 100644 --- a/modelscope/hub/errors.py +++ b/modelscope/hub/errors.py @@ -9,6 +9,10 @@ from modelscope.utils.logger import get_logger logger = get_logger() +class NotSupportError(Exception): + pass + + class NotExistError(Exception): pass @@ -66,6 +70,7 @@ def handle_http_response(response, logger, cookies, model_id): logger.error( f'Authentication token does not exist, failed to access model {model_id} which may not exist or may be \ private. Please login first.') + logger.error('Response details: %s' % response.content) raise error diff --git a/requirements/framework.txt b/requirements/framework.txt index b51faeda..2408cda6 100644 --- a/requirements/framework.txt +++ b/requirements/framework.txt @@ -1,4 +1,5 @@ addict +attrs datasets easydict einops