From ed29a762b5e6fd484edf05d65a4cca059462a573 Mon Sep 17 00:00:00 2001 From: Googol2002 Date: Mon, 27 Nov 2023 10:46:06 +0800 Subject: [PATCH 01/64] [DOC] Update RKME Image in spec.rst --- docs/components/spec.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/components/spec.rst b/docs/components/spec.rst index ed07c7a..f04733d 100644 --- a/docs/components/spec.rst +++ b/docs/components/spec.rst @@ -80,9 +80,17 @@ Table Specification Image Specification -------------------------- +Image data lives in a higher dimensional space than other data types. Unlike lower dimensional spaces, metrics defined based on Euclidean distances (or similar distances) will fail in higher dimensional spaces. This means that measuring the similarity between image samples becomes difficult. + +To address these issues, we use the Neural Tangent Kernel (NTK) based on Convolutional Neural Networks (CNN) to measure the similarity of image samples. As we all know, CNN has greatly advanced the field of computer vision and is still a mainstream deep learning technique. + +Specifically, in the process of constructing the specification, we approximate the Neural Network Gaussian Process (NNGP) kernel with neural networks of finite width; in the process of calculating the similarity of the specification, we use an approximation algorithm to compute the NNGP kernel directly. + +In the calculation of NTK, the NNGP kernel serves as the dominant term and can be used as a good approximation of NTK. The use of NNGP approximation instead of NTK improves the construction and search efficiency of Image Specificatin. + Text Specification -------------------------- System Specification -====================================== \ No newline at end of file +====================================== From c23f0cebc2c09157878b74f41afb70a15abe0de0 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 27 Nov 2023 21:29:12 +0800 Subject: [PATCH 02/64] [ENH] add License in semantic spec --- learnware/client/learnware_client.py | 29 +++++++++++++++++---------- learnware/config.py | 23 +++++++++++++++++++++ learnware/market/easy/database_ops.py | 5 +++++ learnware/market/easy/searcher.py | 18 ++++++++--------- learnware/tests/module.py | 1 + 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index f3cbe61..5b24602 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -54,6 +54,7 @@ class SemanticSpecificationKey(Enum): DATA_TYPE = "Data" TASK_TYPE = "Task" LIBRARY_TYPE = "Library" + LICENSE = "License" SENARIOES = "Scenario" @@ -247,7 +248,7 @@ class LearnwareClient: for learnware in result["data"]["learnware_list_single"]: returns.append( - { + { "type": "single", "learnware_id": learnware["learnware_id"], "semantic_specification": learnware["semantic_specification"], @@ -259,12 +260,12 @@ class LearnwareClient: "type": "multiple", "learnware_ids": [], "semantic_specifications": [], - "matching": result["data"]["learnware_list_multi"][0]["matching"] + "matching": result["data"]["learnware_list_multi"][0]["matching"], } for learnware in result["data"]["learnware_list_multi"]: multiple_learnware["learnware_ids"].append(learnware["learnware_id"]) multiple_learnware["semantic_specifications"].append(learnware["semantic_specification"]) - + returns.append(multiple_learnware) return returns @@ -280,14 +281,15 @@ class LearnwareClient: def create_semantic_specification( self, - name=None, - description=None, - data_type=None, - task_type=None, - library_type=None, - scenarios=None, - input_description=None, - output_description=None, + name: str = None, + description: str = None, + data_type: str = None, + task_type: str = None, + library_type: str = None, + scenarios: Union[str, List(str)] = None, + license: Union[str, List(str)] = None, + input_description: dict = None, + output_description: dict = None, ): semantic_specification = dict() semantic_specification["Data"] = {"Type": "Class", "Values": [data_type] if data_type is not None else []} @@ -296,7 +298,12 @@ class LearnwareClient: "Type": "Class", "Values": [library_type] if library_type is not None else [], } + + license = [license] if isinstance(license, str) else license + semantic_specification["License"] = {"Type": "Class", "Values": license if license is not None else []} + scenarios = [scenarios] if isinstance(scenarios, str) else scenarios semantic_specification["Scenario"] = {"Type": "Tag", "Values": scenarios if scenarios is not None else []} + semantic_specification["Name"] = {"Type": "String", "Values": name if name is not None else ""} semantic_specification["Description"] = { "Type": "String", diff --git a/learnware/config.py b/learnware/config.py index 02f3bcb..7fa89a4 100644 --- a/learnware/config.py +++ b/learnware/config.py @@ -3,6 +3,7 @@ import copy import logging from enum import Enum + class Config: def __init__(self, default_conf): self.__dict__["_default_config"] = copy.deepcopy(default_conf) # avoiding conflictions with __getattr__ @@ -53,8 +54,10 @@ class SystemType(Enum): MACOS = 1 WINDOWS = 2 + def get_platform(): import platform + os_name = platform.system().lower() if "macos" in os_name or "darwin" in os_name: return SystemType.MACOS @@ -64,6 +67,7 @@ def get_platform(): return SystemType.LINUX raise SystemError("Learnware only support MACOS/Linux/Windows") + if get_platform() == SystemType.MACOS: ROOT_DIRPATH = os.path.join(os.path.expanduser("~"), "Library", "Learnware") else: @@ -124,6 +128,25 @@ semantic_config = { "Values": None, "Type": "String", }, + "License": { + "Values": [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0", + "LGPL-2.1", + "LGPL-3.0", + "AGPL-3.0", + "ECL-2.0", + "AFL-3.0", + "CC-BY-4.0", + "CC-BY-SA-4.0", + "Others", + ], + "Type": "Class", + }, } _DEFAULT_CONFIG = { diff --git a/learnware/market/easy/database_ops.py b/learnware/market/easy/database_ops.py index 01bb48d..7f8e87c 100644 --- a/learnware/market/easy/database_ops.py +++ b/learnware/market/easy/database_ops.py @@ -177,6 +177,11 @@ class DatabaseOperations(object): id = id.strip() try: semantic_spec_dict = json.loads(semantic_spec) + if "License" not in semantic_spec_dict: + semantic_spec_dict["License"] = { + "Values": ["Apache-2.0"], + "Type": "Class", + } new_learnware = get_learnware_from_dirpath( id=id, semantic_spec=semantic_spec_dict, learnware_dirpath=folder_path, ignore_error=False ) diff --git a/learnware/market/easy/searcher.py b/learnware/market/easy/searcher.py index 99e1e2d..c0a41e9 100644 --- a/learnware/market/easy/searcher.py +++ b/learnware/market/easy/searcher.py @@ -45,11 +45,9 @@ class EasyExactSemanticSearcher(BaseSearcher): return False if semantic_spec1[key]["Type"] == "Class": - if isinstance(v1, list): - v1 = v1[0] if isinstance(v2, list): v2 = v2[0] - if v1 != v2: + if v2 not in v1: return False elif semantic_spec1[key]["Type"] == "Tag": if not (set(v1) & set(v2)): @@ -67,6 +65,7 @@ class EasyExactSemanticSearcher(BaseSearcher): logger.info("semantic_spec search: choose %d from %d learnwares" % (len(match_learnwares), len(learnware_list))) return SearchResults(single_results=[SingleSearchItem(learnware=_learnware) for _learnware in match_learnwares]) + class EasyFuzzSemanticSearcher(BaseSearcher): def _match_semantic_spec_tag(self, semantic_spec1, semantic_spec2) -> bool: """Judge if tags of two semantic specs are consistent @@ -97,11 +96,9 @@ class EasyFuzzSemanticSearcher(BaseSearcher): return False if semantic_spec1[key]["Type"] == "Class": - if isinstance(v1, list): - v1 = v1[0] if isinstance(v2, list): v2 = v2[0] - if v1 != v2: + if v2 not in v1: return False elif semantic_spec1[key]["Type"] == "Tag": if not (set(v1) & set(v2)): @@ -622,13 +619,16 @@ class EasyStatSearcher(BaseSearcher): logger.info(f"After filter by rkme spec, learnware_list length is {len(learnware_list)}") search_results = SearchResults() - + search_results.update_single_results( - [SingleSearchItem(learnware=_learnware, score=_score) for _score, _learnware in zip(sorted_score_list, single_learnware_list)] + [ + SingleSearchItem(learnware=_learnware, score=_score) + for _score, _learnware in zip(sorted_score_list, single_learnware_list) + ] ) if mixture_score is not None and len(mixture_learnware_list) > 0: search_results.update_multiple_results( - [MultipleSearchItem(learnwares=mixture_learnware_list, score=mixture_score)] + [MultipleSearchItem(learnwares=mixture_learnware_list, score=mixture_score)] ) return search_results diff --git a/learnware/tests/module.py b/learnware/tests/module.py index eedf331..52300a6 100644 --- a/learnware/tests/module.py +++ b/learnware/tests/module.py @@ -4,6 +4,7 @@ def get_semantic_specification(): semantic_specification["Task"] = {"Type": "Class", "Values": ["Segmentation"]} semantic_specification["Library"] = {"Type": "Class", "Values": ["Scikit-learn"]} semantic_specification["Scenario"] = {"Type": "Tag", "Values": ["Financial"]} + semantic_specification["License"] = {"Type": "Class", "Values": ["Apache-2.0"]} semantic_specification["Name"] = {"Type": "String", "Values": "test"} semantic_specification["Description"] = {"Type": "String", "Values": "test"} return semantic_specification From 127c5642222cd7d42ce42302f33345dc899c8e83 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 27 Nov 2023 21:36:42 +0800 Subject: [PATCH 03/64] [FIX] fix bugs in EasyFuzzSemanticSearcher --- learnware/market/easy/searcher.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/learnware/market/easy/searcher.py b/learnware/market/easy/searcher.py index c0a41e9..48bbfa8 100644 --- a/learnware/market/easy/searcher.py +++ b/learnware/market/easy/searcher.py @@ -84,13 +84,11 @@ class EasyFuzzSemanticSearcher(BaseSearcher): """ for key in semantic_spec1.keys(): v1 = semantic_spec1[key].get("Values", "") - v2 = semantic_spec2[key].get("Values", "") - - if len(v1) == 0: - # user input is empty, no need to search + if key not in semantic_spec2 or len(v1) == 0: continue - if key not in "Name": + v2 = semantic_spec2[key].get("Values", "") + if key not in ("Name", "Description"): if len(v2) == 0: # user input contains some key that is not in database return False From a96efe0cfe9655fbdbbe282fd0d6a2feb09e622f Mon Sep 17 00:00:00 2001 From: Asymptotez <201220101@smail.nju.edu.cn> Date: Tue, 28 Nov 2023 10:21:50 +0800 Subject: [PATCH 04/64] [DOC] rewrite the client.rst document --- docs/workflows/client.rst | 192 +++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 85 deletions(-) diff --git a/docs/workflows/client.rst b/docs/workflows/client.rst index 5f0ea5b..cfdfb02 100644 --- a/docs/workflows/client.rst +++ b/docs/workflows/client.rst @@ -6,13 +6,13 @@ Learnware Client Introduction ==================== -``Learnware Client`` is a python api that provides a convenient interface for interacting with the official market. You can easily use the client to upload, download and search learnwares. +``Learnware Client`` is a python api that provides a convenient interface for interacting with the system. You can easily use the client to upload, download and search learnwares. Prepare access token ==================== -Before using the ``Learnware Client``, you'll need to obtain a token from the `official website `_. Just login to the website and click "client token" tab in the user center. +Before using the ``Learnware Client``, you'll need to obtain a token from the `official website `_. Just login to the website and click ``Client Token`` tab in the user center. How to Use Client @@ -25,142 +25,164 @@ Initialize a Learware Client .. code-block:: python - import learnware - from learnware.client import LearnwareClient + from learnware.client import LearnwareClient, SemanticSpecificationKey + # Login to Beiming system client = LearnwareClient() - - # login to official market client.login(email="your email", token="your token") +Where email is the registered mailbox of the system and token is the token obtained in the previous section. Upload Leanware ------------------------------- -Before uploading a learnware, you'll need to prepare the semantic specification of your learnware. You can create a semantic specification by a helper function ``create_semantic_specification``. +Before uploading a learnware, you'll need to prepare the semantic specification of your learnware. Let's take the classification task for tabular data as an example. You can create a semantic specification by a helper function ``create_semantic_specification``. .. code-block:: python + # Prepare input description when data_type="Table" input_description = { - "Dimension": 16, - { - "Description": { - "0": "gender", - "1": "age", - "2": "f2", - "5": "f5" - } - } + "Dimension": 5, + "Description": { + "0": "age", + "1": "weight", + "2": "body length", + "3": "animal type", + "4": "claw length" + }, } + + # Prepare output description when task_type in ["Classification", "Regression"] output_description = { "Dimension": 3, "Description": { - "0": "the probability of being a cat", - "1": "the probability of being a dog", - "2": "the probability of being a bird" - } + "0": "cat", + "1": "dog", + "2": "bird", + }, } + + # Create semantic specification semantic_spec = client.create_semantic_specification( - name="mylearnware1", - description="this is my learnware", - data_type="Table", - task_type="Classification", - library_type="Scikit-learn", - senarioes=["Business", "Financial"], - input_description, output_description) - # data_type, task_type, library_type, senarioes are enums, you can find possible values in `learnware.C` + name="learnware_example", + description="Just a example for uploading a learnware", + data_type="Table", + task_type="Classification", + library_type="Scikit-learn", + scenarios=["Business", "Financial"], + input_description=input_description, + output_description=output_description, + ) +Make sure that the parameter input for the semantic specification is within the range given by ``client.list_semantic_specification_values(key)`` : + +* data_type must in ``key=SemanticSpecificationKey.DATA_TYPE``; +* task_type must in ``key=SemanticSpecificationKey.TASK_TYPE``; +* library_type must in ``key=SemanticSpecificationKey.LIBRARY_TYPE``; +* scenarios must be a subset of ``key=SemanticSpecificationKey.SENARIOES``; +* When data_type is ``"Table"``, input description needs to be filled in; +* When task_type is in ``["Classification", "Regression"]``, output description needs to be filled. + +Finally, the semantic specification and the zip package path of the learnware were filled in to upload the learnware. + +Remember to verify the learnware before uploading it, as shown in the following code example: -After defining the semantic specification, -you can upload your learnware using ``upload_learnware`` function: - .. code-block:: python - + + # Prepare your learnware zip file + zip_path = "your learnware zip" + + # Check your learnware before upload + client.check_learnware( + learnware_zip_path=zip_path, semantic_specification=semantic_spec + ) + + # Upload your learnware learnware_id = client.upload_learnware( - semantic_spec=semantic_spec, - zip_path="path to your learnware zipfile") + learnware_zip_path=zip_path, semantic_specification=semantic_spec + ) -Here, ``zip_path`` is the local path of your learnware zipfile. +After uploading the learnware successfully, you can see it in ``My Learnware``, the background will check it. Click on the learnware, which can be viewed in the ``Verify Status``. After the check passes, the Unverified tag of the learnware will disappear, and the uploaded learnware will appear in the system. Semantic Specification Search ------------------------------- -You can search learnwares in official market using semantic specification. All the learnwares that match the semantic specification will be returned by the api. For example, the code below searches learnwares with `Table` data type: +You can search the learnware in the system through the semantic specification, and all the learnware conforming to the semantic specification will be returned through the API. For example, the following code will give you all the learnware in the system whose task type is classified: .. code-block:: python - semantic_spec = client.create_semantic_specification( - name="", - description="", - data_type="Table", - task_type="", - library_type="", - senarioes=[], - input_description={}, output_description={}) - - specification = learnware.specification.Specification() - specification.update_semantic_spec(specification) - learnware_list = client.search_learnware(specification) + from learnware.market import BaseUserInfo + + user_semantic = client.create_semantic_specification( + task_type="Classification" + ) + user_info = BaseUserInfo(semantic_spec=user_semantic) + learnware_list = client.search_learnware(user_info, page_size=None) Statistical Specification Search --------------------------------- -You can search learnware by providing a statistical specification. The statistical specification is a json file that contains the statistical information of your training data. For example, the code below searches learnwares with `RKMETableSpecification`: +You can also search the learnware in the system through the statistical specification, and all the learnware with similar distribution will be returned through the API. Using the ``generate_stat_spec`` function mentioned above, you can easily get the ``stat_spec`` for your current task, and then get the learnware that meets the statistical specification for the same type of data in the system by using the following code: .. code-block:: python - import learnware.specification as specification - - user_spec = specification.RKMETableSpecification() - user_spec.load(os.path.join(unzip_path, "rkme.json")) - - specification = learnware.specification.Specification() - specification.update_stat_spec(user_spec) - - learnware_list = client.search_learnware(specification) - - # you can view the scores of the searched learnwares - for learnware in learnware_list: - print(f'learnware_id: {learnware["learnware_id"]}, score: {learnware["matching"]}') + user_info = BaseUserInfo(stat_info={stat_spec.type: stat_spec}) + learnware_list = client.search_learnware(user_info, page_size=None) Combine Semantic and Statistical Search ---------------------------------------- -You can provide both semantic and statistical specification to search learnwares. The engine will first filter learnwares by semantic specification and then search by statistical specification. For example, the code below searches learnwares with `Table` data type and `RKMETableSpecification`: +By combining statistical and semantic specifications, you can perform more detailed searches, such as the following code that searches tabular data for pieces of learnware that satisfy your semantic specifications: .. code-block:: python - semantic_spec = client.create_semantic_specification( - name="", - description="", - data_type="Table", - task_type="", - library_type="", - senarioes=[], - input_description={}, output_description={}) + user_semantic = client.create_semantic_specification( + task_type="Classification", + scenarios=["Business"], + ) + rkme_table = generate_stat_spec(type="table", X=train_x) + user_info = BaseUserInfo( + semantic_spec=user_semantic, stat_info={rkme_table.type: rkme_table} + ) + learnware_list = client.search_learnware(user_info, page_size=None) + +Heterogeneous Table Search +---------------------------------------- +When you provide a statistical specification for tabular data, the task type is "Classification" or "Regression", and your semantic specification includes descriptions for each dimension, the system will automatically enable heterogeneous table search. It won't only search in the tabular learnwares with same dimensions. The following code will perform heterogeneous table search through the API: - stat_spec = specification.RKMETableSpecification() - stat_spec.load(os.path.join(unzip_path, "rkme.json")) - specification = learnware.specification.Specification() - specification.update_semantic_spec(semantic_spec) - specification.update_stat_spec(stat_spec) +.. code-block:: python - learnware_list = client.search_learnware(specification) + input_description = { + "Dimension": 2, + "Description": { + "0": "leaf width", + "1": "leaf length", + }, + } + user_semantic = client.create_semantic_specification( + task_type="Classification", + scenarios=["Business"], + input_description=input_description, + ) + rkme_table = generate_stat_spec(type="table", X=train_x) + user_info = BaseUserInfo( + semantic_spec=user_semantic, stat_info={rkme_table.type: rkme_table} + ) + learnware_list = client.search_learnware(user_info) Download and Use Learnware ------------------------------- -When you get a learnware id, you can download and initiate the learnware with the following code: +When the search is complete, you can download the learnware and configure the environment through the following code: .. code-block:: python - client.download_learnware(learnware_id, zip_path) - client.install_environment(zip_path) - learnware = client.load_learnware(zip_path) - # you can use the learnware to make prediction now - - - + for temp_learnware in learnware_list: + learnware_id = temp_learnware["learnware_id"] + # you can use the learnware to make prediction now + learnware = client.load_learnware( + learnware_id=learnware_id, runnable_option="conda" + ) \ No newline at end of file From 061760f8fbe4b791ad151a4e7bbe30dcdece7f1d Mon Sep 17 00:00:00 2001 From: bxdd <45119470+bxdd@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:30:00 +0800 Subject: [PATCH 05/64] Update TypeHint in create_semantic_spec --- learnware/client/learnware_client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 5b24602..4491863 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -281,15 +281,15 @@ class LearnwareClient: def create_semantic_specification( self, - name: str = None, - description: str = None, - data_type: str = None, - task_type: str = None, - library_type: str = None, - scenarios: Union[str, List(str)] = None, - license: Union[str, List(str)] = None, - input_description: dict = None, - output_description: dict = None, + name: Optional[str] = None, + description: Optional[str] = None, + data_type: Optional[str] = None, + task_type: Optional[str] = None, + library_type: Optional[str] = None, + scenarios: Optional[Union[str, List(str)]] = None, + license: Optional[Union[str, List(str)]] = None, + input_description: Optional[dict] = None, + output_description: Optional[dict] = None, ): semantic_specification = dict() semantic_specification["Data"] = {"Type": "Class", "Values": [data_type] if data_type is not None else []} From 18640f9f0ea51a97876cde10f08b930e2febd927 Mon Sep 17 00:00:00 2001 From: Gene Date: Tue, 28 Nov 2023 16:31:56 +0800 Subject: [PATCH 06/64] [FIX] fix details --- learnware/client/learnware_client.py | 4 ++-- tests/test_workflow/test_workflow.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 5b24602..beb7bf9 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -286,8 +286,8 @@ class LearnwareClient: data_type: str = None, task_type: str = None, library_type: str = None, - scenarios: Union[str, List(str)] = None, - license: Union[str, List(str)] = None, + scenarios: Union[str, List[str]] = None, + license: Union[str, List[str]] = None, input_description: dict = None, output_description: dict = None, ): diff --git a/tests/test_workflow/test_workflow.py b/tests/test_workflow/test_workflow.py index 0702a13..83f39ba 100644 --- a/tests/test_workflow/test_workflow.py +++ b/tests/test_workflow/test_workflow.py @@ -27,6 +27,7 @@ user_semantic = { "Scenario": {"Values": ["Education"], "Type": "Tag"}, "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "", "Type": "String"}, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -145,11 +146,15 @@ class TestWorkflow(unittest.TestCase): user_info = BaseUserInfo(semantic_spec=semantic_spec) search_result = easy_market.search_learnware(user_info) single_result = search_result.get_single_results() - + print("User info:", user_info.get_semantic_spec()) print(f"Search result:") for search_item in single_result: - print("Choose learnware:", search_item.learnware.id, search_item.learnware.get_specification().get_semantic_spec()) + print( + "Choose learnware:", + search_item.learnware.id, + search_item.learnware.get_specification().get_semantic_spec(), + ) rmtree(test_folder) # rm -r test_folder @@ -176,12 +181,12 @@ class TestWorkflow(unittest.TestCase): single_result = search_results.get_single_results() multiple_result = search_results.get_multiple_results() - + assert len(single_result) >= 1, f"Statistical search failed!" print(f"search result of user{idx}:") for search_item in single_result: print(f"score: {search_item.score}, learnware_id: {search_item.learnware.id}") - + for mixture_item in multiple_result: print(f"mixture_score: {mixture_item.score}\n") mixture_id = " ".join([learnware.id for learnware in mixture_item.learnwares]) @@ -229,8 +234,8 @@ class TestWorkflow(unittest.TestCase): def suite(): _suite = unittest.TestSuite() - #_suite.addTest(TestWorkflow("test_prepare_learnware_randomly")) - #_suite.addTest(TestWorkflow("test_upload_delete_learnware")) + # _suite.addTest(TestWorkflow("test_prepare_learnware_randomly")) + # _suite.addTest(TestWorkflow("test_upload_delete_learnware")) _suite.addTest(TestWorkflow("test_search_semantics")) _suite.addTest(TestWorkflow("test_stat_search")) _suite.addTest(TestWorkflow("test_learnware_reuse")) From c35505aa5ae7cdacffb65fbd97c23013e932c4e9 Mon Sep 17 00:00:00 2001 From: bxdd <45119470+bxdd@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:35:50 +0800 Subject: [PATCH 07/64] [FIX] fix typehint in Client --- learnware/client/learnware_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 4491863..bb5e9a9 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -286,8 +286,8 @@ class LearnwareClient: data_type: Optional[str] = None, task_type: Optional[str] = None, library_type: Optional[str] = None, - scenarios: Optional[Union[str, List(str)]] = None, - license: Optional[Union[str, List(str)]] = None, + scenarios: Optional[Union[str, List[str]]] = None, + license: Optional[Union[str, List[str]]] = None, input_description: Optional[dict] = None, output_description: Optional[dict] = None, ): From 27a23009e6d92f1f3e7c41c87cc69ae727f57bfb Mon Sep 17 00:00:00 2001 From: Gene Date: Tue, 28 Nov 2023 16:38:58 +0800 Subject: [PATCH 08/64] [FIX] add license in semantic spec --- examples/dataset_image_workflow/main.py | 7 ++++++- examples/dataset_m5_workflow/main.py | 4 +++- examples/dataset_m5_workflow/upload.py | 1 + examples/dataset_pfs_workflow/main.py | 4 +++- examples/dataset_pfs_workflow/upload.py | 1 + examples/dataset_text_workflow/main.py | 16 +++++++--------- tests/test_hetero_market/test_hetero.py | 15 +++++++++------ 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/examples/dataset_image_workflow/main.py b/examples/dataset_image_workflow/main.py index c91981c..0512c31 100644 --- a/examples/dataset_image_workflow/main.py +++ b/examples/dataset_image_workflow/main.py @@ -50,6 +50,7 @@ semantic_specs = [ "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "learnware_1", "Type": "String"}, "Output": {"Dimension": 10}, + "License": {"Values": ["MIT"], "Type": "Class"}, } ] @@ -60,6 +61,7 @@ user_semantic = { "Scenario": {"Values": ["Business"], "Type": "Tag"}, "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "", "Type": "String"}, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -175,7 +177,10 @@ def test_search(gamma=0.1, load_market=True): pred_y = single_item.learnware.predict(user_data) acc = eval_prediction(pred_y, user_label) acc_list.append(acc) - logger.info("Search rank: %d, score: %.3f, learnware_id: %s, acc: %.3f" % (idx, single_item.score, single_item.learnware.id, acc)) + logger.info( + "Search rank: %d, score: %.3f, learnware_id: %s, acc: %.3f" + % (idx, single_item.score, single_item.learnware.id, acc) + ) # test reuse (job selector) # reuse_baseline = JobSelectorReuser(learnware_list=mixture_learnware_list, herding_num=100) diff --git a/examples/dataset_m5_workflow/main.py b/examples/dataset_m5_workflow/main.py index 60ee439..c6dea40 100644 --- a/examples/dataset_m5_workflow/main.py +++ b/examples/dataset_m5_workflow/main.py @@ -36,6 +36,7 @@ semantic_specs = [ "Name": {"Values": "learnware_1", "Type": "String"}, "Input": input_description, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } ] @@ -48,6 +49,7 @@ user_semantic = { "Name": {"Values": "", "Type": "String"}, "Input": input_description, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -158,7 +160,7 @@ class M5DatasetWorkflow: search_result = easy_market.search_learnware(user_info) single_result = search_result.get_single_results() multiple_result = search_result.get_multiple_results() - + print(f"search result of user{idx}:") print( f"single model num: {len(single_result)}, max_score: {single_result[0].score}, min_score: {single_result[-1].score}" diff --git a/examples/dataset_m5_workflow/upload.py b/examples/dataset_m5_workflow/upload.py index 0c9e209..af0a69d 100644 --- a/examples/dataset_m5_workflow/upload.py +++ b/examples/dataset_m5_workflow/upload.py @@ -66,6 +66,7 @@ def main(): "Scenario": {"Values": ["Business"], "Type": "Tag"}, "Description": {"Values": "A sales-forecasting model from Walmart store", "Type": "String"}, "Name": {"Values": name, "Type": "String"}, + "License": {"Values": ["MIT"], "Type": "Class"}, } res = session.post( submit_url, diff --git a/examples/dataset_pfs_workflow/main.py b/examples/dataset_pfs_workflow/main.py index 74c4da5..2c33f04 100644 --- a/examples/dataset_pfs_workflow/main.py +++ b/examples/dataset_pfs_workflow/main.py @@ -35,6 +35,7 @@ semantic_specs = [ "Name": {"Values": "learnware_1", "Type": "String"}, "Input": input_description, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } ] @@ -47,6 +48,7 @@ user_semantic = { "Name": {"Values": "", "Type": "String"}, "Input": input_description, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -155,7 +157,7 @@ class PFSDatasetWorkflow: search_result = easy_market.search_learnware(user_info) single_result = search_result.get_single_results() multiple_result = search_result.get_multiple_results() - + print(f"search result of user{idx}:") print( f"single model num: {len(single_result)}, max_score: {single_result[0].score}, min_score: {single_result[-1].score}" diff --git a/examples/dataset_pfs_workflow/upload.py b/examples/dataset_pfs_workflow/upload.py index 9719230..c9da3db 100644 --- a/examples/dataset_pfs_workflow/upload.py +++ b/examples/dataset_pfs_workflow/upload.py @@ -69,6 +69,7 @@ def main(): "Type": "String", }, "Name": {"Values": name, "Type": "String"}, + "License": {"Values": ["MIT"], "Type": "Class"}, } res = session.post( submit_url, diff --git a/examples/dataset_text_workflow/main.py b/examples/dataset_text_workflow/main.py index c5715e7..dcb3898 100644 --- a/examples/dataset_text_workflow/main.py +++ b/examples/dataset_text_workflow/main.py @@ -48,6 +48,7 @@ semantic_specs = [ "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "learnware_1", "Type": "String"}, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } ] @@ -59,6 +60,7 @@ user_semantic = { "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "", "Type": "String"}, "Output": output_description, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -73,7 +75,6 @@ class TextDatasetWorkflow: generate_uploader(X_train, y_train, n_uploaders=n_uploaders, data_save_root=uploader_save_root) generate_user(X_test, y_test, n_users=n_users, data_save_root=user_save_root) - def _prepare_model(self): dataloader = TextDataLoader(data_save_root, train=True) for i in range(n_uploaders): @@ -92,9 +93,8 @@ class TextDatasetWorkflow: logger.info("Model saved to '%s' and '%s'" % (modelv_save_path, modell_save_path)) - - def _prepare_learnware(self, - data_path, modelv_path, modell_path, init_file_path, yaml_path, env_file_path, save_root, zip_name + def _prepare_learnware( + self, data_path, modelv_path, modell_path, init_file_path, yaml_path, env_file_path, save_root, zip_name ): os.makedirs(save_root, exist_ok=True) tmp_spec_path = os.path.join(save_root, "rkme.json") @@ -139,7 +139,6 @@ class TextDatasetWorkflow: logger.info("New Learnware Saved to %s" % (zip_file_name)) return zip_file_name - def prepare_market(self, regenerate_flag=False): if regenerate_flag: self._init_text_dataset() @@ -175,7 +174,6 @@ class TextDatasetWorkflow: logger.info("Total Item: %d" % (len(text_market))) - def test(self, regenerate_flag=False): self.prepare_market(regenerate_flag) text_market = instantiate_learnware_market(market_id="ae") @@ -199,11 +197,11 @@ class TextDatasetWorkflow: user_stat_spec.generate_stat_spec_from_data(X=user_data) user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETextSpecification": user_stat_spec}) logger.info("Searching Market for user: %d" % (i)) - + search_result = text_market.search_learnware(user_info) single_result = search_result.get_single_results() multiple_result = search_result.get_multiple_results() - + print(f"search result of user{i}:") print( f"single model num: {len(single_result)}, max_score: {single_result[0].score}, min_score: {single_result[-1].score}" @@ -220,7 +218,7 @@ class TextDatasetWorkflow: print( f"Top1-score: {single_result[0].score}, learnware_id: {single_result[0].learnware.id}, acc: {acc_list[0]}" ) - + if len(multiple_result) > 0: mixture_id = " ".join([learnware.id for learnware in multiple_result[0].learnwares]) print(f"mixture_score: {multiple_result[0].score}, mixture_learnware: {mixture_id}") diff --git a/tests/test_hetero_market/test_hetero.py b/tests/test_hetero_market/test_hetero.py index be828e5..41b4261 100644 --- a/tests/test_hetero_market/test_hetero.py +++ b/tests/test_hetero_market/test_hetero.py @@ -35,6 +35,7 @@ user_semantic = { "Scenario": {"Values": ["Education"], "Type": "Tag"}, "Description": {"Values": "", "Type": "String"}, "Name": {"Values": "", "Type": "String"}, + "License": {"Values": ["MIT"], "Type": "Class"}, } @@ -259,15 +260,15 @@ class TestMarket(unittest.TestCase): str(key): semantic_spec["Input"]["Description"][str(key)] for key in range(user_dim) } user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - + search_result = hetero_market.search_learnware(user_info) single_result = search_result.get_single_results() multiple_result = search_result.get_multiple_results() - + print(f"search result of user{idx}:") for single_item in single_result: print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - + for multiple_item in multiple_result: print( f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" @@ -335,7 +336,7 @@ class TestMarket(unittest.TestCase): print(f"search result of user{idx}:") for single_item in single_result: print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - + for multiple_item in multiple_result: print(f"mixture_score: {multiple_item.score}\n") mixture_id = " ".join([learnware.id for learnware in multiple_item.learnwares]) @@ -363,9 +364,11 @@ class TestMarket(unittest.TestCase): # print search results for single_item in single_result: print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - + for multiple_item in multiple_result: - print(f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}") + print( + f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" + ) # single model reuse hetero_learnware = HeteroMapAlignLearnware(single_result[0].learnware, mode="regression") From a3a6f25e3a75ea9cf65355be7d5b7114c105d6a5 Mon Sep 17 00:00:00 2001 From: shihy Date: Tue, 28 Nov 2023 23:19:33 +0800 Subject: [PATCH 09/64] [DOC] Update Usage&Example and Privacy-Protection --- docs/_static/img/image_spec.png | Bin 0 -> 342628 bytes docs/components/spec.rst | 47 ++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 docs/_static/img/image_spec.png diff --git a/docs/_static/img/image_spec.png b/docs/_static/img/image_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3de13d86713cbb3c1f830e4d4198843664f11f GIT binary patch literal 342628 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 z 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%0AoI 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

K`&~YHcm(fO784+lUn|DrS_+U>@ixcSFOyxn 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≥(ggoA3@kUCw~x1vmPca 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 Date: Wed, 29 Nov 2023 11:01:10 +0800 Subject: [PATCH 10/64] [DOC] add more details about using update_learnware() and delete_learnware() --- docs/workflows/client.rst | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/workflows/client.rst b/docs/workflows/client.rst index cfdfb02..143ce64 100644 --- a/docs/workflows/client.rst +++ b/docs/workflows/client.rst @@ -104,6 +104,48 @@ Remember to verify the learnware before uploading it, as shown in the following After uploading the learnware successfully, you can see it in ``My Learnware``, the background will check it. Click on the learnware, which can be viewed in the ``Verify Status``. After the check passes, the Unverified tag of the learnware will disappear, and the uploaded learnware will appear in the system. +Update Learnware +------------------------------- + +The ``update_learnware`` method is used to update the metadata and content of an existing learnware on the server. You can upload a new semantic specification, or directly upload a new learnware. + +.. code-block:: python + + # Replace with the actual learnware ID + learnware_id = "123456789" + + # Create new semantic specification + semantic_spec = client.create_semantic_specification( + name="new learnware name", + description="new description", + data_type="Table", + task_type="Classification", + library_type="Scikit-learn", + scenarios=["Computer", "Internet"], + input_description=new_input_description, + output_description=new_output_description, + ) + + # Update metadata without changing the content + client.update_learnware(learnware_id, semantic_spec) + + # Update metadata and content with a new ZIP file + updated_zip_path = "/path/to/updated_learnware.zip" + client.update_learnware(learnware_id, semantic_spec, learnware_zip_path=updated_zip_path) + +Delete Learnware +------------------------------- + +The ``delete_learnware`` method is used to delete a learnware from the server. + +.. code-block:: python + + # Replace with the actual learnware ID + learnware_id = "123456789" + + # Delete the specified learnware + client.delete_learnware(learnware_id) + Semantic Specification Search ------------------------------- From a358f0c7e96f733e440126fbf2d65f7b090762c4 Mon Sep 17 00:00:00 2001 From: Gene Date: Wed, 29 Nov 2023 20:18:53 +0800 Subject: [PATCH 11/64] [FIX] change error type --- learnware/market/easy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/market/easy/checker.py b/learnware/market/easy/checker.py index eb6fe75..7995d94 100644 --- a/learnware/market/easy/checker.py +++ b/learnware/market/easy/checker.py @@ -57,7 +57,7 @@ class EasySemanticChecker(BaseChecker): return EasySemanticChecker.NONUSABLE_LEARNWARE, "EasySemanticChecker Success" - except AssertionError as err: + except Exception as err: logger.warning(f"semantic_specification is not valid due to {err}!") return EasySemanticChecker.INVALID_LEARNWARE, traceback.format_exc() From bad9612ab8f4339ae821de17945ad4b3ddece3de Mon Sep 17 00:00:00 2001 From: liuht Date: Thu, 30 Nov 2023 13:47:06 +0800 Subject: [PATCH 12/64] [FIX] fix sepc typo --- learnware/market/heterogeneous/organizer/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/learnware/market/heterogeneous/organizer/__init__.py b/learnware/market/heterogeneous/organizer/__init__.py index 113b8c3..0258eea 100644 --- a/learnware/market/heterogeneous/organizer/__init__.py +++ b/learnware/market/heterogeneous/organizer/__init__.py @@ -96,7 +96,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): ) if learnwere_status == BaseChecker.USABLE_LEARWARE and len(self._get_hetero_learnware_ids(learnware_id)): - self._update_learware_hetero_sepc(learnware_id) + self._update_learware_hetero_spec(learnware_id) if self.auto_update: self.count_down -= 1 @@ -113,7 +113,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): f"Market mapping train completed. Now update HeteroMapTableSpecification for {training_learnware_ids}" ) self.market_mapping = updated_market_mapping - self._update_learware_hetero_sepc(training_learnware_ids) + self._update_learware_hetero_spec(training_learnware_ids) self.count_down = self.auto_update_limit @@ -167,7 +167,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): """ final_status = super(HeteroMapTableOrganizer, self).update_learnware(id, zip_path, semantic_spec, check_status) if final_status == BaseChecker.USABLE_LEARWARE and len(self._get_hetero_learnware_ids(id)): - self._update_learware_hetero_sepc(id) + self._update_learware_hetero_spec(id) return final_status def _reload_learnware_hetero_spec(self, learnware_id): @@ -178,7 +178,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): hetero_spec.load(hetero_spec_path) self.learnware_list[learnware_id].update_stat_spec(hetero_spec.type, hetero_spec) else: - self._update_learware_hetero_sepc(learnware_id) + self._update_learware_hetero_spec(learnware_id) logger.info(f"Reload HeteroMapTableSpecification for hetero spec {learnware_id} succeed!") except Exception as err: logger.error(f"Reload HeteroMapTableSpecification for hetero spec {learnware_id} failed! due to {err}.") @@ -196,7 +196,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): if len(self._get_hetero_learnware_ids(learnware_id)): self._reload_learnware_hetero_spec(learnware_id) - def _update_learware_hetero_sepc(self, ids: Union[str, List[str]]): + def _update_learware_hetero_spec(self, ids: Union[str, List[str]]): """Update learnware by ids, attempting to generate HeteroMapTableSpecification for them. Parameters From 1dadf41ad2619f0bab00ddc7708118a12b105f3a Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Thu, 30 Nov 2023 14:02:39 +0800 Subject: [PATCH 13/64] [FIX] fix commands to fit with windows --- learnware/client/container.py | 15 +++++++++------ learnware/client/learnware_client.py | 2 +- learnware/client/utils.py | 6 ++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 5573799..c92d451 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -106,7 +106,7 @@ class ModelCondaContainer(ModelContainer): "-n", f"{self.conda_env}", "--no-capture-output", - "python3", + "python", f"{self.model_script}", "--model-path", f"{model_path}", @@ -149,7 +149,7 @@ class ModelCondaContainer(ModelContainer): "-n", f"{self.conda_env}", "--no-capture-output", - "python3", + "python", f"{self.model_script}", "--model-path", f"{model_path}", @@ -333,10 +333,11 @@ class ModelDockerContainer(ModelContainer): "-n", f"{conda_env}", "--no-capture-output", - "python3", + "python", "-m", "pip", "install", + "--user", "-r", f"{requirements_path_filter}", ] @@ -362,10 +363,11 @@ class ModelDockerContainer(ModelContainer): "-n", f"{conda_env}", "--no-capture-output", - "python3", + "python", "-m", "pip", "install", + "--user", "learnware", ] ) @@ -404,7 +406,7 @@ class ModelDockerContainer(ModelContainer): "-n", f"{self.conda_env}", "--no-capture-output", - "python3", + "python", f"{self.docker_model_script_path}", "--model-path", f"{model_path}", @@ -458,7 +460,7 @@ class ModelDockerContainer(ModelContainer): "-n", f"{self.conda_env}", "--no-capture-output", - "python3", + "python", f"{self.docker_model_script_path}", "--model-path", f"{model_path}", @@ -559,6 +561,7 @@ class LearnwaresContainer: return self def __exit__(self, exc_type, exc_val, exc_tb): + return # TODO if not self.cleanup: logger.warning(f"Notice, the learnware container env is not cleaned up!") self.learnware_containers = None diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index f3cbe61..d442dc6 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -178,7 +178,7 @@ class LearnwareClient: headers=self.headers, stream=True, ) - + print(response) if response.status_code != 200: raise Exception("download failed: " + json.dumps(response.json())) diff --git a/learnware/client/utils.py b/learnware/client/utils.py index 0e11ae0..c4c5c47 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -76,10 +76,11 @@ def install_environment(learnware_dirpath, conda_env): "-n", f"{conda_env}", "--no-capture-output", - "python3", + "python", "-m", "pip", "install", + "--user", "-r", f"{requirements_path_filter}", ] @@ -95,10 +96,11 @@ def install_environment(learnware_dirpath, conda_env): "-n", f"{conda_env}", "--no-capture-output", - "python3", + "python", "-m", "pip", "install", + "--user", "learnware", ] ) From de0ce05613f727e787169dc22d5fef18ee5524d9 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 30 Nov 2023 15:39:52 +0800 Subject: [PATCH 14/64] [FIX] fix details --- learnware/client/container.py | 1 - learnware/client/learnware_client.py | 8 ++++---- .../test_check_learnware.py | 20 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index c92d451..fbef446 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -561,7 +561,6 @@ class LearnwaresContainer: return self def __exit__(self, exc_type, exc_val, exc_tb): - return # TODO if not self.cleanup: logger.warning(f"Notice, the learnware container env is not cleaned up!") self.learnware_containers = None diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index d442dc6..bdc6d6e 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -178,7 +178,7 @@ class LearnwareClient: headers=self.headers, stream=True, ) - print(response) + if response.status_code != 200: raise Exception("download failed: " + json.dumps(response.json())) @@ -247,7 +247,7 @@ class LearnwareClient: for learnware in result["data"]["learnware_list_single"]: returns.append( - { + { "type": "single", "learnware_id": learnware["learnware_id"], "semantic_specification": learnware["semantic_specification"], @@ -259,12 +259,12 @@ class LearnwareClient: "type": "multiple", "learnware_ids": [], "semantic_specifications": [], - "matching": result["data"]["learnware_list_multi"][0]["matching"] + "matching": result["data"]["learnware_list_multi"][0]["matching"], } for learnware in result["data"]["learnware_list_multi"]: multiple_learnware["learnware_ids"].append(learnware["learnware_id"]) multiple_learnware["semantic_specifications"].append(learnware["semantic_specification"]) - + returns.append(multiple_learnware) return returns diff --git a/tests/test_learnware_client/test_check_learnware.py b/tests/test_learnware_client/test_check_learnware.py index 59f0820..f9e6213 100644 --- a/tests/test_learnware_client/test_check_learnware.py +++ b/tests/test_learnware_client/test_check_learnware.py @@ -46,16 +46,16 @@ class TestCheckLearnware(unittest.TestCase): semantic_spec = json.load(json_file) LearnwareClient.check_learnware(self.zip_path, semantic_spec) - def test_check_learnware_image(self): - learnware_id = "00000677" - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - self.zip_path = os.path.join(tempdir, "test.zip") - self.client.download_learnware(learnware_id, self.zip_path) - - with zipfile.ZipFile(self.zip_path, "r") as zip_file: - with zip_file.open("semantic_specification.json") as json_file: - semantic_spec = json.load(json_file) - LearnwareClient.check_learnware(self.zip_path, semantic_spec) + # def test_check_learnware_image(self): + # learnware_id = "00000677" + # with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + # self.zip_path = os.path.join(tempdir, "test.zip") + # self.client.download_learnware(learnware_id, self.zip_path) + + # with zipfile.ZipFile(self.zip_path, "r") as zip_file: + # with zip_file.open("semantic_specification.json") as json_file: + # semantic_spec = json.load(json_file) + # LearnwareClient.check_learnware(self.zip_path, semantic_spec) def test_check_learnware_text(self): learnware_id = "00000662" From 5317c321023e77bb92a83fdc59e1e7714eb2f012 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 30 Nov 2023 18:37:35 +0800 Subject: [PATCH 15/64] [FIX] fix test details --- .../test_check_learnware.py | 20 +++++++++---------- .../test_learnware_client/test_load_conda.py | 18 ++++++++--------- .../test_learnware_client/test_load_docker.py | 10 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_learnware_client/test_check_learnware.py b/tests/test_learnware_client/test_check_learnware.py index f9e6213..59f0820 100644 --- a/tests/test_learnware_client/test_check_learnware.py +++ b/tests/test_learnware_client/test_check_learnware.py @@ -46,16 +46,16 @@ class TestCheckLearnware(unittest.TestCase): semantic_spec = json.load(json_file) LearnwareClient.check_learnware(self.zip_path, semantic_spec) - # def test_check_learnware_image(self): - # learnware_id = "00000677" - # with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - # self.zip_path = os.path.join(tempdir, "test.zip") - # self.client.download_learnware(learnware_id, self.zip_path) - - # with zipfile.ZipFile(self.zip_path, "r") as zip_file: - # with zip_file.open("semantic_specification.json") as json_file: - # semantic_spec = json.load(json_file) - # LearnwareClient.check_learnware(self.zip_path, semantic_spec) + def test_check_learnware_image(self): + learnware_id = "00000677" + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + self.zip_path = os.path.join(tempdir, "test.zip") + self.client.download_learnware(learnware_id, self.zip_path) + + with zipfile.ZipFile(self.zip_path, "r") as zip_file: + with zip_file.open("semantic_specification.json") as json_file: + semantic_spec = json.load(json_file) + LearnwareClient.check_learnware(self.zip_path, semantic_spec) def test_check_learnware_text(self): learnware_id = "00000662" diff --git a/tests/test_learnware_client/test_load_conda.py b/tests/test_learnware_client/test_load_conda.py index 4394348..11f7e40 100644 --- a/tests/test_learnware_client/test_load_conda.py +++ b/tests/test_learnware_client/test_load_conda.py @@ -16,7 +16,7 @@ class TestLearnwareLoad(unittest.TestCase): self.client = LearnwareClient() root = os.path.dirname(__file__) - self.learnware_ids = ["00000084", "00000154", "00000155"] + self.learnware_ids = ["00000910", "00000899", "00000900"] self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] def test_load_single_learnware_by_zippath(self): @@ -26,8 +26,8 @@ class TestLearnwareLoad(unittest.TestCase): learnware_list = [ self.client.load_learnware(learnware_path=zippath, runnable_option="conda") for zippath in self.zip_paths ] - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: @@ -38,8 +38,8 @@ class TestLearnwareLoad(unittest.TestCase): self.client.download_learnware(learnware_id, zip_path) learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option="conda") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: @@ -49,8 +49,8 @@ class TestLearnwareLoad(unittest.TestCase): learnware_list = [ self.client.load_learnware(learnware_id=idx, runnable_option="conda") for idx in self.learnware_ids ] - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: @@ -58,8 +58,8 @@ class TestLearnwareLoad(unittest.TestCase): def test_load_multi_learnware_by_id(self): learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="conda") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: diff --git a/tests/test_learnware_client/test_load_docker.py b/tests/test_learnware_client/test_load_docker.py index 775405c..4436504 100644 --- a/tests/test_learnware_client/test_load_docker.py +++ b/tests/test_learnware_client/test_load_docker.py @@ -16,7 +16,7 @@ class TestLearnwareLoad(unittest.TestCase): self.client = LearnwareClient() root = os.path.dirname(__file__) - self.learnware_ids = ["00000084", "00000154", "00000155"] + self.learnware_ids = ["00000910", "00000899", "00000900"] self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] def test_load_multi_learnware_by_zippath(self): @@ -24,8 +24,8 @@ class TestLearnwareLoad(unittest.TestCase): self.client.download_learnware(learnware_id, zip_path) learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option="docker") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: @@ -33,8 +33,8 @@ class TestLearnwareLoad(unittest.TestCase): def test_load_multi_learnware_by_id(self): learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="docker") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) print(reuser.predict(input_array)) for learnware in learnware_list: From a3612adcd8b9eba8485fb3f54f215a45023ad67f Mon Sep 17 00:00:00 2001 From: bxdd Date: Thu, 30 Nov 2023 19:10:20 +0800 Subject: [PATCH 16/64] [MNT] release new beta version --- learnware/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/__init__.py b/learnware/__init__.py index c3f122e..eed5dde 100644 --- a/learnware/__init__.py +++ b/learnware/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0.4" +__version__ = "0.2.0.5" import os import json From e4d5a0da7ebf866aa298ffadc2715b37b85fad2d Mon Sep 17 00:00:00 2001 From: bxdd Date: Thu, 30 Nov 2023 22:53:05 +0800 Subject: [PATCH 17/64] [FIX] fix client util bug --- learnware/client/utils.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/learnware/client/utils.py b/learnware/client/utils.py index c4c5c47..049d07f 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -10,24 +10,18 @@ logger = get_module_logger(module_name="client_utils") def system_execute(args, timeout=None, env=None, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE): - if env is None: - env = os.environ.copy() - pass - - if isinstance(args, str): - pass - else: - args = " ".join(args) - pass + env = os.environ.copy() if env is None else env + args = args if isinstance(args, str) else " ".join(args) com_process = subprocess.run(args, stdout=stdout, stderr=stderr, timeout=timeout, env=env, shell=True) try: com_process.check_returncode() except subprocess.CalledProcessError as err: - errmsg = com_process.stderr.decode() - logger.warning(f"System Execute Error: {errmsg}") - raise Exception(errmsg) + if err.stderr is not None: + errmsg = err.stderr.decode() + logger.warning(f"System Execute Error: {errmsg}") + raise err def remove_enviroment(conda_env): From 09910cfc0a44181d101aca5aa05238e72247555f Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Fri, 1 Dec 2023 18:30:36 +0800 Subject: [PATCH 18/64] [FIX] fix details about pip packages --- learnware/client/package_utils.py | 78 ++++++++++++++++++------------- learnware/client/utils.py | 2 + 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/learnware/client/package_utils.py b/learnware/client/package_utils.py index 5768a4e..f3266ae 100644 --- a/learnware/client/package_utils.py +++ b/learnware/client/package_utils.py @@ -6,6 +6,7 @@ import tempfile import subprocess from typing import List, Tuple from . import utils +from concurrent.futures import ThreadPoolExecutor from ..logger import get_module_logger @@ -13,38 +14,38 @@ from ..logger import get_module_logger logger = get_module_logger("package_utils") -def try_to_run(args, timeout=5, retry=5): - sucess = False +def try_to_run(args, timeout=10, retry=3): for i in range(retry): try: - utils.system_execute(args=args, timeout=timeout, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - sucess = True - break + result = utils.system_execute(args=args, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + return result.stdout.decode() except subprocess.TimeoutExpired as e: pass - - if not sucess: - raise subprocess.TimeoutExpired(args, timeout) + + raise subprocess.TimeoutExpired(args, timeout) def parse_pip_requirement(line: str): - """Parse pip requirement line to package name""" + """Parse pip requirement line to package name and version""" line = line.strip() - - if len(line) == 0: - return None - - if line[0] in ("#", "-"): + if len(line) == 0 or line[0] in ("#", "-"): return None - package_str = line - for split_ch in ("=", ">", "<", "!", "~", " "): - split_ch_index = package_str.find(split_ch) + package_name, package_version = line, line + for split_ch in ("=", ">", "<", "!", "~", " ", "="): + split_ch_index = package_name.find(split_ch) if split_ch_index != -1: - package_str = package_str[:split_ch_index] + package_name = package_name[:split_ch_index] + + split_ch_index = package_version.find(split_ch) + if split_ch_index != -1: + package_version = package_version[split_ch_index + 1:] + + if package_version == package_name: + package_version = "" - return package_str + return package_name, package_version def read_pip_packages_from_requirements(requirements_file: str) -> Tuple[List[str], List[str]]: @@ -54,7 +55,7 @@ def read_pip_packages_from_requirements(requirements_file: str) -> Tuple[List[st lines = [] with open(requirements_file, "r") as fin: for line in fin: - package_str = parse_pip_requirement(line) + package_str, package_version = parse_pip_requirement(line) packages.append(package_str) lines.append(line) @@ -70,22 +71,34 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: exist_packages: list of exist packages nonexist_packages: list of non-exist packages """ - - exist_packages = [] - nonexist_packages = [] - for package in packages: - if package is None: - continue + def _filter_nonexist_pip_package_worker(package): + # Return filtered package try: - package_name = parse_pip_requirement(package) + package_name, package_version = parse_pip_requirement(package) if package_name is not None and package_name != "learnware": - try_to_run(args=["pip", "index", "versions", package_name], timeout=5) - exist_packages.append(package) - continue + result = try_to_run(args=["pip", "index", "versions", package_name], timeout=10) + if len(package_version) and package_version not in result: + return package_name + else: + return package except Exception as e: logger.error(e) - nonexist_packages.append(package) - + + return None + + exist_packages = [] + nonexist_packages = [] + packages = [package for package in packages if package is not None] + + with ThreadPoolExecutor(max_workers=max(os.cpu_count() // 5, 1)) as executor: + results = executor.map(_filter_nonexist_pip_package_worker, packages) + + for result, package in zip(list(results), packages): + if result is not None: + exist_packages.append(result) + else: + nonexist_packages.append(package) + return exist_packages, nonexist_packages @@ -169,7 +182,6 @@ def read_conda_packages_from_dict(env_desc: dict) -> Tuple[List[str], List[str]] for package in conda_packages: if isinstance(package, dict) and "pip" in package: pip_packages = package["pip"] - # pip_packages = [parse_pip_requirement(line) for line in pip_packages] elif isinstance(package, str): conda_packages_.append(package) diff --git a/learnware/client/utils.py b/learnware/client/utils.py index 049d07f..85f6b45 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -22,6 +22,8 @@ def system_execute(args, timeout=None, env=None, stdout=subprocess.DEVNULL, stde errmsg = err.stderr.decode() logger.warning(f"System Execute Error: {errmsg}") raise err + + return com_process def remove_enviroment(conda_env): From d94514f21067fb037586572d7ad65408b73ace91 Mon Sep 17 00:00:00 2001 From: Asymptotez <201220101@smail.nju.edu.cn> Date: Fri, 1 Dec 2023 18:43:01 +0800 Subject: [PATCH 19/64] [DOC] modify some expressions and add the content about license --- docs/workflows/client.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/workflows/client.rst b/docs/workflows/client.rst index 143ce64..22d58f9 100644 --- a/docs/workflows/client.rst +++ b/docs/workflows/client.rst @@ -70,18 +70,20 @@ Before uploading a learnware, you'll need to prepare the semantic specification task_type="Classification", library_type="Scikit-learn", scenarios=["Business", "Financial"], + license=["Apache-2.0"], input_description=input_description, output_description=output_description, ) -Make sure that the parameter input for the semantic specification is within the range given by ``client.list_semantic_specification_values(key)`` : +Ensure that the input parameters for the semantic specification fall within the specified ranges provided by ``client.list_semantic_specification_values(key)``: -* data_type must in ``key=SemanticSpecificationKey.DATA_TYPE``; -* task_type must in ``key=SemanticSpecificationKey.TASK_TYPE``; -* library_type must in ``key=SemanticSpecificationKey.LIBRARY_TYPE``; -* scenarios must be a subset of ``key=SemanticSpecificationKey.SENARIOES``; -* When data_type is ``"Table"``, input description needs to be filled in; -* When task_type is in ``["Classification", "Regression"]``, output description needs to be filled. +* "data_type" must be within the range of ``key=SemanticSpecificationKey.DATA_TYPE``. +* "task_type" must be within the range of ``key=SemanticSpecificationKey.TASK_TYPE``. +* "library_type" must be within the range of ``key=SemanticSpecificationKey.LIBRARY_TYPE``. +* "scenarios" must be a subset of ``key=SemanticSpecificationKey.SENARIOS``. +* "license" must be a subset of ``key=SemanticSpecificationKey.LICENSE``. +* When "data_type" is set to "Table", it is necessary to provide "input_description". +* When "task_type" is either "Classification" or "Regression", it is necessary to provide "output_description". Finally, the semantic specification and the zip package path of the learnware were filled in to upload the learnware. @@ -122,6 +124,7 @@ The ``update_learnware`` method is used to update the metadata and content of an task_type="Classification", library_type="Scikit-learn", scenarios=["Computer", "Internet"], + license=["CC-BY-4.0"], input_description=new_input_description, output_description=new_output_description, ) From 155f6ee1c1233a7380d1c64b85dc7dfd48791243 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 19:46:29 +0800 Subject: [PATCH 20/64] [FIX] fix bug for pip user install --- learnware/client/container.py | 5 +---- learnware/client/utils.py | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index fbef446..48835b3 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -337,7 +337,6 @@ class ModelDockerContainer(ModelContainer): "-m", "pip", "install", - "--user", "-r", f"{requirements_path_filter}", ] @@ -367,7 +366,6 @@ class ModelDockerContainer(ModelContainer): "-m", "pip", "install", - "--user", "learnware", ] ) @@ -555,14 +553,13 @@ class LearnwaresContainer: if sum(self.results) < len(self.learnware_list): logger.warning( - f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! This learnware will be ignored" + f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! This learnwares will be ignored" ) return self def __exit__(self, exc_type, exc_val, exc_tb): if not self.cleanup: - logger.warning(f"Notice, the learnware container env is not cleaned up!") self.learnware_containers = None self.results = None return diff --git a/learnware/client/utils.py b/learnware/client/utils.py index 85f6b45..132c9a0 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -76,7 +76,6 @@ def install_environment(learnware_dirpath, conda_env): "-m", "pip", "install", - "--user", "-r", f"{requirements_path_filter}", ] @@ -96,7 +95,6 @@ def install_environment(learnware_dirpath, conda_env): "-m", "pip", "install", - "--user", "learnware", ] ) From 346d9cfb5cef3b932d94454ba017e194ccfe0392 Mon Sep 17 00:00:00 2001 From: Gene Date: Fri, 1 Dec 2023 19:51:17 +0800 Subject: [PATCH 21/64] [MNT] modify typo --- learnware/client/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 48835b3..f1f90e7 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -553,7 +553,7 @@ class LearnwaresContainer: if sum(self.results) < len(self.learnware_list): logger.warning( - f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! This learnwares will be ignored" + f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! These learnwares will be ignored" ) return self From 50e8f494355e375c9bfb78f532727653f5811d50 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 20:52:06 +0800 Subject: [PATCH 22/64] [FIX] make container env remove always done --- learnware/client/container.py | 11 +++-- .../test_learnware_client/test_load_conda.py | 45 ------------------- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 48835b3..590af59 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -43,16 +43,19 @@ class ModelContainer(BaseModel): """We must set `input_shape` and `output_shape`""" if self.build: self.cleanup_flag = True - self._init_env() atexit.register(self.remove_env) - + self._init_env() self._setup_env_and_metadata() def remove_env(self): if self.cleanup_flag is True: - self.cleanup_flag = False try: + self.cleanup_flag = False self._remove_env() + except KeyboardInterrupt: + self.cleanup_flag = True + logger.warning("The KeyboardInterrupt is ignored when removing the container env!") + self.remove_env() except Exception as err: self.cleanup_flag = True raise err @@ -553,7 +556,7 @@ class LearnwaresContainer: if sum(self.results) < len(self.learnware_list): logger.warning( - f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! This learnwares will be ignored" + f"{len(self.learnware_list) - sum(results)} of {len(self.learnware_list)} learnwares init failed! These learnwares will be ignored" ) return self diff --git a/tests/test_learnware_client/test_load_conda.py b/tests/test_learnware_client/test_load_conda.py index 11f7e40..f623e95 100644 --- a/tests/test_learnware_client/test_load_conda.py +++ b/tests/test_learnware_client/test_load_conda.py @@ -19,20 +19,6 @@ class TestLearnwareLoad(unittest.TestCase): self.learnware_ids = ["00000910", "00000899", "00000900"] self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] - def test_load_single_learnware_by_zippath(self): - for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - self.client.download_learnware(learnware_id, zip_path) - - learnware_list = [ - self.client.load_learnware(learnware_path=zippath, runnable_option="conda") for zippath in self.zip_paths - ] - reuser = AveragingReuser(learnware_list, mode="mean") - input_array = np.random.random(size=(20, 40)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - def test_load_multi_learnware_by_zippath(self): for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): self.client.download_learnware(learnware_id, zip_path) @@ -45,37 +31,6 @@ class TestLearnwareLoad(unittest.TestCase): for learnware in learnware_list: print(learnware.id, learnware.predict(input_array)) - def test_load_single_learnware_by_id(self): - learnware_list = [ - self.client.load_learnware(learnware_id=idx, runnable_option="conda") for idx in self.learnware_ids - ] - reuser = AveragingReuser(learnware_list, mode="mean") - input_array = np.random.random(size=(20, 40)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_multi_learnware_by_id(self): - learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="conda") - reuser = AveragingReuser(learnware_list, mode="mean") - input_array = np.random.random(size=(20, 40)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_single_learnware_by_id_pip(self): - learnware_id = "00000147" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") - input_array = np.random.random(size=(20, 23)) - print(learnware.predict(input_array)) - - def test_load_single_learnware_by_id_conda(self): - learnware_id = "00000148" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") - input_array = np.random.random(size=(20, 204)) - print(learnware.predict(input_array)) if __name__ == "__main__": From 350786fdb7926c13ac03f79a1a13a641b0a32e54 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 20:53:19 +0800 Subject: [PATCH 23/64] [MNT] recover tests --- .../test_learnware_client/test_load_conda.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_learnware_client/test_load_conda.py b/tests/test_learnware_client/test_load_conda.py index f623e95..11f7e40 100644 --- a/tests/test_learnware_client/test_load_conda.py +++ b/tests/test_learnware_client/test_load_conda.py @@ -19,6 +19,20 @@ class TestLearnwareLoad(unittest.TestCase): self.learnware_ids = ["00000910", "00000899", "00000900"] self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] + def test_load_single_learnware_by_zippath(self): + for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): + self.client.download_learnware(learnware_id, zip_path) + + learnware_list = [ + self.client.load_learnware(learnware_path=zippath, runnable_option="conda") for zippath in self.zip_paths + ] + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) + print(reuser.predict(input_array)) + + for learnware in learnware_list: + print(learnware.id, learnware.predict(input_array)) + def test_load_multi_learnware_by_zippath(self): for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): self.client.download_learnware(learnware_id, zip_path) @@ -31,6 +45,37 @@ class TestLearnwareLoad(unittest.TestCase): for learnware in learnware_list: print(learnware.id, learnware.predict(input_array)) + def test_load_single_learnware_by_id(self): + learnware_list = [ + self.client.load_learnware(learnware_id=idx, runnable_option="conda") for idx in self.learnware_ids + ] + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) + print(reuser.predict(input_array)) + + for learnware in learnware_list: + print(learnware.id, learnware.predict(input_array)) + + def test_load_multi_learnware_by_id(self): + learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="conda") + reuser = AveragingReuser(learnware_list, mode="mean") + input_array = np.random.random(size=(20, 40)) + print(reuser.predict(input_array)) + + for learnware in learnware_list: + print(learnware.id, learnware.predict(input_array)) + + def test_load_single_learnware_by_id_pip(self): + learnware_id = "00000147" + learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") + input_array = np.random.random(size=(20, 23)) + print(learnware.predict(input_array)) + + def test_load_single_learnware_by_id_conda(self): + learnware_id = "00000148" + learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") + input_array = np.random.random(size=(20, 204)) + print(learnware.predict(input_array)) if __name__ == "__main__": From 3436b88008968eae83114522b5ce50decf33a9b0 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 21:16:26 +0800 Subject: [PATCH 24/64] [DOC] fix doc link --- docs/workflows/client.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/workflows/client.rst b/docs/workflows/client.rst index 22d58f9..48db96e 100644 --- a/docs/workflows/client.rst +++ b/docs/workflows/client.rst @@ -12,7 +12,7 @@ Introduction Prepare access token ==================== -Before using the ``Learnware Client``, you'll need to obtain a token from the `official website `_. Just login to the website and click ``Client Token`` tab in the user center. +Before using the ``Learnware Client``, you'll need to obtain a token from the `official website `_. Just login to the website and click ``Client Token`` tab in the ``Personal Information``. How to Use Client @@ -143,7 +143,7 @@ The ``delete_learnware`` method is used to delete a learnware from the server. .. code-block:: python - # Replace with the actual learnware ID + # Replace with the actual learnware ID to delete learnware_id = "123456789" # Delete the specified learnware From d4d0291fcbc862e7562be7dc0b5c7cb57187e2bc Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 21:16:48 +0800 Subject: [PATCH 25/64] [DOC] update doc index --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 91c514f..33441a3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,7 +39,7 @@ Document Structure .. toctree:: :maxdepth: 3 - :caption: CONCEPTS: + :caption: COMPONENTS: Market Learnware & Reuser From 77dc5f9efc0ed5d1d717d6862d8a854af7bc18e1 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 1 Dec 2023 21:21:27 +0800 Subject: [PATCH 26/64] [DOC] update doc --- docs/workflows/client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/workflows/client.rst b/docs/workflows/client.rst index 48db96e..b76fb8e 100644 --- a/docs/workflows/client.rst +++ b/docs/workflows/client.rst @@ -6,7 +6,7 @@ Learnware Client Introduction ==================== -``Learnware Client`` is a python api that provides a convenient interface for interacting with the system. You can easily use the client to upload, download and search learnwares. +``Learnware Client`` is a ``Python API`` that provides a convenient interface for interacting with the ``BeimingWu`` system. You can easily use the client to upload, download, delete, update, and search learnwares. Prepare access token From 5ae2358ffdec981c2e19cb5575140645df2597b0 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Sat, 2 Dec 2023 22:59:26 +0800 Subject: [PATCH 27/64] [FIX] fix path error in win docker --- learnware/client/container.py | 66 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 590af59..eb6809e 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -245,6 +245,15 @@ class ModelDockerContainer(ModelContainer): else: logger.error("Type of docker_container is not docker.models.containers.Container.") + @staticmethod + def _change_path_to_container(path): + file_name = os.path.basename(path) + if "." in file_name: + file_dir = os.path.basename(os.path.dirname(path)) + return f"/tmp/{file_dir}/{file_name}" + else: + return f"/tmp/{file_name}" + def _copy_file_to_container(self, local_path, container_path): directory_path = os.path.dirname(container_path) container_name = os.path.basename(container_path) @@ -298,13 +307,15 @@ class ModelDockerContainer(ModelContainer): yaml_path_filter: str = os.path.join(tempdir, "environment_filter.yaml") logger.info(f"checking the avaliabe conda packages for {conda_env}") filter_nonexist_conda_packages_file(yaml_file=yaml_path, output_yaml_file=yaml_path_filter) - self._copy_file_to_container(yaml_path_filter, yaml_path_filter) + + yaml_path_filter_container = self._change_path_to_container(yaml_path_filter) + self._copy_file_to_container(yaml_path_filter, yaml_path_filter_container) # create environment logger.info(f"Create and update conda env [{conda_env}] according to .yaml file") for i in range(run_cmd_times): result = self.docker_container.exec_run( - " ".join(["conda", "env", "create", "--name", f"{conda_env}", "--file", f"{yaml_path_filter}"]) + " ".join(["conda", "env", "create", "--name", f"{conda_env}", "--file", f"{yaml_path_filter_container}"]) ) if result.exit_code == 0: success_flag = True @@ -326,7 +337,9 @@ class ModelDockerContainer(ModelContainer): break logger.info(f"install pip requirements for conda env [{conda_env}] in docker.") - self._copy_file_to_container(requirements_path_filter, requirements_path_filter) + requirements_path_filter_container = self._change_path_to_container(requirements_path_filter) + self._copy_file_to_container(requirements_path_filter, requirements_path_filter_container) + for i in range(run_cmd_times): result = self.docker_container.exec_run( " ".join( @@ -341,7 +354,7 @@ class ModelDockerContainer(ModelContainer): "pip", "install", "-r", - f"{requirements_path_filter}", + f"{requirements_path_filter_container}", ] ) ) @@ -386,19 +399,21 @@ class ModelDockerContainer(ModelContainer): with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: output_path = os.path.join(tempdir, "output.pkl") model_path = os.path.join(tempdir, "model.pkl") - self.docker_model_script_path = os.path.join(tempdir, "run_model.py") + self.docker_model_script_path = self._change_path_to_container(os.path.join(tempdir, "run_model.py")) + output_path_container = self._change_path_to_container(output_path) + model_path_container = self._change_path_to_container(model_path) + learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) + docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = Learnware.get_model_module_abspath( - self.learnware_dirpath, docker_model_config["module_path"] - ) - self._copy_file_to_container(self.learnware_dirpath, self.learnware_dirpath) + docker_model_config["module_path"] = learnware_dirpath_container + "/" + docker_model_config["module_path"] + self._copy_file_to_container(self.learnware_dirpath, learnware_dirpath_container) with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) - - self._copy_file_to_container(model_path, model_path) + self._copy_file_to_container(model_path, model_path_container) self._copy_file_to_container(self.model_script, self.docker_model_script_path) + self.docker_container.exec_run( " ".join( [ @@ -410,13 +425,13 @@ class ModelDockerContainer(ModelContainer): "python", f"{self.docker_model_script_path}", "--model-path", - f"{model_path}", + f"{model_path_container}", "--output-path", - f"{output_path}", + f"{output_path_container}", ] ) ) - self._copy_file_from_container(output_path, output_path) + self._copy_file_from_container(output_path_container, output_path) with open(output_path, "rb") as output_fp: output_results = pickle.load(output_fp) @@ -439,19 +454,22 @@ class ModelDockerContainer(ModelContainer): input_path = os.path.join(tempdir, "input.pkl") output_path = os.path.join(tempdir, "output.pkl") model_path = os.path.join(tempdir, "model.pkl") - + + input_path_container = self._change_path_to_container(input_path) + output_path_container = self._change_path_to_container(output_path) + model_path_container = self._change_path_to_container(model_path) + learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) + docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = Learnware.get_model_module_abspath( - self.learnware_dirpath, docker_model_config["module_path"] - ) + docker_model_config["module_path"] = learnware_dirpath_container + "/" + docker_model_config["module_path"] with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) with open(input_path, "wb") as input_fp: pickle.dump({"method": method, "kargs": kargs}, input_fp) - self._copy_file_to_container(model_path, model_path) - self._copy_file_to_container(input_path, input_path) + self._copy_file_to_container(model_path, model_path_container) + self._copy_file_to_container(input_path, input_path_container) self.docker_container.exec_run( " ".join( @@ -464,15 +482,15 @@ class ModelDockerContainer(ModelContainer): "python", f"{self.docker_model_script_path}", "--model-path", - f"{model_path}", + f"{model_path_container}", "--input-path", - f"{input_path}", + f"{input_path_container}", "--output-path", - f"{output_path}", + f"{output_path_container}", ] ) ) - self._copy_file_from_container(output_path, output_path) + self._copy_file_from_container(output_path_container, output_path) with open(output_path, "rb") as output_fp: output_results = pickle.load(output_fp) From 82ffaeaa97e04018f225f0fa9e1446f84800c1a3 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Sat, 2 Dec 2023 23:16:14 +0800 Subject: [PATCH 28/64] [MNT] modify logger --- learnware/client/package_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/learnware/client/package_utils.py b/learnware/client/package_utils.py index f3266ae..2312661 100644 --- a/learnware/client/package_utils.py +++ b/learnware/client/package_utils.py @@ -81,8 +81,10 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: return package_name else: return package - except Exception as e: - logger.error(e) + except subprocess.CalledProcessError: + pass + except Exception as err: + logger.error(err) return None @@ -99,6 +101,8 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: else: nonexist_packages.append(package) + if len(nonexist_packages) > 0: + logger.info(f"Filtered out {len(nonexist_packages)} non-exist pip packages.") return exist_packages, nonexist_packages From 235495cfc2b0b939e6d49b24221add143fa2ab24 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Sun, 3 Dec 2023 14:05:32 +0800 Subject: [PATCH 29/64] [ENH] add install env script for docker --- learnware/client/scripts/install_env.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 learnware/client/scripts/install_env.py diff --git a/learnware/client/scripts/install_env.py b/learnware/client/scripts/install_env.py new file mode 100644 index 0000000..528b1e6 --- /dev/null +++ b/learnware/client/scripts/install_env.py @@ -0,0 +1,15 @@ +import argparse +from learnware.client.utils import install_environment + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--learnware-dirpath", type=str, required=True, help="path of learnware dir") + parser.add_argument("--conda-env", type=str, required=False, help="name of conda env") + + args = parser.parse_args() + + learnware_dirpath = args.learnware_dirpath + conda_env = args.conda_env + + install_environment(learnware_dirpath, conda_env) From 2f06c14ae4661a679a8822355493e4fb2961ffaf Mon Sep 17 00:00:00 2001 From: bxdd Date: Sun, 3 Dec 2023 14:06:12 +0800 Subject: [PATCH 30/64] [FIX] fix hetero import torch --- learnware/client/package_utils.py | 2 +- learnware/market/heterogeneous/__init__.py | 14 ++++++++++++-- learnware/market/heterogeneous/searcher.py | 6 ++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/learnware/client/package_utils.py b/learnware/client/package_utils.py index f3266ae..dbc8679 100644 --- a/learnware/client/package_utils.py +++ b/learnware/client/package_utils.py @@ -30,7 +30,7 @@ def parse_pip_requirement(line: str): line = line.strip() if len(line) == 0 or line[0] in ("#", "-"): - return None + return None, None package_name, package_version = line, line for split_ch in ("=", ">", "<", "!", "~", " ", "="): diff --git a/learnware/market/heterogeneous/__init__.py b/learnware/market/heterogeneous/__init__.py index e7ab916..df0ad39 100644 --- a/learnware/market/heterogeneous/__init__.py +++ b/learnware/market/heterogeneous/__init__.py @@ -1,2 +1,12 @@ -from .organizer import HeteroMapTableOrganizer -from .searcher import HeteroSearcher +from ...utils import is_torch_available +from ...logger import get_module_logger + +logger = get_module_logger("market_hetero") + +if not is_torch_available(verbose=False): + HeteroMapTableOrganizer = None + HeteroSearcher = None + logger.error("HeteroMapTableOrganizer and HeteroSearcher are not available because 'torch' is not installed!") +else: + from .organizer import HeteroMapTableOrganizer + from .searcher import HeteroSearcher diff --git a/learnware/market/heterogeneous/searcher.py b/learnware/market/heterogeneous/searcher.py index f261703..7194f9c 100644 --- a/learnware/market/heterogeneous/searcher.py +++ b/learnware/market/heterogeneous/searcher.py @@ -1,11 +1,9 @@ -import traceback -from typing import Tuple, List +from typing import Optional from .utils import is_hetero from ..base import BaseUserInfo, SearchResults from ..easy import EasySearcher from ..utils import parse_specification_type -from ...learnware import Learnware from ...logger import get_module_logger @@ -14,7 +12,7 @@ logger = get_module_logger("hetero_searcher") class HeteroSearcher(EasySearcher): def __call__( - self, user_info: BaseUserInfo, check_status: int = None, max_search_num: int = 5, search_method: str = "greedy" + self, user_info: BaseUserInfo, check_status: Optional[int] = None, max_search_num: int = 5, search_method: str = "greedy" ) -> SearchResults: """Search learnwares based on user_info from learnwares with check_status. Employs heterogeneous learnware search if specific requirements are met, otherwise resorts to homogeneous search methods. From c28fb14cf142e45ef994df87f75f2f7448098fde Mon Sep 17 00:00:00 2001 From: bxdd Date: Sun, 3 Dec 2023 14:07:19 +0800 Subject: [PATCH 31/64] [MNT] publish 0.2.0.6 beta version --- learnware/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/__init__.py b/learnware/__init__.py index eed5dde..b7fb924 100644 --- a/learnware/__init__.py +++ b/learnware/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0.5" +__version__ = "0.2.0.6" import os import json From 7ec8a4d443c0ab7483939510cfbda118785e753c Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Sun, 3 Dec 2023 14:06:21 +0800 Subject: [PATCH 32/64] [MNT] modify details to support docker in win --- learnware/client/container.py | 242 ++++++++++++++++----------- learnware/client/learnware_client.py | 10 +- 2 files changed, 152 insertions(+), 100 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index eb6809e..d94b5a6 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -201,6 +201,7 @@ class ModelDockerContainer(ModelContainer): self.conda_env = f"learnware_{shortuuid.uuid()}" self.docker_model_config = None self.docker_model_script_path = None + self.env_script = os.path.join(C.package_path, "client", "scripts", "install_env.py") # call init method of parent of parent class super(ModelDockerContainer, self).__init__(model_config, learnware_dirpath, build) @@ -228,6 +229,26 @@ class ModelDockerContainer(ModelContainer): ] for _cmd in environment_cmd: container.exec_run(_cmd) + + logger.info("Install learnware package in docker.") + result = container.exec_run( + " ".join( + [ + "conda", + "run", + "-n", + "base", + "--no-capture-output", + "python", + "-m", + "pip", + "install", + "learnware[full]", + ] + ) + ) + if result.exit_code != 0: + logger.error(f"Install learnware package in docker failed!\n{result.output.decode('utf-8')}") return container @staticmethod @@ -282,13 +303,11 @@ class ModelDockerContainer(ModelContainer): except docker.errors.NotFound as err: logger.error(f"Copy file from container failed due to {err}") - def _install_environment(self, learnware_dirpath, conda_env): + def _install_environment(self, conda_env): """Install environment of a learnware in docker container Parameters ---------- - learnware_dirpath : str - Path of the learnware dir conda_env : str a new conda environment will be created with the given name @@ -297,105 +316,137 @@ class ModelDockerContainer(ModelContainer): Exception Lack of the environment configuration file. """ - run_cmd_times = 10 + self.learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) + self._copy_file_to_container(self.learnware_dirpath, self.learnware_dirpath_container) + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - success_flag = False - logger.info(f"learnware_dir namelist: {os.listdir(learnware_dirpath)}") - - if "environment.yaml" in os.listdir(learnware_dirpath): - yaml_path: str = os.path.join(learnware_dirpath, "environment.yaml") - yaml_path_filter: str = os.path.join(tempdir, "environment_filter.yaml") - logger.info(f"checking the avaliabe conda packages for {conda_env}") - filter_nonexist_conda_packages_file(yaml_file=yaml_path, output_yaml_file=yaml_path_filter) - - yaml_path_filter_container = self._change_path_to_container(yaml_path_filter) - self._copy_file_to_container(yaml_path_filter, yaml_path_filter_container) - - # create environment - logger.info(f"Create and update conda env [{conda_env}] according to .yaml file") - for i in range(run_cmd_times): - result = self.docker_container.exec_run( - " ".join(["conda", "env", "create", "--name", f"{conda_env}", "--file", f"{yaml_path_filter_container}"]) - ) - if result.exit_code == 0: - success_flag = True - break - - elif "requirements.txt" in os.listdir(learnware_dirpath): - requirements_path: str = os.path.join(learnware_dirpath, "requirements.txt") - requirements_path_filter: str = os.path.join(tempdir, "requirements_filter.txt") - logger.info(f"checking the avaliabe pip packages for {conda_env}.") - filter_nonexist_pip_packages_file( - requirements_file=requirements_path, output_file=requirements_path_filter - ) - logger.info(f"Create empty conda env [{conda_env}] in docker.") - for i in range(run_cmd_times): - result = self.docker_container.exec_run( - " ".join(["conda", "create", "-y", "--name", f"{conda_env}", "python=3.8"]) - ) - if result.exit_code == 0: - break - logger.info(f"install pip requirements for conda env [{conda_env}] in docker.") - - requirements_path_filter_container = self._change_path_to_container(requirements_path_filter) - self._copy_file_to_container(requirements_path_filter, requirements_path_filter_container) - - for i in range(run_cmd_times): - result = self.docker_container.exec_run( - " ".join( - [ - "conda", - "run", - "-n", - f"{conda_env}", - "--no-capture-output", - "python", - "-m", - "pip", - "install", - "-r", - f"{requirements_path_filter_container}", - ] - ) - ) - if result.exit_code == 0: - success_flag = True - break - else: - raise Exception("Environment.yaml or requirements.txt not found in the learnware zip file.") - - if not success_flag: - logger.error(f"Install conda env [{conda_env}] in docker failed!") - - success_flag = False - logger.info(f"Install learnware package for conda env [{conda_env}] in docker.") - for i in range(run_cmd_times): + env_script_container_path = self._change_path_to_container(os.path.join(tempdir, "install_env.py")) + self._copy_file_to_container(self.env_script, env_script_container_path) + + logger.info("Install environment dependencies in docker.") result = self.docker_container.exec_run( " ".join( [ "conda", "run", "-n", - f"{conda_env}", + "base", "--no-capture-output", "python", - "-m", - "pip", - "install", - "learnware", + f"{env_script_container_path}", + "--learnware-dirpath", + f"{self.learnware_dirpath_container}", + "--conda-env", + f"{conda_env}", ] - ) + ), + stream=True ) - if result.exit_code == 0: - success_flag = True - break - - if not success_flag: - logger.error(f"Install learnware package for conda env [{conda_env}] in docker failed!") + for output in result.output: + print(output.decode("utf-8")) + + if result.exit_code != 0: + logger.error(f"Install environment dependencies in docker failed!") + + # run_cmd_times = 10 + # with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + # success_flag = False + # logger.info(f"learnware_dir namelist: {os.listdir(learnware_dirpath)}") + + # if "environment.yaml" in os.listdir(learnware_dirpath): + # yaml_path: str = os.path.join(learnware_dirpath, "environment.yaml") + # yaml_path_filter: str = os.path.join(tempdir, "environment_filter.yaml") + # logger.info(f"checking the avaliabe conda packages for {conda_env}") + # filter_nonexist_conda_packages_file(yaml_file=yaml_path, output_yaml_file=yaml_path_filter) + + # yaml_path_filter_container = self._change_path_to_container(yaml_path_filter) + # self._copy_file_to_container(yaml_path_filter, yaml_path_filter_container) + + # # create environment + # logger.info(f"Create and update conda env [{conda_env}] according to .yaml file") + # for i in range(run_cmd_times): + # result = self.docker_container.exec_run( + # " ".join(["conda", "env", "create", "--name", f"{conda_env}", "--file", f"{yaml_path_filter_container}"]) + # ) + # if result.exit_code == 0: + # success_flag = True + # break + + # elif "requirements.txt" in os.listdir(learnware_dirpath): + # requirements_path: str = os.path.join(learnware_dirpath, "requirements.txt") + # requirements_path_filter: str = os.path.join(tempdir, "requirements_filter.txt") + # logger.info(f"checking the avaliabe pip packages for {conda_env}.") + # filter_nonexist_pip_packages_file( + # requirements_file=requirements_path, output_file=requirements_path_filter + # ) + # logger.info(f"Create empty conda env [{conda_env}] in docker.") + # for i in range(run_cmd_times): + # result = self.docker_container.exec_run( + # " ".join(["conda", "create", "-y", "--name", f"{conda_env}", "python=3.8"]) + # ) + # if result.exit_code == 0: + # break + # logger.info(f"install pip requirements for conda env [{conda_env}] in docker.") + + # requirements_path_filter_container = self._change_path_to_container(requirements_path_filter) + # self._copy_file_to_container(requirements_path_filter, requirements_path_filter_container) + + # for i in range(run_cmd_times): + # result = self.docker_container.exec_run( + # " ".join( + # [ + # "conda", + # "run", + # "-n", + # f"{conda_env}", + # "--no-capture-output", + # "python", + # "-m", + # "pip", + # "install", + # "-r", + # f"{requirements_path_filter_container}", + # ] + # ) + # ) + # if result.exit_code == 0: + # success_flag = True + # break + # else: + # raise Exception("Environment.yaml or requirements.txt not found in the learnware zip file.") + + # if not success_flag: + # logger.error(f"Install conda env [{conda_env}] in docker failed!") + + # success_flag = False + # logger.info(f"Install learnware package for conda env [{conda_env}] in docker.") + # for i in range(run_cmd_times): + # result = self.docker_container.exec_run( + # " ".join( + # [ + # "conda", + # "run", + # "-n", + # f"{conda_env}", + # "--no-capture-output", + # "python", + # "-m", + # "pip", + # "install", + # "learnware", + # ] + # ) + # ) + # if result.exit_code == 0: + # success_flag = True + # break + + # if not success_flag: + # logger.error(f"Install learnware package for conda env [{conda_env}] in docker failed!") def _setup_env_and_metadata(self): """setup env and set the input and output shape by communicating with docker""" - self._install_environment(self.learnware_dirpath, self.conda_env) + self._install_environment(self.conda_env) with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: output_path = os.path.join(tempdir, "output.pkl") model_path = os.path.join(tempdir, "model.pkl") @@ -403,18 +454,16 @@ class ModelDockerContainer(ModelContainer): output_path_container = self._change_path_to_container(output_path) model_path_container = self._change_path_to_container(model_path) - learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = learnware_dirpath_container + "/" + docker_model_config["module_path"] - self._copy_file_to_container(self.learnware_dirpath, learnware_dirpath_container) + docker_model_config["module_path"] = self.learnware_dirpath_container + "/" + docker_model_config["module_path"] with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) self._copy_file_to_container(model_path, model_path_container) self._copy_file_to_container(self.model_script, self.docker_model_script_path) - self.docker_container.exec_run( + result = self.docker_container.exec_run( " ".join( [ "conda", @@ -431,6 +480,8 @@ class ModelDockerContainer(ModelContainer): ] ) ) + if result.exit_code != 0: + logger.error(f"Instantiate learnware in docker failed!\n{result.output.decode('utf-8')}") self._copy_file_from_container(output_path_container, output_path) with open(output_path, "rb") as output_fp: @@ -458,10 +509,9 @@ class ModelDockerContainer(ModelContainer): input_path_container = self._change_path_to_container(input_path) output_path_container = self._change_path_to_container(output_path) model_path_container = self._change_path_to_container(model_path) - learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = learnware_dirpath_container + "/" + docker_model_config["module_path"] + docker_model_config["module_path"] = self.learnware_dirpath_container + "/" + docker_model_config["module_path"] with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) @@ -471,7 +521,7 @@ class ModelDockerContainer(ModelContainer): self._copy_file_to_container(model_path, model_path_container) self._copy_file_to_container(input_path, input_path_container) - self.docker_container.exec_run( + result = self.docker_container.exec_run( " ".join( [ "conda", @@ -490,6 +540,8 @@ class ModelDockerContainer(ModelContainer): ] ) ) + if result.exit_code != 0: + logger.error(f"Run learnware in docker failed!\n{result.output.decode('utf-8')}") self._copy_file_from_container(output_path_container, output_path) with open(output_path, "rb") as output_fp: diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index bb5e9a9..2895e5c 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -12,11 +12,9 @@ from tqdm import tqdm from typing import Union, List, Optional from ..config import C -from .. import learnware from .container import LearnwaresContainer -from ..market import BaseChecker, EasySemanticChecker, EasyStatChecker +from ..market import BaseChecker from ..logger import get_module_logger -from ..specification import Specification from ..learnware import get_learnware_from_dirpath from ..market import BaseUserInfo from ..tests import get_semantic_specification @@ -384,7 +382,7 @@ class LearnwareClient: with open(os.path.join(tempdir, semantic_file), "r") as fin: semantic_specification = json.load(fin) - return learnware.get_learnware_from_dirpath(learnware_id, semantic_specification, tempdir) + return get_learnware_from_dirpath(learnware_id, semantic_specification, tempdir) learnware_list = [] if learnware_path is not None: @@ -417,12 +415,14 @@ class LearnwareClient: @staticmethod def _check_semantic_specification(semantic_spec): + from ..market import EasySemanticChecker + check_status, message = EasySemanticChecker.check_semantic_spec(semantic_spec) return check_status != BaseChecker.INVALID_LEARNWARE, message @staticmethod def _check_stat_specification(learnware): - from ..market import CondaChecker + from ..market import EasyStatChecker, CondaChecker stat_checker = CondaChecker(inner_checker=EasyStatChecker()) check_status, message = stat_checker(learnware) From f55a94e4442c9d475393de8e8d2681c2248a3d2e Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 3 Dec 2023 15:11:18 +0800 Subject: [PATCH 33/64] [FIX] fix conda deps filter --- learnware/client/package_utils.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/learnware/client/package_utils.py b/learnware/client/package_utils.py index dbc8679..267c090 100644 --- a/learnware/client/package_utils.py +++ b/learnware/client/package_utils.py @@ -21,7 +21,7 @@ def try_to_run(args, timeout=10, retry=3): return result.stdout.decode() except subprocess.TimeoutExpired as e: pass - + raise subprocess.TimeoutExpired(args, timeout) @@ -37,11 +37,11 @@ def parse_pip_requirement(line: str): split_ch_index = package_name.find(split_ch) if split_ch_index != -1: package_name = package_name[:split_ch_index] - + split_ch_index = package_version.find(split_ch) if split_ch_index != -1: - package_version = package_version[split_ch_index + 1:] - + package_version = package_version[split_ch_index + 1 :] + if package_version == package_name: package_version = "" @@ -71,6 +71,7 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: exist_packages: list of exist packages nonexist_packages: list of non-exist packages """ + def _filter_nonexist_pip_package_worker(package): # Return filtered package try: @@ -83,13 +84,13 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: return package except Exception as e: logger.error(e) - + return None - + exist_packages = [] nonexist_packages = [] packages = [package for package in packages if package is not None] - + with ThreadPoolExecutor(max_workers=max(os.cpu_count() // 5, 1)) as executor: results = executor.map(_filter_nonexist_pip_package_worker, packages) @@ -98,7 +99,7 @@ def filter_nonexist_pip_packages(packages: list) -> Tuple[List[str], List[str]]: exist_packages.append(result) else: nonexist_packages.append(package) - + return exist_packages, nonexist_packages @@ -129,7 +130,14 @@ def filter_nonexist_conda_packages(packages: list) -> Tuple[List[str], List[str] last_bracket = stdout.rfind("\n{") if last_bracket != -1: stdout = stdout[last_bracket:] - return json.loads(stdout).get("bad_deps", []) + + stdout_json = json.loads(stdout) + if "error" in stdout_json: + if "bad_deps" in stdout_json: + return stdout_json["bad_deps"] + elif "packages" in stdout_json: + return stdout_json["packages"] + return [] org_yaml = { "channels": ["defaults"], From 9756792cacebac910fba1cffde44aeed8241dd00 Mon Sep 17 00:00:00 2001 From: bxdd Date: Sun, 3 Dec 2023 15:22:58 +0800 Subject: [PATCH 34/64] [MNT] publish 0.2.0.7 beta version --- learnware/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/__init__.py b/learnware/__init__.py index b7fb924..6d86b3d 100644 --- a/learnware/__init__.py +++ b/learnware/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0.6" +__version__ = "0.2.0.7" import os import json From fdfd5764821ab14c249aeeaeeb9c5a5fc4efe185 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Sun, 3 Dec 2023 15:32:13 +0800 Subject: [PATCH 35/64] [MNT] modify logger details --- learnware/client/container.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index d94b5a6..65973a1 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -243,7 +243,7 @@ class ModelDockerContainer(ModelContainer): "-m", "pip", "install", - "learnware[full]", + "learnware", ] ) ) @@ -339,14 +339,10 @@ class ModelDockerContainer(ModelContainer): "--conda-env", f"{conda_env}", ] - ), - stream=True + ) ) - for output in result.output: - print(output.decode("utf-8")) - if result.exit_code != 0: - logger.error(f"Install environment dependencies in docker failed!") + logger.error(f"Install environment dependencies in docker failed!\n{result.output.decode('utf-8')}") # run_cmd_times = 10 # with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: From 9ae77efa5846280c970d33ebd87a422945318275 Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 3 Dec 2023 17:08:46 +0800 Subject: [PATCH 36/64] [MNT] add multiple attempts to install env --- learnware/client/container.py | 162 ++++++++-------------------------- 1 file changed, 38 insertions(+), 124 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 65973a1..6beede6 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -229,7 +229,7 @@ class ModelDockerContainer(ModelContainer): ] for _cmd in environment_cmd: container.exec_run(_cmd) - + logger.info("Install learnware package in docker.") result = container.exec_run( " ".join( @@ -274,7 +274,7 @@ class ModelDockerContainer(ModelContainer): return f"/tmp/{file_dir}/{file_name}" else: return f"/tmp/{file_name}" - + def _copy_file_to_container(self, local_path, container_path): directory_path = os.path.dirname(container_path) container_name = os.path.basename(container_path) @@ -316,129 +316,39 @@ class ModelDockerContainer(ModelContainer): Exception Lack of the environment configuration file. """ + run_cmd_times = 10 self.learnware_dirpath_container = self._change_path_to_container(self.learnware_dirpath) self._copy_file_to_container(self.learnware_dirpath, self.learnware_dirpath_container) - + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: env_script_container_path = self._change_path_to_container(os.path.join(tempdir, "install_env.py")) self._copy_file_to_container(self.env_script, env_script_container_path) - + + success_flag = False logger.info("Install environment dependencies in docker.") - result = self.docker_container.exec_run( - " ".join( - [ - "conda", - "run", - "-n", - "base", - "--no-capture-output", - "python", - f"{env_script_container_path}", - "--learnware-dirpath", - f"{self.learnware_dirpath_container}", - "--conda-env", - f"{conda_env}", - ] + for i in range(run_cmd_times): + result = self.docker_container.exec_run( + " ".join( + [ + "conda", + "run", + "-n", + "base", + "--no-capture-output", + "python", + f"{env_script_container_path}", + "--learnware-dirpath", + f"{self.learnware_dirpath_container}", + "--conda-env", + f"{conda_env}", + ] + ) ) - ) - if result.exit_code != 0: + if result.exit_code == 0: + success_flag = True + break + if not success_flag: logger.error(f"Install environment dependencies in docker failed!\n{result.output.decode('utf-8')}") - - # run_cmd_times = 10 - # with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - # success_flag = False - # logger.info(f"learnware_dir namelist: {os.listdir(learnware_dirpath)}") - - # if "environment.yaml" in os.listdir(learnware_dirpath): - # yaml_path: str = os.path.join(learnware_dirpath, "environment.yaml") - # yaml_path_filter: str = os.path.join(tempdir, "environment_filter.yaml") - # logger.info(f"checking the avaliabe conda packages for {conda_env}") - # filter_nonexist_conda_packages_file(yaml_file=yaml_path, output_yaml_file=yaml_path_filter) - - # yaml_path_filter_container = self._change_path_to_container(yaml_path_filter) - # self._copy_file_to_container(yaml_path_filter, yaml_path_filter_container) - - # # create environment - # logger.info(f"Create and update conda env [{conda_env}] according to .yaml file") - # for i in range(run_cmd_times): - # result = self.docker_container.exec_run( - # " ".join(["conda", "env", "create", "--name", f"{conda_env}", "--file", f"{yaml_path_filter_container}"]) - # ) - # if result.exit_code == 0: - # success_flag = True - # break - - # elif "requirements.txt" in os.listdir(learnware_dirpath): - # requirements_path: str = os.path.join(learnware_dirpath, "requirements.txt") - # requirements_path_filter: str = os.path.join(tempdir, "requirements_filter.txt") - # logger.info(f"checking the avaliabe pip packages for {conda_env}.") - # filter_nonexist_pip_packages_file( - # requirements_file=requirements_path, output_file=requirements_path_filter - # ) - # logger.info(f"Create empty conda env [{conda_env}] in docker.") - # for i in range(run_cmd_times): - # result = self.docker_container.exec_run( - # " ".join(["conda", "create", "-y", "--name", f"{conda_env}", "python=3.8"]) - # ) - # if result.exit_code == 0: - # break - # logger.info(f"install pip requirements for conda env [{conda_env}] in docker.") - - # requirements_path_filter_container = self._change_path_to_container(requirements_path_filter) - # self._copy_file_to_container(requirements_path_filter, requirements_path_filter_container) - - # for i in range(run_cmd_times): - # result = self.docker_container.exec_run( - # " ".join( - # [ - # "conda", - # "run", - # "-n", - # f"{conda_env}", - # "--no-capture-output", - # "python", - # "-m", - # "pip", - # "install", - # "-r", - # f"{requirements_path_filter_container}", - # ] - # ) - # ) - # if result.exit_code == 0: - # success_flag = True - # break - # else: - # raise Exception("Environment.yaml or requirements.txt not found in the learnware zip file.") - - # if not success_flag: - # logger.error(f"Install conda env [{conda_env}] in docker failed!") - - # success_flag = False - # logger.info(f"Install learnware package for conda env [{conda_env}] in docker.") - # for i in range(run_cmd_times): - # result = self.docker_container.exec_run( - # " ".join( - # [ - # "conda", - # "run", - # "-n", - # f"{conda_env}", - # "--no-capture-output", - # "python", - # "-m", - # "pip", - # "install", - # "learnware", - # ] - # ) - # ) - # if result.exit_code == 0: - # success_flag = True - # break - - # if not success_flag: - # logger.error(f"Install learnware package for conda env [{conda_env}] in docker failed!") def _setup_env_and_metadata(self): """setup env and set the input and output shape by communicating with docker""" @@ -450,15 +360,17 @@ class ModelDockerContainer(ModelContainer): output_path_container = self._change_path_to_container(output_path) model_path_container = self._change_path_to_container(model_path) - + docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = self.learnware_dirpath_container + "/" + docker_model_config["module_path"] + docker_model_config["module_path"] = ( + self.learnware_dirpath_container + "/" + docker_model_config["module_path"] + ) with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) self._copy_file_to_container(model_path, model_path_container) self._copy_file_to_container(self.model_script, self.docker_model_script_path) - + result = self.docker_container.exec_run( " ".join( [ @@ -501,13 +413,15 @@ class ModelDockerContainer(ModelContainer): input_path = os.path.join(tempdir, "input.pkl") output_path = os.path.join(tempdir, "output.pkl") model_path = os.path.join(tempdir, "model.pkl") - + input_path_container = self._change_path_to_container(input_path) output_path_container = self._change_path_to_container(output_path) model_path_container = self._change_path_to_container(model_path) - + docker_model_config = self.model_config.copy() - docker_model_config["module_path"] = self.learnware_dirpath_container + "/" + docker_model_config["module_path"] + docker_model_config["module_path"] = ( + self.learnware_dirpath_container + "/" + docker_model_config["module_path"] + ) with open(model_path, "wb") as model_fp: pickle.dump(docker_model_config, model_fp) @@ -602,6 +516,7 @@ class LearnwaresContainer: ] else: self._docker_container = ModelDockerContainer._generate_docker_container() + atexit.register(ModelDockerContainer._destroy_docker_container, self._docker_container) self.learnware_containers = [ Learnware( _learnware.id, @@ -613,7 +528,6 @@ class LearnwaresContainer: ) for _learnware in self.learnware_list ] - atexit.register(ModelDockerContainer._destroy_docker_container, self._docker_container) model_list = [_learnware.get_model() for _learnware in self.learnware_containers] with ThreadPoolExecutor(max_workers=max(os.cpu_count() // 2, 1)) as executor: From a64f0cfd26fd6cabd8c34d1c21796d5d1d41669c Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 3 Dec 2023 21:30:10 +0800 Subject: [PATCH 37/64] [FIX] add clean conda cache --- learnware/client/container.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/learnware/client/container.py b/learnware/client/container.py index 6beede6..ccae0ba 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -218,6 +218,7 @@ class ModelDockerContainer(ModelContainer): "tty": True, "command": "bash", "environment": {"http_proxy": http_proxy, "https_proxy": https_proxy}, + "pids_limit": -1, } container = client.containers.run(**container_config) environment_cmd = [ @@ -249,6 +250,8 @@ class ModelDockerContainer(ModelContainer): ) if result.exit_code != 0: logger.error(f"Install learnware package in docker failed!\n{result.output.decode('utf-8')}") + container.exec_run("conda clean --all") + return container @staticmethod @@ -347,6 +350,8 @@ class ModelDockerContainer(ModelContainer): if result.exit_code == 0: success_flag = True break + else: + self.docker_container.exec_run("conda clean --all") if not success_flag: logger.error(f"Install environment dependencies in docker failed!\n{result.output.decode('utf-8')}") From 7c76c675ac827ada00e24df5c5f122c5ae0a2d97 Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 3 Dec 2023 22:51:10 +0800 Subject: [PATCH 38/64] [MNT] change docker container to serial initialization --- learnware/client/container.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index ccae0ba..85be5c8 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -250,7 +250,6 @@ class ModelDockerContainer(ModelContainer): ) if result.exit_code != 0: logger.error(f"Install learnware package in docker failed!\n{result.output.decode('utf-8')}") - container.exec_run("conda clean --all") return container @@ -535,9 +534,16 @@ class LearnwaresContainer: ] model_list = [_learnware.get_model() for _learnware in self.learnware_containers] - with ThreadPoolExecutor(max_workers=max(os.cpu_count() // 2, 1)) as executor: - results = executor.map(self._initialize_model_container, model_list, [self.ignore_error] * len(model_list)) - self.results = list(results) + if self.mode == "conda": + with ThreadPoolExecutor(max_workers=max(os.cpu_count() // 2, 1)) as executor: + results = executor.map( + self._initialize_model_container, model_list, [self.ignore_error] * len(model_list) + ) + self.results = list(results) + else: + self.results = [] + for model_item in model_list: + self.results.append(self._initialize_model_container(model_item, self.ignore_error)) if sum(self.results) < len(self.learnware_list): logger.warning( From 72214b8116e84b8c725b4b52a113dc3e26baf8e5 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 13:26:06 +0800 Subject: [PATCH 39/64] [MNT] add type check in spec load --- learnware/specification/regular/image/rkme.py | 28 +++++++++++++------ learnware/specification/regular/table/rkme.py | 21 +++++++++----- .../specification/system/hetero_table.py | 14 +++++++--- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/learnware/specification/regular/image/rkme.py b/learnware/specification/regular/image/rkme.py index 81b68f7..821924b 100644 --- a/learnware/specification/regular/image/rkme.py +++ b/learnware/specification/regular/image/rkme.py @@ -1,7 +1,6 @@ from __future__ import annotations import codecs -import copy import functools import json import os @@ -17,8 +16,11 @@ from tqdm import tqdm from . import cnn_gp from ..base import RegularStatSpecification from ..table.rkme import rkme_solve_qp +from ....logger import get_module_logger from ....utils import choose_device, allocate_cuda_idx +logger = get_module_logger("image_rkme") + class RKMEImageSpecification(RegularStatSpecification): # INNER_PRODUCT_COUNT = 0 @@ -127,8 +129,10 @@ class RKMEImageSpecification(RegularStatSpecification): try: from torchvision.transforms import Resize except ModuleNotFoundError: - raise ModuleNotFoundError(f"RKMEImageSpecification is not available because 'torchvision' is not installed! Please install it manually." ) - + raise ModuleNotFoundError( + f"RKMEImageSpecification is not available because 'torchvision' is not installed! Please install it manually." + ) + if X.shape[2] != RKMEImageSpecification.IMAGE_WIDTH or X.shape[3] != RKMEImageSpecification.IMAGE_WIDTH: X = Resize((RKMEImageSpecification.IMAGE_WIDTH, RKMEImageSpecification.IMAGE_WIDTH), antialias=True)(X) @@ -154,12 +158,14 @@ class RKMEImageSpecification(RegularStatSpecification): with torch.no_grad(): x_features = self._generate_random_feature(X_train, random_models=random_models) self._update_beta(x_features, nonnegative_beta, random_models=random_models) - + try: import torch_optimizer except ModuleNotFoundError: - raise ModuleNotFoundError(f"RKMEImageSpecification is not available because 'torch-optimizer' is not installed! Please install it manually.") - + raise ModuleNotFoundError( + f"RKMEImageSpecification is not available because 'torch-optimizer' is not installed! Please install it manually." + ) + optimizer = torch_optimizer.AdaBelief([{"params": [self.z]}], lr=step_size, eps=1e-16) for _ in tqdm(range(steps)) if verbose else range(steps): @@ -385,9 +391,13 @@ class RKMEImageSpecification(RegularStatSpecification): self.beta = self.beta.to(self._device) self.z = self.z.to(self._device) - return True - else: - return False + if self.type == self.__class__.__name__: + logger.error( + f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" + ) + return True + + return False def _get_zca_matrix(X, reg_coef=0.1): diff --git a/learnware/specification/regular/table/rkme.py b/learnware/specification/regular/table/rkme.py index f335f4d..af8ff20 100644 --- a/learnware/specification/regular/table/rkme.py +++ b/learnware/specification/regular/table/rkme.py @@ -140,15 +140,17 @@ class RKMETableSpecification(RegularStatSpecification): if isinstance(X, np.ndarray): X = X.astype("float32") X = torch.from_numpy(X) - + X = X.to(self._device) - + try: from fast_pytorch_kmeans import KMeans except ModuleNotFoundError: - raise ModuleNotFoundError(f"RKMETableSpecification is not available because 'fast_pytorch_kmeans' is not installed! Please install it manually." ) + raise ModuleNotFoundError( + f"RKMETableSpecification is not available because 'fast_pytorch_kmeans' is not installed! Please install it manually." + ) - kmeans = KMeans(n_clusters=K, mode='euclidean', max_iter=100, verbose=0) + kmeans = KMeans(n_clusters=K, mode="euclidean", max_iter=100, verbose=0) kmeans.fit(X) self.z = kmeans.centroids.double() @@ -455,9 +457,14 @@ class RKMETableSpecification(RegularStatSpecification): for d in self.get_states(): if d in rkme_load.keys(): setattr(self, d, rkme_load[d]) - return True - else: - return False + + if self.type == self.__class__.__name__: + logger.error( + f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" + ) + return True + + return False class RKMEStatSpecification(RKMETableSpecification): diff --git a/learnware/specification/system/hetero_table.py b/learnware/specification/system/hetero_table.py index 3987040..ad9d7a8 100644 --- a/learnware/specification/system/hetero_table.py +++ b/learnware/specification/system/hetero_table.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import copy import json import torch import codecs @@ -10,8 +9,11 @@ import numpy as np from .base import SystemStatSpecification from ..regular import RKMETableSpecification from ..regular.table.rkme import torch_rbf_kernel +from ...logger import get_module_logger from ...utils import choose_device, allocate_cuda_idx +logger = get_module_logger("hetero_map_table_spec") + class HeteroMapTableSpecification(SystemStatSpecification): """Heterogeneous Map-Table Specification""" @@ -135,9 +137,13 @@ class HeteroMapTableSpecification(SystemStatSpecification): if d in embedding_load.keys(): setattr(self, d, embedding_load[d]) - return True - else: - return False + if self.type == self.__class__.__name__: + logger.error( + f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" + ) + return True + + return False def save(self, filepath: str) -> bool: """Save the computed HeteroMapTableSpecification to a specified path in JSON format. From a7b8f8d12fa7e3bf27525b39b67d3b4bb8e8e5e6 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 13:36:16 +0800 Subject: [PATCH 40/64] [FIX] add spec.dist() check --- learnware/market/easy/checker.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/learnware/market/easy/checker.py b/learnware/market/easy/checker.py index 7995d94..57cae22 100644 --- a/learnware/market/easy/checker.py +++ b/learnware/market/easy/checker.py @@ -95,8 +95,8 @@ class EasyStatChecker(BaseChecker): logger.warning(f"The learnware [{learnware.id}] is instantiated failed! Due to {e}.") return self.INVALID_LEARNWARE, traceback.format_exc() try: - learnware_model = learnware.get_model() # Check input shape + learnware_model = learnware.get_model() input_shape = learnware_model.input_shape if semantic_spec["Data"]["Values"][0] == "Table" and input_shape != ( @@ -106,14 +106,18 @@ class EasyStatChecker(BaseChecker): logger.warning(message) return self.INVALID_LEARNWARE, message + # Check statistical specification spec_type = parse_specification_type(learnware.get_specification().stat_spec) if spec_type is None: message = f"No valid specification is found in stat spec {spec_type}" logger.warning(message) return self.INVALID_LEARNWARE, message + # Check if statistical specification is computable in dist() + stat_spec = learnware.get_specification().get_stat_spec_by_name(spec_type) + stat_spec.dist(stat_spec) + if spec_type == "RKMETableSpecification": - stat_spec = learnware.get_specification().get_stat_spec_by_name(spec_type) if not isinstance(input_shape, tuple) or not all(isinstance(item, int) for item in input_shape): raise ValueError( f"For RKMETableSpecification, input_shape should be tuple of int, but got {input_shape}" @@ -124,14 +128,17 @@ class EasyStatChecker(BaseChecker): logger.warning(message) return self.INVALID_LEARNWARE, message inputs = np.random.randn(10, *input_shape) + elif spec_type == "RKMETextSpecification": inputs = EasyStatChecker._generate_random_text_list(10) + elif spec_type == "RKMEImageSpecification": if not isinstance(input_shape, tuple) or not all(isinstance(item, int) for item in input_shape): raise ValueError( f"For RKMEImageSpecification, input_shape should be tuple of int, but got {input_shape}" ) inputs = np.random.randint(0, 255, size=(10, *input_shape)) + else: raise ValueError(f"not supported spec type for spec_type = {spec_type}") From d9a0ccc9588e771f1f0a7799ec688fe5e31458ec Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 13:46:47 +0800 Subject: [PATCH 41/64] [FIX] fix details --- learnware/specification/regular/image/rkme.py | 2 +- learnware/specification/regular/table/rkme.py | 2 +- learnware/specification/system/hetero_table.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/learnware/specification/regular/image/rkme.py b/learnware/specification/regular/image/rkme.py index 821924b..448ae62 100644 --- a/learnware/specification/regular/image/rkme.py +++ b/learnware/specification/regular/image/rkme.py @@ -391,7 +391,7 @@ class RKMEImageSpecification(RegularStatSpecification): self.beta = self.beta.to(self._device) self.z = self.z.to(self._device) - if self.type == self.__class__.__name__: + if self.type != self.__class__.__name__: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) diff --git a/learnware/specification/regular/table/rkme.py b/learnware/specification/regular/table/rkme.py index af8ff20..78e4996 100644 --- a/learnware/specification/regular/table/rkme.py +++ b/learnware/specification/regular/table/rkme.py @@ -458,7 +458,7 @@ class RKMETableSpecification(RegularStatSpecification): if d in rkme_load.keys(): setattr(self, d, rkme_load[d]) - if self.type == self.__class__.__name__: + if self.type != self.__class__.__name__: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) diff --git a/learnware/specification/system/hetero_table.py b/learnware/specification/system/hetero_table.py index ad9d7a8..5bec254 100644 --- a/learnware/specification/system/hetero_table.py +++ b/learnware/specification/system/hetero_table.py @@ -137,7 +137,7 @@ class HeteroMapTableSpecification(SystemStatSpecification): if d in embedding_load.keys(): setattr(self, d, embedding_load[d]) - if self.type == self.__class__.__name__: + if self.type != self.__class__.__name__: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) From 61ed6da6c43cadd5624e1249f5d94c4a11d94ace Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 13:57:49 +0800 Subject: [PATCH 42/64] [FIX] fix bugs --- learnware/specification/regular/image/rkme.py | 5 +++-- learnware/specification/regular/table/rkme.py | 7 ++++--- learnware/specification/system/hetero_table.py | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/learnware/specification/regular/image/rkme.py b/learnware/specification/regular/image/rkme.py index 448ae62..32809b7 100644 --- a/learnware/specification/regular/image/rkme.py +++ b/learnware/specification/regular/image/rkme.py @@ -391,11 +391,12 @@ class RKMEImageSpecification(RegularStatSpecification): self.beta = self.beta.to(self._device) self.z = self.z.to(self._device) - if self.type != self.__class__.__name__: + if self.type == self.__class__.__name__: + return True + else: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) - return True return False diff --git a/learnware/specification/regular/table/rkme.py b/learnware/specification/regular/table/rkme.py index 78e4996..046a956 100644 --- a/learnware/specification/regular/table/rkme.py +++ b/learnware/specification/regular/table/rkme.py @@ -6,7 +6,7 @@ import json import codecs import scipy import numpy as np -from qpsolvers import solve_qp, Problem, solve_problem +from qpsolvers import Problem, solve_problem from collections import Counter from typing import Any, Union @@ -458,11 +458,12 @@ class RKMETableSpecification(RegularStatSpecification): if d in rkme_load.keys(): setattr(self, d, rkme_load[d]) - if self.type != self.__class__.__name__: + if self.type == self.__class__.__name__: + return True + else: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) - return True return False diff --git a/learnware/specification/system/hetero_table.py b/learnware/specification/system/hetero_table.py index 5bec254..2a8ab6b 100644 --- a/learnware/specification/system/hetero_table.py +++ b/learnware/specification/system/hetero_table.py @@ -137,11 +137,12 @@ class HeteroMapTableSpecification(SystemStatSpecification): if d in embedding_load.keys(): setattr(self, d, embedding_load[d]) - if self.type != self.__class__.__name__: + if self.type == self.__class__.__name__: + return True + else: logger.error( f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" ) - return True return False From 13bb47359ddb453616dc9921ba4b28cb115fd0ac Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 14:10:18 +0800 Subject: [PATCH 43/64] [FIX] fix bugs in test_hetero --- tests/test_hetero_market/test_hetero.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_hetero_market/test_hetero.py b/tests/test_hetero_market/test_hetero.py index 41b4261..7b0740b 100644 --- a/tests/test_hetero_market/test_hetero.py +++ b/tests/test_hetero_market/test_hetero.py @@ -5,10 +5,10 @@ import copy import joblib import zipfile import numpy as np +import multiprocessing from sklearn.linear_model import Ridge from sklearn.datasets import make_regression from shutil import copyfile, rmtree -from multiprocessing import Pool from learnware.client import LearnwareClient from sklearn.metrics import mean_squared_error @@ -121,7 +121,8 @@ class TestMarket(unittest.TestCase): dir_path = os.path.join(curr_root, "learnware_pool") # Execute multi-process checking using Pool - with Pool() as pool: + mp_context = multiprocessing.get_context("spawn") + with mp_context.Pool() as pool: results = pool.starmap(check_learnware, [(name, dir_path) for name in os.listdir(dir_path)]) # Use an assert statement to ensure that all checks return True From b3c50926d42b59e82db1dc155c4b86b98b64cb92 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 14:54:48 +0800 Subject: [PATCH 44/64] [MNT] add dist method in base stat_spec class --- learnware/specification/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/learnware/specification/base.py b/learnware/specification/base.py index 064173d..6b1c5f5 100644 --- a/learnware/specification/base.py +++ b/learnware/specification/base.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import numpy as np from typing import Dict @@ -22,6 +24,9 @@ class BaseStatSpecification: def get_states(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + def dist(self, stat_spec: BaseStatSpecification): + raise NotImplementedError("dist is not implemented") + def save(self, filepath: str): """Save the statistical specification into file in filepath From d10d87914ce68a3374b2d1a590ab6ede9ce2d413 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 15:12:53 +0800 Subject: [PATCH 45/64] [FIX] fix bugs which assumes type of stat_spec is class_name --- learnware/learnware/utils.py | 5 ++--- learnware/specification/regular/image/rkme.py | 13 +++---------- learnware/specification/regular/table/rkme.py | 11 ++--------- learnware/specification/system/hetero_table.py | 11 ++--------- 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/learnware/learnware/utils.py b/learnware/learnware/utils.py index e677c58..ebc4243 100644 --- a/learnware/learnware/utils.py +++ b/learnware/learnware/utils.py @@ -44,7 +44,6 @@ def get_stat_spec_from_config(stat_spec: dict) -> BaseStatSpecification: raise TypeError( f"Statistic specification must be type of BaseStatSpecification, not {BaseStatSpecification.__class__.__name__}" ) - if stat_spec_inst.load(stat_spec["file_name"]) is False: - raise ValueError("Load statistic specification failed!") - + stat_spec_inst.load(stat_spec["file_name"]) + return stat_spec_inst diff --git a/learnware/specification/regular/image/rkme.py b/learnware/specification/regular/image/rkme.py index 32809b7..84222f6 100644 --- a/learnware/specification/regular/image/rkme.py +++ b/learnware/specification/regular/image/rkme.py @@ -383,23 +383,16 @@ class RKMEImageSpecification(RegularStatSpecification): rkme_load = json.loads(obj_text) rkme_load["z"] = torch.from_numpy(np.array(rkme_load["z"], dtype="float32")) rkme_load["beta"] = torch.from_numpy(np.array(rkme_load["beta"], dtype="float64")) - + for d in self.get_states(): if d in rkme_load.keys(): + if d == "type" and rkme_load[d] != self.type: + raise TypeError(f"The type of loaded RKME ({rkme_load[d]}) is different from the expected type ({self.type})!") setattr(self, d, rkme_load[d]) self.beta = self.beta.to(self._device) self.z = self.z.to(self._device) - if self.type == self.__class__.__name__: - return True - else: - logger.error( - f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" - ) - - return False - def _get_zca_matrix(X, reg_coef=0.1): X_flat = X.reshape(X.shape[0], -1) diff --git a/learnware/specification/regular/table/rkme.py b/learnware/specification/regular/table/rkme.py index 046a956..abecf6f 100644 --- a/learnware/specification/regular/table/rkme.py +++ b/learnware/specification/regular/table/rkme.py @@ -456,17 +456,10 @@ class RKMETableSpecification(RegularStatSpecification): for d in self.get_states(): if d in rkme_load.keys(): + if d == "type" and rkme_load[d] != self.type: + raise TypeError(f"The type of loaded RKME ({rkme_load[d]}) is different from the expected type ({self.type})!") setattr(self, d, rkme_load[d]) - if self.type == self.__class__.__name__: - return True - else: - logger.error( - f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" - ) - - return False - class RKMEStatSpecification(RKMETableSpecification): """nickname for RKMETableSpecification, for compatibility currently. diff --git a/learnware/specification/system/hetero_table.py b/learnware/specification/system/hetero_table.py index 2a8ab6b..4726f83 100644 --- a/learnware/specification/system/hetero_table.py +++ b/learnware/specification/system/hetero_table.py @@ -135,17 +135,10 @@ class HeteroMapTableSpecification(SystemStatSpecification): for d in self.get_states(): if d in embedding_load.keys(): + if d == "type" and embedding_load[d] != self.type: + raise TypeError(f"The type of loaded RKME ({embedding_load[d]}) is different from the expected type ({self.type})!") setattr(self, d, embedding_load[d]) - if self.type == self.__class__.__name__: - return True - else: - logger.error( - f"The type of loaded RKME ({self.type}) is different from the expected type ({self.__class__.__name__})!" - ) - - return False - def save(self, filepath: str) -> bool: """Save the computed HeteroMapTableSpecification to a specified path in JSON format. From 7bd8e89508d9a5d61332875c846ba9ec430aa393 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 15:36:53 +0800 Subject: [PATCH 46/64] [MNT] make clean docker container always not be interrupted --- learnware/client/container.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 85be5c8..b549fa5 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -507,6 +507,14 @@ class LearnwaresContainer: self.cleanup = cleanup self.ignore_error = ignore_error + @staticmethod + def _destroy_docker_container(container): + try: + ModelDockerContainer._destroy_docker_container(container) + except KeyboardInterrupt: + logger.warning("The KeyboardInterrupt is ignored when removing the container env!") + LearnwaresContainer._destroy_docker_container(container) + def __enter__(self): if self.mode == "conda": self.learnware_containers = [ @@ -519,8 +527,8 @@ class LearnwaresContainer: for _learnware in self.learnware_list ] else: + atexit.register(self._destroy_docker_container, self._docker_container) self._docker_container = ModelDockerContainer._generate_docker_container() - atexit.register(ModelDockerContainer._destroy_docker_container, self._docker_container) self.learnware_containers = [ Learnware( _learnware.id, From b084dd87e11b531bcebea311e9ea0519899622e9 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 16:37:55 +0800 Subject: [PATCH 47/64] [FIX] fix docker remove bugs --- learnware/client/container.py | 75 +++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index b549fa5..399b4ab 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -58,7 +58,7 @@ class ModelContainer(BaseModel): self.remove_env() except Exception as err: self.cleanup_flag = True - raise err + logger.error(f"Failed to remove container env due to {err}!") def _setup_env_and_metadata(self): raise NotImplementedError("_setup_env_and_metadata method is not implemented!") @@ -221,40 +221,44 @@ class ModelDockerContainer(ModelContainer): "pids_limit": -1, } container = client.containers.run(**container_config) - environment_cmd = [ - "pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple", - "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/", - "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/", - "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/", - "conda config --set show_channel_urls yes", - ] - for _cmd in environment_cmd: - container.exec_run(_cmd) + + try: + environment_cmd = [ + "pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple", + "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/", + "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/", + "conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/", + "conda config --set show_channel_urls yes", + ] + for _cmd in environment_cmd: + container.exec_run(_cmd) - logger.info("Install learnware package in docker.") - result = container.exec_run( - " ".join( - [ - "conda", - "run", - "-n", - "base", - "--no-capture-output", - "python", - "-m", - "pip", - "install", - "learnware", - ] + logger.info("Install learnware package in docker.") + result = container.exec_run( + " ".join( + [ + "conda", + "run", + "-n", + "base", + "--no-capture-output", + "python", + "-m", + "pip", + "install", + "learnware", + ] + ) ) - ) - if result.exit_code != 0: - logger.error(f"Install learnware package in docker failed!\n{result.output.decode('utf-8')}") - + if result.exit_code != 0: + logger.error(f"Install learnware package in docker failed!\n{result.output.decode('utf-8')}") + except KeyboardInterrupt: + logger.error("The container docker generation and setup procedure is KeyboardInterrupted!") + return container return container @staticmethod - def _destroy_docker_container(docker_container): + def _destroy_docker_container(docker_container) -> None: if isinstance(docker_container, docker.models.containers.Container): client = docker.from_env() container_ids = [container.id for container in client.containers.list()] @@ -507,13 +511,13 @@ class LearnwaresContainer: self.cleanup = cleanup self.ignore_error = ignore_error - @staticmethod - def _destroy_docker_container(container): + def _destroy_docker_container(self): try: - ModelDockerContainer._destroy_docker_container(container) + if self._docker_container is not None: + ModelDockerContainer._destroy_docker_container(self._docker_container) except KeyboardInterrupt: logger.warning("The KeyboardInterrupt is ignored when removing the container env!") - LearnwaresContainer._destroy_docker_container(container) + self._destroy_docker_container() def __enter__(self): if self.mode == "conda": @@ -527,7 +531,8 @@ class LearnwaresContainer: for _learnware in self.learnware_list ] else: - atexit.register(self._destroy_docker_container, self._docker_container) + self._docker_container = None + atexit.register(self._destroy_docker_container) self._docker_container = ModelDockerContainer._generate_docker_container() self.learnware_containers = [ Learnware( From 731110601d06dcddf799fce878184c087ca4536f Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 16:46:38 +0800 Subject: [PATCH 48/64] [MNT] add log info when removing container env --- learnware/client/container.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 399b4ab..8d9caae 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -89,6 +89,7 @@ class ModelCondaContainer(ModelContainer): def _remove_env(self): remove_enviroment(self.conda_env) + logger.info(f"Conda env {self.conda_env} is removed.") def _setup_env_and_metadata(self): with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: @@ -266,9 +267,9 @@ class ModelDockerContainer(ModelContainer): if docker_container.id in container_ids: docker_container.stop() docker_container.remove() - logger.info("Docker container is stopped and removed.") + logger.info(f"Docker container {docker_container.id} is stopped and removed.") else: - logger.info("Docker container has already been removed.") + logger.info(f"Docker container {docker_container.id} has already been removed.") else: logger.error("Type of docker_container is not docker.models.containers.Container.") From 90bdaa00eeb4c973b1b5f2cf015bd4ec36acdc02 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 17:10:08 +0800 Subject: [PATCH 49/64] [MNT] modify log to add container id info --- learnware/client/container.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/learnware/client/container.py b/learnware/client/container.py index 8d9caae..6c9bdf1 100644 --- a/learnware/client/container.py +++ b/learnware/client/container.py @@ -86,6 +86,7 @@ class ModelCondaContainer(ModelContainer): def _init_env(self): install_environment(self.learnware_dirpath, self.conda_env) + logger.info(f"Conda env {self.conda_env} is generated.") def _remove_env(self): remove_enviroment(self.conda_env) @@ -222,6 +223,7 @@ class ModelDockerContainer(ModelContainer): "pids_limit": -1, } container = client.containers.run(**container_config) + logger.info(f"Docker container {container.id[:12]} is generated.") try: environment_cmd = [ @@ -263,13 +265,13 @@ class ModelDockerContainer(ModelContainer): if isinstance(docker_container, docker.models.containers.Container): client = docker.from_env() container_ids = [container.id for container in client.containers.list()] - + if docker_container.id in container_ids: docker_container.stop() docker_container.remove() - logger.info(f"Docker container {docker_container.id} is stopped and removed.") + logger.info(f"Docker container {docker_container.id[:12]} is stopped and removed.") else: - logger.info(f"Docker container {docker_container.id} has already been removed.") + logger.info(f"Docker container {docker_container.id[:12]} has already been removed.") else: logger.error("Type of docker_container is not docker.models.containers.Container.") From 4228012194b2b786196ca8ab0d4eb28742fc1b04 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 19:17:44 +0800 Subject: [PATCH 50/64] [MNT] add dist.isfinite check --- learnware/market/easy/checker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/learnware/market/easy/checker.py b/learnware/market/easy/checker.py index 57cae22..a7cea3b 100644 --- a/learnware/market/easy/checker.py +++ b/learnware/market/easy/checker.py @@ -115,7 +115,11 @@ class EasyStatChecker(BaseChecker): # Check if statistical specification is computable in dist() stat_spec = learnware.get_specification().get_stat_spec_by_name(spec_type) - stat_spec.dist(stat_spec) + distance = stat_spec.dist(stat_spec) + if not np.isfinite(distance): + message = f"The distance between statistical specifications is not finite, where distance={distance}" + logger.warning(message) + return self.INVALID_LEARNWARE, message if spec_type == "RKMETableSpecification": if not isinstance(input_shape, tuple) or not all(isinstance(item, int) for item in input_shape): From ec5c46bc6cf01a88fd964b1044251bdfe53f03cc Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 19:18:17 +0800 Subject: [PATCH 51/64] [MNT] filter learnware with infinite dist --- learnware/market/easy/searcher.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/learnware/market/easy/searcher.py b/learnware/market/easy/searcher.py index 48bbfa8..f08b9b2 100644 --- a/learnware/market/easy/searcher.py +++ b/learnware/market/easy/searcher.py @@ -538,14 +538,20 @@ class EasyStatSearcher(BaseSearcher): both lists are sorted by mmd dist """ rkme_list = [learnware.specification.get_stat_spec_by_name(self.stat_spec_type) for learnware in learnware_list] - mmd_dist_list = [] - for rkme in rkme_list: - mmd_dist = rkme.dist(user_rkme) - mmd_dist_list.append(mmd_dist) + filtered_idx_list, mmd_dist_list = [], [] + for idx in range(len(rkme_list)): + mmd_dist = rkme_list[idx].dist(user_rkme) + if np.isfinite(mmd_dist): + mmd_dist_list.append(mmd_dist) + filtered_idx_list.append(idx) + else: + logger.warning( + f"The distance between user_spec and learnware_spec (id: {learnware_list[idx].id}) is not finite, where distance is {mmd_dist}" + ) - sorted_idx_list = sorted(range(len(learnware_list)), key=lambda k: mmd_dist_list[k]) + sorted_idx_list = sorted(range(len(mmd_dist_list)), key=lambda k: mmd_dist_list[k]) sorted_dist_list = [mmd_dist_list[idx] for idx in sorted_idx_list] - sorted_learnware_list = [learnware_list[idx] for idx in sorted_idx_list] + sorted_learnware_list = [learnware_list[filtered_idx_list[idx]] for idx in sorted_idx_list] return sorted_dist_list, sorted_learnware_list @@ -561,6 +567,9 @@ class EasyStatSearcher(BaseSearcher): raise KeyError("No supported stat specification is given in the user info") user_rkme = user_info.stat_info[self.stat_spec_type] + if not np.isfinite(user_rkme.dist(user_rkme)): + raise ValueError("The distance between uploaded statistical specifications is not finite!") + learnware_list = self._filter_by_rkme_spec_metadata(learnware_list, user_rkme) logger.info(f"After filter by rkme dimension, learnware_list length is {len(learnware_list)}") From 9bba4146f1033230f8de724d17cbafebe27c6c81 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 19:23:22 +0800 Subject: [PATCH 52/64] [MNT] modify details --- learnware/market/easy/checker.py | 2 +- learnware/market/easy/searcher.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/learnware/market/easy/checker.py b/learnware/market/easy/checker.py index a7cea3b..95c0f1a 100644 --- a/learnware/market/easy/checker.py +++ b/learnware/market/easy/checker.py @@ -115,7 +115,7 @@ class EasyStatChecker(BaseChecker): # Check if statistical specification is computable in dist() stat_spec = learnware.get_specification().get_stat_spec_by_name(spec_type) - distance = stat_spec.dist(stat_spec) + distance = float(stat_spec.dist(stat_spec)) if not np.isfinite(distance): message = f"The distance between statistical specifications is not finite, where distance={distance}" logger.warning(message) diff --git a/learnware/market/easy/searcher.py b/learnware/market/easy/searcher.py index f08b9b2..7f6690f 100644 --- a/learnware/market/easy/searcher.py +++ b/learnware/market/easy/searcher.py @@ -540,7 +540,7 @@ class EasyStatSearcher(BaseSearcher): rkme_list = [learnware.specification.get_stat_spec_by_name(self.stat_spec_type) for learnware in learnware_list] filtered_idx_list, mmd_dist_list = [], [] for idx in range(len(rkme_list)): - mmd_dist = rkme_list[idx].dist(user_rkme) + mmd_dist = float(rkme_list[idx].dist(user_rkme)) if np.isfinite(mmd_dist): mmd_dist_list.append(mmd_dist) filtered_idx_list.append(idx) @@ -567,7 +567,7 @@ class EasyStatSearcher(BaseSearcher): raise KeyError("No supported stat specification is given in the user info") user_rkme = user_info.stat_info[self.stat_spec_type] - if not np.isfinite(user_rkme.dist(user_rkme)): + if not np.isfinite(float(user_rkme.dist(user_rkme))): raise ValueError("The distance between uploaded statistical specifications is not finite!") learnware_list = self._filter_by_rkme_spec_metadata(learnware_list, user_rkme) From 70953f8cc36def9e8801449a3e896de6b741dda7 Mon Sep 17 00:00:00 2001 From: liuht-0807 Date: Mon, 4 Dec 2023 20:41:15 +0800 Subject: [PATCH 53/64] [FIX] fix exceed GPU memory generating hetero_spec --- .../market/heterogeneous/organizer/hetero_map/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 2e31195..6735879 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -11,6 +11,8 @@ from .....specification import HeteroMapTableSpecification, RKMETableSpecificati from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer from .trainer import Trainer, TransTabCollatorForCL +from loguru import logger + class HeteroMap(nn.Module): """ @@ -127,6 +129,7 @@ class HeteroMap(nn.Module): self.base_temperature = base_temperature self.num_partition = num_partition self.overlap_ratio = overlap_ratio + self.max_process_size = 20480 self.to(device) def to(self, device: Union[str, torch.device]): @@ -306,6 +309,10 @@ class HeteroMap(nn.Module): """ self.eval() output_feas_list = [] + + if eval_batch_size * x_test.shape[1] > self.max_process_size: + eval_batch_size = int(self.max_process_size / x_test.shape[1]) + for i in range(0, len(x_test), eval_batch_size): bs_x_test = x_test.iloc[i : i + eval_batch_size] with torch.no_grad(): From d5b45f5d9fcb2742242ce5df5c93cdb217ca0ad9 Mon Sep 17 00:00:00 2001 From: liuht-0807 Date: Mon, 4 Dec 2023 20:44:29 +0800 Subject: [PATCH 54/64] [FIX] delete loguru --- .../market/heterogeneous/organizer/hetero_map/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 6735879..4ebb0a8 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -9,9 +9,7 @@ from torch import Tensor, nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer -from .trainer import Trainer, TransTabCollatorForCL - -from loguru import logger +from .trainer import TransTabCollatorForCL class HeteroMap(nn.Module): From a31fcd6c189176ea6e8fca66f0e60efb97b60d0a Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 22:43:46 +0800 Subject: [PATCH 55/64] [MNT] change client search return --- learnware/client/learnware_client.py | 43 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 2895e5c..7a5eaed 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -216,7 +216,18 @@ class LearnwareClient: else: stat_spec = None - returns = [] + returns = { + "single": { + "learnware_ids": [], + "semantic_specifications": [], + "matching": [], + }, + "multiple": { + "learnware_ids": [], + "semantic_specifications": [], + "matching": None, + }, + } with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json") as ftemp: if stat_spec is not None: stat_spec.save(ftemp.name) @@ -245,26 +256,16 @@ class LearnwareClient: raise Exception("search failed: " + json.dumps(result)) for learnware in result["data"]["learnware_list_single"]: - returns.append( - { - "type": "single", - "learnware_id": learnware["learnware_id"], - "semantic_specification": learnware["semantic_specification"], - "matching": learnware["matching"], - } - ) + returns["single"]["learnware_ids"].append(learnware["learnware_id"]) + returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) + returns["single"]["matching"].append(learnware["matching"]) + if len(result["data"]["learnware_list_multi"]) > 0: - multiple_learnware = { - "type": "multiple", - "learnware_ids": [], - "semantic_specifications": [], - "matching": result["data"]["learnware_list_multi"][0]["matching"], - } - for learnware in result["data"]["learnware_list_multi"]: - multiple_learnware["learnware_ids"].append(learnware["learnware_id"]) - multiple_learnware["semantic_specifications"].append(learnware["semantic_specification"]) - - returns.append(multiple_learnware) + multi_learnware = result["data"]["learnware_list_multi"][0] + returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) + returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) + returns["multiple"]["matching"] = learnware["matching"] + return returns @require_login @@ -416,7 +417,7 @@ class LearnwareClient: @staticmethod def _check_semantic_specification(semantic_spec): from ..market import EasySemanticChecker - + check_status, message = EasySemanticChecker.check_semantic_spec(semantic_spec) return check_status != BaseChecker.INVALID_LEARNWARE, message From c72bc0799768e5370907260ada8eead12af92d57 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Wed, 6 Dec 2023 11:45:51 +0800 Subject: [PATCH 56/64] [FIX] fix file bug in search --- learnware/client/learnware_client.py | 75 +++++++++++++++------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 7a5eaed..cb2cbcd 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -228,43 +228,46 @@ class LearnwareClient: "matching": None, }, } - with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json") as ftemp: + with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json", delete=False) as ftemp: + temp_file_name = ftemp.name if stat_spec is not None: - stat_spec.save(ftemp.name) - - with open(ftemp.name, "r") as fin: - semantic_specification = user_info.get_semantic_spec() - if stat_spec is None: - files = None - else: - files = {"statistical_specification": fin} - - response = requests.post( - url, - files=files, - data={ - "semantic_specification": json.dumps(semantic_specification), - "limit": page_size, - "page": page_index, - }, - headers=self.headers, - ) - - result = response.json() - - if result["code"] != 0: - raise Exception("search failed: " + json.dumps(result)) - - for learnware in result["data"]["learnware_list_single"]: - returns["single"]["learnware_ids"].append(learnware["learnware_id"]) - returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) - returns["single"]["matching"].append(learnware["matching"]) - - if len(result["data"]["learnware_list_multi"]) > 0: - multi_learnware = result["data"]["learnware_list_multi"][0] - returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) - returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) - returns["multiple"]["matching"] = learnware["matching"] + stat_spec.save(temp_file_name) + + with open(temp_file_name, "r") as fin: + semantic_specification = user_info.get_semantic_spec() + if stat_spec is None: + files = None + else: + files = {"statistical_specification": fin} + + response = requests.post( + url, + files=files, + data={ + "semantic_specification": json.dumps(semantic_specification), + "limit": page_size, + "page": page_index, + }, + headers=self.headers, + ) + result = response.json() + + if result["code"] != 0: + raise Exception("search failed: " + json.dumps(result)) + + for learnware in result["data"]["learnware_list_single"]: + returns["single"]["learnware_ids"].append(learnware["learnware_id"]) + returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) + returns["single"]["matching"].append(learnware["matching"]) + + if len(result["data"]["learnware_list_multi"]) > 0: + multi_learnware = result["data"]["learnware_list_multi"][0] + returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) + returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) + returns["multiple"]["matching"] = learnware["matching"] + + # Delete temp json file + os.remove(temp_file_name) return returns From 47f2bbc7a8b6ef21089a914ce113f1d70719b1bb Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Wed, 6 Dec 2023 11:46:12 +0800 Subject: [PATCH 57/64] [FIX] fit with client --- tests/test_learnware_client/test_all_learnware.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 276ac00..69108f8 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -30,15 +30,17 @@ class TestAllLearnware(unittest.TestCase): self.client.login(email, token) def test_all_learnware(self): - max_learnware_num = 1000 + max_learnware_num = 2000 semantic_spec = self.client.create_semantic_specification() user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={}) result = self.client.search_learnware(user_info, page_size=max_learnware_num) - print(f"result size: {len(result)}") - print(f"key in result: {[key for key in result[0]]}") + + learnware_ids = result["single"]["learnware_ids"] + keys = [key for key in result["single"]["semantic_specifications"][0]] + print(f"result size: {len(learnware_ids)}") + print(f"key in result: {keys}") failed_ids = [] - learnware_ids = [res["learnware_id"] for res in result] with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: for idx in learnware_ids: zip_path = os.path.join(tempdir, f"test_{idx}.zip") From 2ff7e15248b513702379a870fb389bf006806c8a Mon Sep 17 00:00:00 2001 From: liuht Date: Wed, 6 Dec 2023 15:51:20 +0800 Subject: [PATCH 58/64] [FIX] fix wrong delete --- learnware/market/heterogeneous/organizer/hetero_map/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 4ebb0a8..e124bcb 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -9,7 +9,7 @@ from torch import Tensor, nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer -from .trainer import TransTabCollatorForCL +from .trainer import TransTabCollatorForCL, Trainer class HeteroMap(nn.Module): From dc7f5b100bfb2f5d41ed01a21fda16d9dd10e61f Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 21:09:32 +0800 Subject: [PATCH 59/64] feat(docs): add checkers --- docs/components/market.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/components/market.rst b/docs/components/market.rst index 2a7893e..cdf10ef 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -59,6 +59,8 @@ Framework ====================================== + + Current Markets ====================================== @@ -80,3 +82,17 @@ One important case is that models have different feature spaces. In order to ena Current Checkers ====================================== +The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. Currently, we have three checkers, which are described below. + + +CondaChecker Class +------------------ +This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.INVALID_LEARNWARE`` status and error message. + +EasySemanticChecker Class +------------------------- +This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in ``semantic_spec`` dictionary against expected types and values. If the check fails, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. + +EasyStatChecker Class +--------------------- +This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape and dimensions using random generated data. In case of any exceptions, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. \ No newline at end of file From 5d170297d038c05f6b3d3ec69cf873d1d3c93624 Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 21:55:47 +0800 Subject: [PATCH 60/64] [DOC]: add the market description --- docs/components/market.rst | 58 ++++++-------------------------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index cdf10ef..9d967e3 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -3,62 +3,20 @@ Market ================================ +The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -Concepts -====================================== - -In the learnware paradigm, there are three key players: *developers*, *users*, and the *market*. - -* Developers: Typically machine learning experts who create and aim to share or sell their high-performing trained machine learning models. -* Users: Require machine learning services but often possess limited data and lack the necessary knowledge and expertise in machine learning. -* Market: Acquires top-performing trained models from developers, houses them within the marketplace, and offers services to users by identifying and reusing learnwares to help users with their current tasks. - -This process can be broken down into two main stages. - -Submitting Stage ------------------------------- - -During the *submitting stage*, developers can voluntarily submit their trained models to the learnware market. The market will then implement a quality assurance mechanism, such as performance validation, to determine if a submitted model is suitable for acceptance. In a learnware market with millions of models, identifying potentially helpful models for a new user is a challenge. - -Requiring users to submit their own data to the market for model testing is impractical, time-consuming, and costly, as it could lead to data leakage. Straightforward approaches, such as measuring the similarity between user data and the original training data of models, are also infeasible due to privacy and proprietary concerns. Our design operates under the constraint that the learnware market has no access to the original training data from developers or users. Furthermore, it assumes that users have limited knowledge of the models available in the market. - -The solution's crux lies in the *specification*, which is central to the learnware proposal. Once a submitted model is accepted by the learnware market, it is assigned a specification, which conveys the model's specialty and utility without revealing its original training data. For simplicity, consider models as functions that map input domain :math:`\mathcal{X}` to output domain :math:`\mathcal{Y}` with respect to objective 'obj.' These models exist in a functional space :math:`\mathcal{F}: \mathcal{X} \mapsto \mathcal{Y}` with respect to 'obj.' Each model has a specification, and all specifications form a specification space, where those for models that serve similar tasks are situated closely. - -In a learnware market, heterogeneous models may have different :math:`\mathcal{X}`, :math:`\mathcal{Y}`, or objectives. If we refer to the specification space that covers all possible models in all possible functional spaces as the 'specification world' analogously, then each specification space corresponding to one possible functional space can be called a 'specification island.' Designing an elegant specification format that encompasses the entire specification world and allows all possible models to be efficiently and adequately identified is a significant challenge. Currently, we adopt a practical design, where each learnware's specification consists of two parts. - - -Reusing Stage ------------------------------- - -Creating Learnware Specifications -++++++++++++++++++++++++++++++++++++ - -The first part of the learnware specification can be realized by a string consisting of a set of descriptions/tags given by the learnware market. These tags address aspects such as the task, input, output, and objective. Based on the user's provided descriptions/tags, the corresponding specification island can be efficiently and accurately located. The designer of the learnware market can create an initial set of descriptions/tags, which can grow as new models are accepted, and new functional spaces and specification islands are created. - -Merging Specification Islands -+++++++++++++++++++++++++++++++++ - -Specification islands can merge into larger ones. For example, when a new model about :math:`F: \mathcal{X}_1 \cup \mathcal{X}_2 \mapsto \mathcal{Y}` with respect to 'obj' is accepted by the learnware market, two islands can be merged. This is possible because the market can have synthetic data by randomly generating inputs, feeding them to models, and concatenating each input with its corresponding output to construct a dataset reflecting the function of a model. In principle, specification islands can be merged if there are common ingredients in :math:`\mathcal{X}`, :math:`\mathcal{Y}`, and 'obj.' - -Deploying Learnware Models -++++++++++++++++++++++++++++++ - -In the deploying stage, the user submits their requirement to the learnware market, which then identifies and returns helpful learnwares to the user. There are two issues to address: how to identify learnwares matching the user requirement and how to reuse the returned learnwares. - -The learnware market can house thousands or millions of models. Efficiently identifying helpful learnwares is challenging, especially given that the market has no access to the original training data of learnwares or the current user's data. With the specification design mentioned earlier, the market can request users to describe their intentions using a set of descriptions/tags, through a user interface or a learnware description language. Based on this information, the task becomes identifying helpful learnwares in a specification island. - -Reusing Learnwares -++++++++++++++++++++++ - -Once helpful learnwares are identified and delivered to the user, they can be reused in various ways. Users can apply the received learnware directly to their data, use multiple learnwares to create an ensemble, or adapt and polish the received learnware(s) using their own data. Learnwares can also be used as feature augmentors, with their outputs used as augmented features for building the final model. - -Helpful learnwares may be trained for tasks that are not exactly the same as the user's current task. In such cases, users can tackle their tasks in a divide-and-conquer way or reuse the learnwares collectively through measuring the utility of each model on each testing instance. If users find it difficult to express their requirements accurately, they can adapt and polish the received learnwares directly using their own data. - +The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space as the user's task requirements. Framework ====================================== +The market class is initialized with a organizer class, a searcher class, and a list of checker classes. + +The organizer class should be able to organize the learnware in the market. It should be able to add, delete, and update learnware. It should also be able to search for learnware based on user requirement. + +The searcher class should be able to search for learnware based on user requirement. It should be able to search for learnware based on user requirement. +The checker class is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. Current Markets From 889304cabc033de10cf735ed806ba80cf0244066 Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 22:03:56 +0800 Subject: [PATCH 61/64] [DOC] modify details --- docs/components/market.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index 9d967e3..01f8880 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -5,7 +5,7 @@ Market The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space as the user's task requirements. +The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space with the user's task requirements. Framework ====================================== @@ -40,17 +40,17 @@ One important case is that models have different feature spaces. In order to ena Current Checkers ====================================== -The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. Currently, we have three checkers, which are described below. +The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: INVALID_LEARNWARE denotes the learnware does not pass the check, NONUSABLE_LEARNWARE denotes the learnware pass the check but cannot make prediction due to some env dependency, USABLE_LEARWARE denotes the leanrware pass the check and can make prediction. Currently, we have three checkers, which are described below. CondaChecker Class ------------------ -This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.INVALID_LEARNWARE`` status and error message. +This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.NONUSABLE_LEARNWARE`` status and error message. EasySemanticChecker Class ------------------------- -This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in ``semantic_spec`` dictionary against expected types and values. If the check fails, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. +This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. EasyStatChecker Class --------------------- -This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape and dimensions using random generated data. In case of any exceptions, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. \ No newline at end of file +This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. \ No newline at end of file From 0560f90a0fd65715239159053e821ec348dda2c2 Mon Sep 17 00:00:00 2001 From: bxdd Date: Thu, 7 Dec 2023 20:25:07 +0800 Subject: [PATCH 62/64] [DOC] modify components/market doc --- docs/components/market.rst | 56 ++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index 01f8880..8421d94 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -3,25 +3,52 @@ Market ================================ -The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. +The ``learnware market`` receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space with the user's task requirements. +The ``learnware market`` will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the ``specification world`` in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares support search learnwares among all learnwares, not just among learnwares which has the same feature space and label space with the user's task requirements. Framework ====================================== -The market class is initialized with a organizer class, a searcher class, and a list of checker classes. +The ``learnware market`` is combined with a ``organizer``, a ``searcher``, and a list of ``checker``s. -The organizer class should be able to organize the learnware in the market. It should be able to add, delete, and update learnware. It should also be able to search for learnware based on user requirement. +The ``organizer`` can store and organize learnwares in the market. It supports ``add``, ``delete``, and ``update`` operations for learnwares. It also provides the interface for ``searcher`` to search learnwares based on user requirement. -The searcher class should be able to search for learnware based on user requirement. It should be able to search for learnware based on user requirement. +The ``searcher`` can search learnwares based on user requirement. The implementation of ``searcher`` is dependent on the concrete implementation and interface for ``organizer``, where usually an ``organizer`` can be compatible with multiple different ``searcher``s. -The checker class is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. +The ``checker`` is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. Only the learnwares who passed the ``checker`` could be able to be stored and added into the ``learnware market``. + + + +Current Checkers +====================================== + +The ``learnware`` package provide two different implementation of ``market`` where both of them share the same ``checker`` list. So we first introduce the details of ``checker``s. + +The ``checker``s check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: ``INVALID_LEARNWARE`` denotes the learnware does not pass the check, ``NONUSABLE_LEARNWARE`` denotes the learnware pass the check but cannot make prediction, ``USABLE_LEARWARE`` denotes the leanrware pass the check and can make prediction. Currently, we have three ``checker``s, which are described below. + + +``CondaChecker`` +------------------ +This ``checker`` checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. + + +``EasySemanticChecker`` +------------------------- +This ``checker`` checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. + + +``EasyStatChecker`` +--------------------- + +This ``checker`` checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. Current Markets ====================================== +The ``learnware`` package provide two different implementation of ``market``, i.e. ``Easy Market`` and ``Hetero Market``. They have different implementation of ``organizer`` and ``searcher``. + Easy Market ------------- @@ -37,20 +64,3 @@ One important case is that models have different feature spaces. In order to ena - First, design a method for the market to connect different feature spaces to a common subspace and implement the function ``HeterogeneousFeatureMarket.learn_mapping_functions``. This function uses specifications of all submitted models to learn mapping functions that can map the data in the original feature space to the common subspace and vice verse. - Second, use learned mapping functions to implement the functions ``HeterogeneousFeatureMarket.transform_original_to_subspace`` and ``HeterogeneousFeatureMarket.transform_subspace_to_original``. - Third, use the functions ``HeterogeneousFeatureMarket.transform_original_to_subspace`` and ``HeterogeneousFeatureMarket.transform_subspace_to_original`` to overwrite the mehtod ``EvolvedMarket.generate_new_stat_specification`` and ``EvolvedMarket.EvolvedMarket.evolve_learnware_list`` of the base class ``EvolvedMarket``. - -Current Checkers -====================================== -The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: INVALID_LEARNWARE denotes the learnware does not pass the check, NONUSABLE_LEARNWARE denotes the learnware pass the check but cannot make prediction due to some env dependency, USABLE_LEARWARE denotes the leanrware pass the check and can make prediction. Currently, we have three checkers, which are described below. - - -CondaChecker Class ------------------- -This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.NONUSABLE_LEARNWARE`` status and error message. - -EasySemanticChecker Class -------------------------- -This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. - -EasyStatChecker Class ---------------------- -This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. \ No newline at end of file From a9c443aefbbc42c34923922e72515bd92ef83450 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 7 Dec 2023 21:04:39 +0800 Subject: [PATCH 63/64] [FIX] fix bugs about batch_size --- learnware/market/heterogeneous/organizer/hetero_map/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index e124bcb..aca3d02 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -309,7 +309,7 @@ class HeteroMap(nn.Module): output_feas_list = [] if eval_batch_size * x_test.shape[1] > self.max_process_size: - eval_batch_size = int(self.max_process_size / x_test.shape[1]) + eval_batch_size = max(1, self.max_process_size // x_test.shape[1]) for i in range(0, len(x_test), eval_batch_size): bs_x_test = x_test.iloc[i : i + eval_batch_size] From 25a8493ec5cffb8711568f580623e01b9b6089e6 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 7 Dec 2023 21:13:05 +0800 Subject: [PATCH 64/64] [FIX] delete extra import --- learnware/market/anchor/organizer.py | 3 +-- learnware/market/anchor/searcher.py | 3 +-- learnware/market/base.py | 27 +++++++++++-------- learnware/market/easy/database_ops.py | 2 +- .../organizer/hetero_map/__init__.py | 4 +-- .../organizer/hetero_map/feature_extractor.py | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/learnware/market/anchor/organizer.py b/learnware/market/anchor/organizer.py index 405060b..4c3b668 100644 --- a/learnware/market/anchor/organizer.py +++ b/learnware/market/anchor/organizer.py @@ -1,9 +1,8 @@ -from typing import List, Dict, Tuple, Any +from typing import Dict from ..easy.organizer import EasyOrganizer from ...logger import get_module_logger from ...learnware import Learnware -from ...specification import BaseStatSpecification logger = get_module_logger("anchor_organizer") diff --git a/learnware/market/anchor/searcher.py b/learnware/market/anchor/searcher.py index 34d326d..60bca9f 100644 --- a/learnware/market/anchor/searcher.py +++ b/learnware/market/anchor/searcher.py @@ -1,7 +1,6 @@ -from typing import List, Dict, Tuple, Any, Union +from typing import List, Tuple, Any from .user_info import AnchoredUserInfo -from ..base import BaseUserInfo from ..easy.searcher import EasySearcher from ...logger import get_module_logger from ...learnware import Learnware diff --git a/learnware/market/base.py b/learnware/market/base.py index 78d06e6..fce2f18 100644 --- a/learnware/market/base.py +++ b/learnware/market/base.py @@ -3,7 +3,7 @@ from __future__ import annotations import traceback import zipfile import tempfile -from typing import Tuple, Any, List, Union, Dict, Optional +from typing import Tuple, Any, List, Union, Optional from dataclasses import dataclass from ..learnware import Learnware, get_learnware_from_dirpath from ..logger import get_module_logger @@ -45,7 +45,7 @@ class BaseUserInfo: def update_semantic_spec(self, semantic_spec: dict): self.semantic_spec = semantic_spec - + def update_stat_info(self, name: str, item: Any): """Update stat_info by market @@ -64,28 +64,35 @@ class SingleSearchItem: learnware: Learnware score: Optional[float] = None + @dataclass class MultipleSearchItem: learnwares: List[Learnware] score: float - + + class SearchResults: - def __init__(self, single_results: Optional[List[SingleSearchItem]] = None, multiple_results: Optional[List[MultipleSearchItem]] = None): + def __init__( + self, + single_results: Optional[List[SingleSearchItem]] = None, + multiple_results: Optional[List[MultipleSearchItem]] = None, + ): self.update_single_results([] if single_results is None else single_results) self.update_multiple_results([] if multiple_results is None else multiple_results) - + def get_single_results(self) -> List[SingleSearchItem]: return self.single_results - + def get_multiple_results(self) -> List[MultipleSearchItem]: return self.multiple_results - + def update_single_results(self, single_results: List[SingleSearchItem]): self.single_results = single_results - + def update_multiple_results(self, multiple_results: List[MultipleSearchItem]): self.multiple_results = multiple_results + class LearnwareMarket: """Base interface for market, it provide the interface of search/add/detele/update learnwares""" @@ -179,9 +186,7 @@ class LearnwareMarket: zip_path=zip_path, semantic_spec=semantic_spec, check_status=check_status, **kwargs ) - def search_learnware( - self, user_info: BaseUserInfo, check_status: int = None, **kwargs - ) -> SearchResults: + def search_learnware(self, user_info: BaseUserInfo, check_status: int = None, **kwargs) -> SearchResults: """Search learnwares based on user_info from learnwares with check_status Parameters diff --git a/learnware/market/easy/database_ops.py b/learnware/market/easy/database_ops.py index 7f8e87c..e27577c 100644 --- a/learnware/market/easy/database_ops.py +++ b/learnware/market/easy/database_ops.py @@ -1,6 +1,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine, text -from sqlalchemy import Column, Integer, Text, DateTime, String +from sqlalchemy import Column, Text, String import os import json import traceback diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index aca3d02..b2f39fe 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -1,10 +1,10 @@ -from typing import Callable, List, Optional, Union +from typing import Callable, Union import numpy as np import pandas as pd import torch import torch.nn.functional as F -from torch import Tensor, nn +from torch import nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification diff --git a/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py b/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py index d98bd2e..325f74e 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py @@ -1,6 +1,6 @@ import math import os -from typing import Callable, Dict, List, Union +from typing import Dict, List, Union import numpy as np import pandas as pd