autogluon.cloud 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {autogluon.cloud-0.3.0/src/autogluon.cloud.egg-info → autogluon.cloud-0.3.1}/PKG-INFO +1 -1
  2. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/setup.py +3 -2
  3. autogluon.cloud-0.3.1/src/autogluon/cloud/endpoint/__init__.py +0 -0
  4. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/ray_scripts/__init__.py +0 -0
  5. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/ray_scripts/train.py +124 -0
  6. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/sagemaker_scripts/__init__.py +0 -0
  7. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/sagemaker_scripts/multimodal_serve.py +155 -0
  8. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/sagemaker_scripts/tabular_serve.py +121 -0
  9. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/sagemaker_scripts/timeseries_serve.py +93 -0
  10. autogluon.cloud-0.3.1/src/autogluon/cloud/scripts/sagemaker_scripts/train.py +173 -0
  11. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/version.py +1 -1
  12. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1/src/autogluon.cloud.egg-info}/PKG-INFO +1 -1
  13. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/SOURCES.txt +8 -0
  14. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/LICENSE +0 -0
  15. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/NOTICE +0 -0
  16. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/README.md +0 -0
  17. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/pyproject.toml +0 -0
  18. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/setup.cfg +0 -0
  19. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/__init__.py +0 -0
  20. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/__init__.py +0 -0
  21. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/__init__.py +0 -0
  22. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/backend.py +0 -0
  23. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/backend_factory.py +0 -0
  24. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/constant.py +0 -0
  25. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/multimodal_sagemaker_backend.py +0 -0
  26. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/ray_aws_backend.py +0 -0
  27. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/ray_backend.py +0 -0
  28. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/sagemaker_backend.py +0 -0
  29. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/tabular_sagemaker_backend.py +0 -0
  30. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/backend/timeseries_sagemaker_backend.py +0 -0
  31. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/__init__.py +0 -0
  32. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/cluster_config_generator.py +0 -0
  33. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/cluster_manager.py +0 -0
  34. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/constants.py +0 -0
  35. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/ray_aws_cluster_config_generator.py +0 -0
  36. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/ray_aws_cluster_manager.py +0 -0
  37. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/ray_cluster_config_generator.py +0 -0
  38. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/cluster/ray_cluster_manager.py +0 -0
  39. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/data/__init__.py +0 -0
  40. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/data/format_converter.py +0 -0
  41. {autogluon.cloud-0.3.0/src/autogluon/cloud/endpoint → autogluon.cloud-0.3.1/src/autogluon/cloud/default_cluster_configs}/__init__.py +0 -0
  42. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/endpoint/endpoint.py +0 -0
  43. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/endpoint/sagemaker_endpoint.py +0 -0
  44. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/job/__init__.py +0 -0
  45. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/job/ray_job.py +0 -0
  46. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/job/remote_job.py +0 -0
  47. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/job/sagemaker_job.py +0 -0
  48. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/predictor/__init__.py +0 -0
  49. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/predictor/cloud_predictor.py +0 -0
  50. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/predictor/multimodal_cloud_predictor.py +0 -0
  51. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/predictor/tabular_cloud_predictor.py +0 -0
  52. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/predictor/timeseries_cloud_predictor.py +0 -0
  53. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/scripts/__init__.py +0 -0
  54. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/scripts/script_manager.py +0 -0
  55. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/__init__.py +0 -0
  56. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/ag_sagemaker.py +0 -0
  57. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/aws_utils.py +0 -0
  58. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/constants.py +0 -0
  59. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/deserializers.py +0 -0
  60. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/dlc_utils.py +0 -0
  61. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/ec2.py +0 -0
  62. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/iam.py +0 -0
  63. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/misc.py +0 -0
  64. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/ray_aws_iam.py +0 -0
  65. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/s3_utils.py +0 -0
  66. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/sagemaker_iam.py +0 -0
  67. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/serializers.py +0 -0
  68. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon/cloud/utils/utils.py +0 -0
  69. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/dependency_links.txt +0 -0
  70. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/namespace_packages.txt +0 -0
  71. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/requires.txt +3 -3
  72. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/top_level.txt +0 -0
  73. {autogluon.cloud-0.3.0 → autogluon.cloud-0.3.1}/src/autogluon.cloud.egg-info/zip-safe +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogluon.cloud
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Train and deploy AutoGluon backed models on the cloud
5
5
  Home-page: https://github.com/autogluon/autogluon-cloud
6
6
  Author: AutoGluon Community
@@ -67,7 +67,8 @@ def default_setup_args(*, version):
67
67
  package_data={
68
68
  AUTOGLUON: [
69
69
  "LICENSE",
70
- ]
70
+ ],
71
+ 'autogluon.cloud': ['default_cluster_configs/*.yaml'],
71
72
  },
72
73
  classifiers=[
73
74
  "Development Status :: 4 - Beta",
@@ -103,7 +104,7 @@ def default_setup_args(*, version):
103
104
  return setup_args
104
105
 
105
106
 
106
- version = "0.3.0"
107
+ version = "0.3.1"
107
108
  version = update_version(version, use_file_if_exists=False, create_file=True)
108
109
 
109
110
  install_requires = [
@@ -0,0 +1,124 @@
1
+ import argparse
2
+ import os
3
+ import pickle
4
+ import shutil
5
+ import time
6
+ from datetime import datetime, timezone
7
+ from typing import Optional
8
+
9
+ import boto3
10
+ import ray
11
+
12
+ from autogluon.common.utils.s3_utils import s3_path_to_bucket_prefix
13
+ from autogluon.tabular import TabularDataset, TabularPredictor
14
+
15
+
16
+ def wait_for_nodes_to_be_ready():
17
+ expected_num_nodes = int(os.environ.get("AG_NUM_NODES"))
18
+ ray.init("auto")
19
+ print("Waiting for worker nodes to be ready")
20
+ while len(ray.nodes()) < expected_num_nodes:
21
+ time.sleep(5)
22
+ ray.shutdown()
23
+ print("All nodes ready")
24
+
25
+
26
+ def get_utc_timestamp_now():
27
+ return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
28
+
29
+
30
+ def upload_log_file(job_id: str, model_output_path: str):
31
+ from ray.job_submission import JobSubmissionClient
32
+
33
+ client = JobSubmissionClient("http://127.0.0.1:8265")
34
+ logs = client.get_job_logs(job_id=job_id)
35
+ log_file = "training.log"
36
+ with open(log_file, "w") as f:
37
+ f.write(logs)
38
+ cloud_bucket, cloud_prefix = s3_path_to_bucket_prefix(model_output_path)
39
+ upload_file(file_name=log_file, bucket=cloud_bucket, prefix=cloud_prefix)
40
+
41
+
42
+ def tear_down_cluster(cluster_config_file: str):
43
+ import subprocess
44
+
45
+ print("Will tear down the cluster in 10 secs")
46
+ time.sleep(10)
47
+ cmd = f"ray stop --force; ray down {cluster_config_file} -y"
48
+ subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) # Avoid being terminated because ray runtime is down
49
+
50
+
51
+ def upload_file(file_name: str, bucket: str, prefix: Optional[str] = None):
52
+ """
53
+ Upload a file to an S3 bucket
54
+
55
+ Parameters
56
+ ----------
57
+ file_name: str,
58
+ File to upload
59
+ bucket: str,
60
+ Bucket to upload to
61
+ prefix: Optional[str], default = None
62
+ S3 prefix. If not specified then will upload to the root of the bucket
63
+ """
64
+ object_name = os.path.basename(file_name)
65
+ if prefix is not None and len(prefix) == 0:
66
+ prefix = None
67
+ if prefix is not None:
68
+ object_name = prefix + "/" + object_name
69
+
70
+ # Upload the file
71
+ s3_client = boto3.client("s3")
72
+ s3_client.upload_file(file_name, bucket, object_name)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ parser = argparse.ArgumentParser()
77
+ parser.add_argument("--ag_args_path", type=str, required=True)
78
+ parser.add_argument("--train_data", type=str, required=True)
79
+ parser.add_argument("--tune_data", type=str, required=False)
80
+ parser.add_argument("--model_output_path", type=str, required=True)
81
+ parser.add_argument("--ray_job_id", type=str, required=True)
82
+ parser.add_argument("--leaderboard", action="store_true")
83
+ parser.add_argument("--cluster_config_file", type=str, required=False)
84
+ parser.set_defaults(leaderboard=False)
85
+ args = parser.parse_args()
86
+
87
+ try:
88
+ wait_for_nodes_to_be_ready()
89
+ train_data = TabularDataset(args.train_data)
90
+ tune_data = None
91
+ if args.tune_data is not None:
92
+ tune_data = TabularDataset(args.tune_data)
93
+ with open(args.ag_args_path, "rb") as f:
94
+ ag_args = pickle.load(f)
95
+ predictor_init_args = ag_args["predictor_init_args"]
96
+ predictor_fit_args = ag_args["predictor_fit_args"]
97
+ save_path = f"ag_distributed_training_{get_utc_timestamp_now()}"
98
+ predictor_init_args["path"] = save_path
99
+
100
+ print("Start training TabularPredictor")
101
+ predictor = TabularPredictor(**predictor_init_args).fit(
102
+ train_data=train_data, tuning_data=tune_data, **predictor_fit_args
103
+ )
104
+ print("Compressing the model artifacts")
105
+ model_artifact = shutil.make_archive("model", "zip", save_path)
106
+ model_output_path = os.path.dirname(args.model_output_path)
107
+ cloud_bucket, cloud_prefix = s3_path_to_bucket_prefix(model_output_path)
108
+ print(f"Uploading model artifact to {model_output_path}")
109
+ upload_file(file_name=model_artifact, bucket=cloud_bucket, prefix=cloud_prefix)
110
+
111
+ if args.leaderboard:
112
+ lb = predictor.leaderboard(silent=False)
113
+ leaderboard_file = "leaderboard.csv"
114
+ lb.to_csv(leaderboard_file)
115
+ upload_file(file_name=leaderboard_file, bucket=cloud_bucket, prefix=cloud_prefix)
116
+ print("Training finished")
117
+ except Exception as e:
118
+ raise e
119
+ finally:
120
+ upload_log_file(job_id=args.ray_job_id, model_output_path=os.path.dirname(args.model_output_path))
121
+
122
+ cluster_config_file = args.cluster_config_file
123
+ if cluster_config_file is not None:
124
+ tear_down_cluster(cluster_config_file)
@@ -0,0 +1,155 @@
1
+ # flake8: noqa
2
+ import base64
3
+ import copy
4
+ import os
5
+ import pickle
6
+ from io import BytesIO, StringIO
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from autogluon.core.constants import BINARY, MULTICLASS
12
+ from autogluon.core.utils import get_pred_from_proba_df
13
+ from autogluon.multimodal import MultiModalPredictor
14
+
15
+
16
+ def _cleanup_images():
17
+ files = os.listdir(".")
18
+ for file in files:
19
+ if file.endswith(".png"):
20
+ os.remove(file)
21
+
22
+
23
+ def model_fn(model_dir):
24
+ """Loads model from previously saved artifact"""
25
+ model = MultiModalPredictor.load(model_dir)
26
+
27
+ # Check for _learner class which is available post 1.0.0 version of AutoGluon
28
+ if hasattr(model, "_learner") and hasattr(model._learner, "_label_column"):
29
+ label_column = model._learner._label_column
30
+ column_types = copy.copy(model._learner._column_types)
31
+ else:
32
+ # Fallback for older versions
33
+ label_column = model._label_column
34
+ column_types = copy.copy(model._column_types)
35
+ column_types.pop(label_column)
36
+ globals()["column_names"] = list(column_types.keys())
37
+
38
+ return model
39
+
40
+
41
+ def transform_fn(model, request_body, input_content_type, output_content_type="application/json"):
42
+ image_bytearrays = None
43
+ inference_kwargs = {}
44
+ if input_content_type == "application/x-parquet":
45
+ buf = BytesIO(request_body)
46
+ data = pd.read_parquet(buf)
47
+
48
+ elif input_content_type == "text/csv":
49
+ buf = StringIO(request_body)
50
+ data = pd.read_csv(buf)
51
+
52
+ elif input_content_type == "application/json":
53
+ buf = StringIO(request_body)
54
+ data = pd.read_json(buf)
55
+
56
+ elif input_content_type == "application/jsonl":
57
+ buf = StringIO(request_body)
58
+ data = pd.read_json(buf, orient="records", lines=True)
59
+
60
+ elif input_content_type == "application/x-npy":
61
+ buf = BytesIO(request_body)
62
+ data = np.load(buf, allow_pickle=True)
63
+ image_bytearrays = []
64
+ for _bytes in data:
65
+ im_bytes = base64.b85decode(_bytes)
66
+ image_bytearrays.append(im_bytes)
67
+
68
+ elif input_content_type == "application/x-autogluon-parquet":
69
+ buf = bytes(request_body)
70
+ payload = pickle.loads(buf)
71
+ data = pd.read_parquet(BytesIO(payload["data"]))
72
+ inference_kwargs = payload["inference_kwargs"]
73
+ if inference_kwargs is None:
74
+ inference_kwargs = {}
75
+
76
+ elif input_content_type == "application/x-autogluon-npy":
77
+ buf = bytes(request_body)
78
+ payload = pickle.loads(buf)
79
+ data = np.load(BytesIO(payload["data"]), allow_pickle=True)
80
+ image_bytearrays = []
81
+ for _bytes in data:
82
+ im_bytes = base64.b85decode(_bytes)
83
+ image_bytearrays.append(im_bytes)
84
+ inference_kwargs = payload["inference_kwargs"]
85
+ if inference_kwargs is None:
86
+ inference_kwargs = {}
87
+
88
+ elif input_content_type == "application/x-image":
89
+ image_bytearrays = []
90
+ image_bytearrays.append(request_body)
91
+
92
+ else:
93
+ raise ValueError(f"{input_content_type} input content type not supported.")
94
+
95
+ if image_bytearrays is not None:
96
+ data = dict(image=image_bytearrays) # multimodal image prediction takes in a dict containing image bytearrays
97
+ else:
98
+ # no header
99
+ test_columns = sorted(list(data.columns))
100
+ train_columns = sorted(column_names)
101
+ if test_columns != train_columns:
102
+ num_cols = len(data.columns)
103
+
104
+ if num_cols != len(column_names):
105
+ raise Exception(
106
+ f"Invalid data format. Input data has {num_cols} while the model expects {len(column_names)}"
107
+ )
108
+
109
+ else:
110
+ new_row = pd.DataFrame(data.columns).transpose()
111
+ old_rows = pd.DataFrame(data.values)
112
+ data = pd.concat([new_row, old_rows]).reset_index(drop=True)
113
+ data.columns = column_names
114
+ # find image column
115
+ image_column = None
116
+ # Check for _learner class which is available post 1.0.0 version of AutoGluon
117
+ if hasattr(model, "_learner") and hasattr(model._learner, "_column_types"):
118
+ column_types = model._learner._column_types
119
+ else:
120
+ # Fallback for older versions
121
+ column_types = model._column_types
122
+ for column_name, column_type in column_types.items():
123
+ if column_type in ("image_path", "image", "image_bytearray"):
124
+ image_column = column_name
125
+ break
126
+ if image_column is not None:
127
+ print(f"Detected image column {image_column}")
128
+ data[image_column] = [base64.b85decode(_bytes) for _bytes in data[image_column]]
129
+
130
+ if model.problem_type == BINARY or model.problem_type == MULTICLASS:
131
+ pred_proba = model.predict_proba(data, as_pandas=True, **inference_kwargs)
132
+ pred = get_pred_from_proba_df(pred_proba, problem_type=model.problem_type)
133
+ pred_proba.columns = [str(c) + "_proba" for c in pred_proba.columns]
134
+ pred.name = model.label
135
+ prediction = pd.concat([pred, pred_proba], axis=1)
136
+ else:
137
+ prediction = model.predict(data, as_pandas=True, **inference_kwargs)
138
+ if isinstance(prediction, pd.Series):
139
+ prediction = prediction.to_frame()
140
+
141
+ if "application/x-parquet" in output_content_type:
142
+ output = prediction.to_parquet()
143
+ output_content_type = "application/x-parquet"
144
+ elif "application/json" in output_content_type:
145
+ output = prediction.to_json()
146
+ output_content_type = "application/json"
147
+ elif "text/csv" in output_content_type:
148
+ output = prediction.to_csv(index=None)
149
+ output_content_type = "text/csv"
150
+ else:
151
+ raise ValueError(f"{output_content_type} content type not supported")
152
+
153
+ _cleanup_images()
154
+
155
+ return output, output_content_type
@@ -0,0 +1,121 @@
1
+ # flake8: noqa
2
+ import base64
3
+ import hashlib
4
+ import os
5
+ import pickle
6
+ from io import BytesIO, StringIO
7
+
8
+ import pandas as pd
9
+ from PIL import Image
10
+
11
+ from autogluon.core.constants import QUANTILE, REGRESSION
12
+ from autogluon.core.utils import get_pred_from_proba_df
13
+ from autogluon.tabular import TabularPredictor
14
+
15
+ image_dir = os.path.join("/tmp", "ag_images")
16
+
17
+
18
+ def _save_image_and_update_dataframe_column(bytes):
19
+ os.makedirs(image_dir, exist_ok=True)
20
+ im_bytes = base64.b85decode(bytes)
21
+ # nosec B303 - not a cryptographic use
22
+ im_hash = hashlib.sha1(im_bytes).hexdigest()
23
+ im = Image.open(BytesIO(im_bytes))
24
+ im_name = f"tabular_image_{im_hash}.png"
25
+ im_path = os.path.join(image_dir, im_name)
26
+ im.save(im_path)
27
+ print(f"Image saved as {im_path}")
28
+
29
+ return im_path
30
+
31
+
32
+ def model_fn(model_dir):
33
+ """loads model from previously saved artifact"""
34
+ model = TabularPredictor.load(model_dir)
35
+ model.persist_models()
36
+ globals()["column_names"] = model.original_features
37
+
38
+ return model
39
+
40
+
41
+ def transform_fn(model, request_body, input_content_type, output_content_type="application/json"):
42
+ inference_kwargs = {}
43
+ if input_content_type == "application/x-parquet":
44
+ buf = BytesIO(request_body)
45
+ data = pd.read_parquet(buf)
46
+
47
+ elif input_content_type == "text/csv":
48
+ buf = StringIO(request_body)
49
+ data = pd.read_csv(buf)
50
+
51
+ elif input_content_type == "application/json":
52
+ buf = StringIO(request_body)
53
+ data = pd.read_json(buf)
54
+
55
+ elif input_content_type == "application/jsonl":
56
+ buf = StringIO(request_body)
57
+ data = pd.read_json(buf, orient="records", lines=True)
58
+
59
+ elif input_content_type == "application/x-autogluon":
60
+ buf = bytes(request_body)
61
+ payload = pickle.loads(buf)
62
+ data = pd.read_parquet(BytesIO(payload["data"]))
63
+ inference_kwargs = payload["inference_kwargs"]
64
+ if inference_kwargs is None:
65
+ inference_kwargs = {}
66
+
67
+ else:
68
+ raise ValueError(f"{input_content_type} input content type not supported.")
69
+
70
+ test_columns = sorted(list(data.columns))
71
+ train_columns = sorted(column_names)
72
+ if test_columns != train_columns:
73
+ num_cols = len(data.columns)
74
+
75
+ if num_cols != len(column_names):
76
+ raise Exception(
77
+ f"Invalid data format. Input data has {num_cols} while the model expects {len(column_names)}"
78
+ )
79
+
80
+ else:
81
+ new_row = pd.DataFrame(data.columns).transpose()
82
+ old_rows = pd.DataFrame(data.values)
83
+ data = pd.concat([new_row, old_rows]).reset_index(drop=True)
84
+ data.columns = column_names
85
+
86
+ # find image column
87
+ image_column = None
88
+ for column_name, special_types in model.feature_metadata.get_type_map_special().items():
89
+ if "image_path" in special_types:
90
+ image_column = column_name
91
+ break
92
+ # save image column bytes to disk and update the column with saved path
93
+ if image_column is not None:
94
+ print(f"Detected image column {image_column}")
95
+ data[image_column] = [_save_image_and_update_dataframe_column(bytes) for bytes in data[image_column]]
96
+
97
+ if model.problem_type not in [REGRESSION, QUANTILE]:
98
+ pred_proba = model.predict_proba(data, as_pandas=True, **inference_kwargs)
99
+ pred = get_pred_from_proba_df(pred_proba, problem_type=model.problem_type)
100
+ pred_proba.columns = [str(c) + "_proba" for c in pred_proba.columns]
101
+ pred.name = model.label
102
+ prediction = pd.concat([pred, pred_proba], axis=1)
103
+ else:
104
+ prediction = model.predict(data, as_pandas=True, **inference_kwargs)
105
+ if isinstance(prediction, pd.Series):
106
+ prediction = prediction.to_frame()
107
+
108
+ if "application/x-parquet" in output_content_type:
109
+ prediction.columns = prediction.columns.astype(str)
110
+ output = prediction.to_parquet()
111
+ output_content_type = "application/x-parquet"
112
+ elif "application/json" in output_content_type:
113
+ output = prediction.to_json()
114
+ output_content_type = "application/json"
115
+ elif "text/csv" in output_content_type:
116
+ output = prediction.to_csv(index=None)
117
+ output_content_type = "text/csv"
118
+ else:
119
+ raise ValueError(f"{output_content_type} content type not supported")
120
+
121
+ return output, output_content_type
@@ -0,0 +1,93 @@
1
+ # flake8: noqa
2
+ import os
3
+ import pickle
4
+ import shutil
5
+ from io import BytesIO, StringIO
6
+
7
+ import pandas as pd
8
+
9
+ from autogluon.timeseries import TimeSeriesDataFrame, TimeSeriesPredictor
10
+
11
+
12
+ def model_fn(model_dir):
13
+ """loads model from previously saved artifact"""
14
+ # TSPredictor will write to the model file during inference while the default model_dir is read only
15
+ # Copy the model file to a writable location as a temporary workaround
16
+ tmp_model_dir = os.path.join("/tmp", "model")
17
+ try:
18
+ shutil.copytree(model_dir, tmp_model_dir, dirs_exist_ok=False)
19
+ except:
20
+ # model already copied
21
+ pass
22
+ model = TimeSeriesPredictor.load(tmp_model_dir)
23
+ print("MODEL LOADED")
24
+ return model
25
+
26
+
27
+ def prepare_timeseries_dataframe(df, predictor):
28
+ target = predictor.target
29
+ cols = df.columns.to_list()
30
+ id_column = cols[0]
31
+ timestamp_column = cols[1]
32
+ df[timestamp_column] = pd.to_datetime(df[timestamp_column])
33
+ static_features = None
34
+ if target != cols[-1]:
35
+ # target is not the last column, then there are static features being merged in
36
+ target_index = cols.index(target)
37
+ static_columns = cols[target_index + 1 :]
38
+ static_features = df[[id_column] + static_columns].groupby([id_column], sort=False).head(1)
39
+ static_features.set_index(id_column, inplace=True)
40
+ df.drop(columns=static_columns, inplace=True)
41
+ df = TimeSeriesDataFrame.from_data_frame(df, id_column=id_column, timestamp_column=timestamp_column)
42
+ if static_features is not None:
43
+ df.static_features = static_features
44
+ return df
45
+
46
+
47
+ def transform_fn(model, request_body, input_content_type, output_content_type="application/json"):
48
+ inference_kwargs = {}
49
+ if input_content_type == "application/x-parquet":
50
+ buf = BytesIO(request_body)
51
+ data = pd.read_parquet(buf)
52
+
53
+ elif input_content_type == "text/csv":
54
+ buf = StringIO(request_body)
55
+ data = pd.read_csv(buf)
56
+
57
+ elif input_content_type == "application/json":
58
+ buf = StringIO(request_body)
59
+ data = pd.read_json(buf)
60
+
61
+ elif input_content_type == "application/jsonl":
62
+ buf = StringIO(request_body)
63
+ data = pd.read_json(buf, orient="records", lines=True)
64
+
65
+ elif input_content_type == "application/x-autogluon":
66
+ buf = bytes(request_body)
67
+ payload = pickle.loads(buf)
68
+ data = pd.read_parquet(BytesIO(payload["data"]))
69
+ inference_kwargs = payload["inference_kwargs"]
70
+ if inference_kwargs is None:
71
+ inference_kwargs = {}
72
+
73
+ else:
74
+ raise ValueError(f"{input_content_type} input content type not supported.")
75
+
76
+ data = prepare_timeseries_dataframe(data, model)
77
+ prediction = model.predict(data, **inference_kwargs)
78
+ prediction = pd.DataFrame(prediction)
79
+
80
+ if "application/x-parquet" in output_content_type:
81
+ prediction.columns = prediction.columns.astype(str)
82
+ output = prediction.to_parquet()
83
+ output_content_type = "application/x-parquet"
84
+ elif "application/json" in output_content_type:
85
+ output = prediction.to_json()
86
+ output_content_type = "application/json"
87
+ elif "text/csv" in output_content_type:
88
+ output = prediction.to_csv(index=None)
89
+ output_content_type = "text/csv"
90
+ else:
91
+ raise ValueError(f"{output_content_type} content type not supported")
92
+
93
+ return output, output_content_type
@@ -0,0 +1,173 @@
1
+ # isort: skip_file
2
+ # flake8: noqa
3
+ # The import order of autogluon sub module here could cause seg fault. Ignore isort for now
4
+ # https://github.com/autogluon/autogluon/issues/2042
5
+ import argparse
6
+ import os
7
+ import pandas as pd
8
+ import shutil
9
+ from pprint import pprint
10
+
11
+ import pickle
12
+
13
+ from autogluon.common.loaders import load_pd
14
+ from autogluon.tabular import TabularPredictor, TabularDataset
15
+
16
+
17
+ def get_input_path(path):
18
+ file = os.listdir(path)[0]
19
+ if len(os.listdir(path)) > 1:
20
+ raise ValueError(f"WARN: more than one file is found in {channel} directory")
21
+ print(f"Using {file}")
22
+ filename = f"{path}/{file}"
23
+ return filename
24
+
25
+
26
+ def get_env_if_present(name):
27
+ result = None
28
+ if name in os.environ:
29
+ result = os.environ[name]
30
+ return result
31
+
32
+
33
+ def prepare_timeseries_dataframe(df, predictor_init_args):
34
+ target = predictor_init_args["target"]
35
+ cols = df.columns.to_list()
36
+ id_column = cols[0]
37
+ timestamp_column = cols[1]
38
+ df[timestamp_column] = pd.to_datetime(df[timestamp_column])
39
+ static_features = None
40
+ if target != cols[-1]:
41
+ # target is not the last column, then there are static features being merged in
42
+ target_index = cols.index(target)
43
+ static_columns = cols[target_index + 1 :]
44
+ static_features = df[[id_column] + static_columns].groupby([id_column], sort=False).head(1)
45
+ static_features.set_index(id_column, inplace=True)
46
+ df.drop(columns=static_columns, inplace=True)
47
+ df = TimeSeriesDataFrame.from_data_frame(df, id_column=id_column, timestamp_column=timestamp_column)
48
+ if static_features is not None:
49
+ print(static_features)
50
+ df.static_features = static_features
51
+ return df
52
+
53
+
54
+ def prepare_data(data_file, predictor_type, predictor_init_args=None):
55
+ if predictor_type == "timeseries":
56
+ assert predictor_init_args is not None
57
+ data = load_pd.load(data_file)
58
+ data = prepare_timeseries_dataframe(data, predictor_init_args)
59
+ else:
60
+ data = TabularDataset(data_file)
61
+ return data
62
+
63
+
64
+ if __name__ == "__main__":
65
+ # Disable Autotune
66
+ os.environ["MXNET_CUDNN_AUTOTUNE_DEFAULT"] = "0"
67
+
68
+ # ------------------------------------------------------------ Args parsing
69
+ print("Starting AG")
70
+ parser = argparse.ArgumentParser()
71
+
72
+ # Data, model, and output directories
73
+ parser.add_argument("--output-data-dir", type=str, default=get_env_if_present("SM_OUTPUT_DATA_DIR"))
74
+ parser.add_argument("--model-dir", type=str, default=get_env_if_present("SM_MODEL_DIR"))
75
+ parser.add_argument("--n_gpus", type=str, default=get_env_if_present("SM_NUM_GPUS"))
76
+ parser.add_argument("--train_dir", type=str, default=get_env_if_present("SM_CHANNEL_TRAIN"))
77
+ parser.add_argument("--tune_dir", type=str, required=False, default=get_env_if_present("SM_CHANNEL_TUNE"))
78
+ parser.add_argument(
79
+ "--train_images", type=str, required=False, default=get_env_if_present("SM_CHANNEL_TRAIN_IMAGES")
80
+ )
81
+ parser.add_argument(
82
+ "--tune_images", type=str, required=False, default=get_env_if_present("SM_CHANNEL_TUNE_IMAGES")
83
+ )
84
+ parser.add_argument("--ag_args", type=str, default=get_env_if_present("SM_CHANNEL_AG_ARGS"))
85
+ parser.add_argument("--serving_script", type=str, default=get_env_if_present("SM_CHANNEL_SERVING"))
86
+
87
+ args, _ = parser.parse_known_args()
88
+
89
+ print(f"Args: {args}")
90
+
91
+ # See SageMaker-specific environment variables: https://sagemaker.readthedocs.io/en/stable/overview.html#prepare-a-training-script
92
+ os.makedirs(args.output_data_dir, mode=0o777, exist_ok=True)
93
+
94
+ ag_args_file = get_input_path(args.ag_args)
95
+ with open(ag_args_file, "rb") as f:
96
+ ag_args = pickle.load(f) # AutoGluon-specific args
97
+
98
+ if args.n_gpus:
99
+ ag_args["num_gpus"] = int(args.n_gpus)
100
+
101
+ print("Running training job with the config:")
102
+ pprint(ag_args)
103
+
104
+ # ---------------------------------------------------------------- Training
105
+ save_path = os.path.normpath(args.model_dir)
106
+ predictor_type = ag_args["predictor_type"]
107
+ predictor_init_args = ag_args["predictor_init_args"]
108
+ predictor_init_args["path"] = save_path
109
+ predictor_fit_args = ag_args["predictor_fit_args"]
110
+ valid_predictor_types = ["tabular", "multimodal", "timeseries"]
111
+ assert (
112
+ predictor_type in valid_predictor_types
113
+ ), f"predictor_type {predictor_type} not supported. Valid options are {valid_predictor_types}"
114
+ if predictor_type == "tabular":
115
+ predictor_cls = TabularPredictor
116
+ elif predictor_type == "multimodal":
117
+ from autogluon.multimodal import MultiModalPredictor
118
+
119
+ predictor_cls = MultiModalPredictor
120
+ elif predictor_type == "timeseries":
121
+ from autogluon.timeseries import TimeSeriesPredictor, TimeSeriesDataFrame
122
+
123
+ predictor_cls = TimeSeriesPredictor
124
+
125
+ train_file = get_input_path(args.train_dir)
126
+ training_data = prepare_data(train_file, predictor_type, predictor_init_args)
127
+
128
+ if predictor_type == "tabular" and "image_column" in ag_args:
129
+ feature_metadata = predictor_fit_args.get("feature_metadata", None)
130
+ assert (
131
+ feature_metadata is not None
132
+ ), f"Detected image_column: {ag_args['image_column']} while feature metadata is not included"
133
+ feature_metadata = feature_metadata.add_special_types({ag_args["image_column"]: ["image_path"]})
134
+ predictor_fit_args["feature_metadata"] = feature_metadata
135
+
136
+ tuning_data = None
137
+ if args.tune_dir:
138
+ tune_file = get_input_path(args.tune_dir)
139
+ tuning_data = prepare_data(tune_file, predictor_type)
140
+
141
+ if args.train_images:
142
+ train_image_compressed_file = get_input_path(args.train_images)
143
+ train_images_dir = "train_images"
144
+ shutil.unpack_archive(train_image_compressed_file, train_images_dir)
145
+ image_column = ag_args["image_column"]
146
+ training_data[image_column] = training_data[image_column].apply(
147
+ lambda path: os.path.join(train_images_dir, path)
148
+ )
149
+
150
+ if args.tune_images:
151
+ tune_image_compressed_file = get_input_path(args.tune_images)
152
+ tune_images_dir = "tune_images"
153
+ shutil.unpack_archive(tune_image_compressed_file, tune_images_dir)
154
+ image_column = ag_args["image_column"]
155
+ tuning_data[image_column] = tuning_data[image_column].apply(lambda path: os.path.join(tune_images_dir, path))
156
+
157
+ predictor = predictor_cls(**predictor_init_args).fit(training_data, tuning_data=tuning_data, **predictor_fit_args)
158
+
159
+ # When use automm backend, predictor needs to be saved with standalone flag to avoid need of internet access when loading
160
+ # This is required because of https://discuss.huggingface.co/t/error-403-when-downloading-model-for-sagemaker-batch-inference/12571/6
161
+ if predictor_type == "multimodal":
162
+ predictor.save(path=save_path, standalone=True)
163
+
164
+ if predictor_cls == TabularPredictor:
165
+ if ag_args.get("leaderboard", False):
166
+ lb = predictor.leaderboard(silent=False)
167
+ lb.to_csv(f"{args.output_data_dir}/leaderboard.csv")
168
+
169
+ print("Saving serving script")
170
+ serving_script_saving_path = os.path.join(save_path, "code")
171
+ os.mkdir(serving_script_saving_path)
172
+ serving_script_path = get_input_path(args.serving_script)
173
+ shutil.move(serving_script_path, os.path.join(serving_script_saving_path, os.path.basename(serving_script_path)))
@@ -1,2 +1,2 @@
1
1
  """This is the autogluon.cloud version file."""
2
- __version__ = '0.3.0'
2
+ __version__ = '0.3.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogluon.cloud
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Train and deploy AutoGluon backed models on the cloud
5
5
  Home-page: https://github.com/autogluon/autogluon-cloud
6
6
  Author: AutoGluon Community
@@ -34,6 +34,7 @@ src/autogluon/cloud/cluster/ray_cluster_config_generator.py
34
34
  src/autogluon/cloud/cluster/ray_cluster_manager.py
35
35
  src/autogluon/cloud/data/__init__.py
36
36
  src/autogluon/cloud/data/format_converter.py
37
+ src/autogluon/cloud/default_cluster_configs/__init__.py
37
38
  src/autogluon/cloud/endpoint/__init__.py
38
39
  src/autogluon/cloud/endpoint/endpoint.py
39
40
  src/autogluon/cloud/endpoint/sagemaker_endpoint.py
@@ -48,6 +49,13 @@ src/autogluon/cloud/predictor/tabular_cloud_predictor.py
48
49
  src/autogluon/cloud/predictor/timeseries_cloud_predictor.py
49
50
  src/autogluon/cloud/scripts/__init__.py
50
51
  src/autogluon/cloud/scripts/script_manager.py
52
+ src/autogluon/cloud/scripts/ray_scripts/__init__.py
53
+ src/autogluon/cloud/scripts/ray_scripts/train.py
54
+ src/autogluon/cloud/scripts/sagemaker_scripts/__init__.py
55
+ src/autogluon/cloud/scripts/sagemaker_scripts/multimodal_serve.py
56
+ src/autogluon/cloud/scripts/sagemaker_scripts/tabular_serve.py
57
+ src/autogluon/cloud/scripts/sagemaker_scripts/timeseries_serve.py
58
+ src/autogluon/cloud/scripts/sagemaker_scripts/train.py
51
59
  src/autogluon/cloud/utils/__init__.py
52
60
  src/autogluon/cloud/utils/ag_sagemaker.py
53
61
  src/autogluon/cloud/utils/aws_utils.py
File without changes
File without changes
@@ -13,8 +13,8 @@ ray[default]<2.7,>=2.6.3
13
13
  autogluon<1.0,>=0.7
14
14
 
15
15
  [tests]
16
- autogluon.common<1.1,>=0.7.0b
17
- tox
18
16
  pytest-cov
19
- moto
17
+ autogluon.common<1.1,>=0.7.0b
20
18
  pytest
19
+ moto
20
+ tox