Skip to content

Commit 915dbcd

Browse files
committed
Add managed SageMaker job connectors
Expand the existing SageMaker plugin with async lifecycle support for training, processing, batch transform, hyperparameter tuning, and Inference Recommender jobs. Include typed task wrappers, stable projected outputs, idempotent retries, public documentation, and comprehensive unit tests without requiring Propeller changes. Signed-off-by: Rohit Sharma <rohitrsh@gmail.com>
1 parent 0d2366a commit 915dbcd

31 files changed

Lines changed: 4698 additions & 21 deletions

plugins/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ All the Flytekit plugins maintained by the core team are added here. It is not n
66

77
| Plugin | Installation | Description | Version | Type |
88
| ---------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
9-
| AWS SageMaker | `bash pip install flytekitplugins-awssagemaker` | Deploy SageMaker models and manage inference endpoints with ease. | [![PyPI version fury.io](https://badge.fury.io/py/flytekitplugins-awssagemaker.svg)](https://pypi.python.org/pypi/flytekitplugins-awssagemaker/) | Flytekit-only |
9+
| AWS SageMaker | `bash pip install flytekitplugins-awssagemaker` | Run SageMaker training, processing, tuning, transform, recommendation, and deployment workloads. | [![PyPI version fury.io](https://badge.fury.io/py/flytekitplugins-awssagemaker.svg)](https://pypi.python.org/pypi/flytekitplugins-awssagemaker/) | Flytekit-only |
1010
| dask | `bash pip install flytekitplugins-dask ` | Installs SDK to author dask jobs that can be executed natively on Kubernetes using the Flyte backend plugin | [![PyPI version fury.io](https://badge.fury.io/py/flytekitplugins-dask.svg)](https://pypi.python.org/pypi/flytekitplugins-dask/) | Backend |
1111
| Hive Queries | `bash pip install flytekitplugins-hive ` | Installs SDK to author Hive Queries that can be executed on a configured hive backend using Flyte backend plugin | [![PyPI version fury.io](https://badge.fury.io/py/flytekitplugins-hive.svg)](https://pypi.python.org/pypi/flytekitplugins-hive/) | Backend |
1212
| K8s distributed PyTorch Jobs | `bash pip install flytekitplugins-kfpytorch ` | Installs SDK to author Distributed pyTorch Jobs in python using Kubeflow PyTorch Operator | [![PyPI version fury.io](https://badge.fury.io/py/flytekitplugins-kfpytorch.svg)](https://pypi.python.org/pypi/flytekitplugins-kfpytorch/) | Backend |

plugins/flytekit-aws-sagemaker/README.md

Lines changed: 527 additions & 1 deletion
Large diffs are not rendered by default.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
.. currentmodule:: flytekitplugins.awssagemaker_batch_transform
3+
4+
.. autosummary::
5+
:template: custom.rst
6+
:toctree: generated/
7+
8+
SageMakerTransformJobConnector
9+
SageMakerTransformJobTask
10+
SageMakerStopTransformJobTask
11+
SageMakerDescribeTransformJobTask
12+
"""
13+
14+
from .connector import SageMakerTransformJobConnector, SageMakerTransformJobMetadata
15+
from .task import (
16+
SageMakerDescribeTransformJobTask,
17+
SageMakerStopTransformJobTask,
18+
SageMakerTransformJobTask,
19+
)
20+
21+
__all__ = [
22+
"SageMakerTransformJobConnector",
23+
"SageMakerTransformJobMetadata",
24+
"SageMakerTransformJobTask",
25+
"SageMakerStopTransformJobTask",
26+
"SageMakerDescribeTransformJobTask",
27+
]
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""SageMaker batch-transform connector.
2+
3+
Mirrors the training-job connector. Targets ``CreateTransformJob`` /
4+
``DescribeTransformJob`` / ``StopTransformJob``. Surfaces the predictions
5+
``S3OutputPath`` so downstream Flyte tasks can read scores written by SageMaker
6+
without any extra plumbing.
7+
8+
Note: ``TransformJobStatus`` has no ``Deleting`` state and there is no
9+
``SecondaryStatus`` — running phase has no live progress signal beyond the job
10+
being in flight.
11+
"""
12+
13+
from dataclasses import dataclass
14+
from datetime import datetime
15+
from typing import Any, Dict, Optional
16+
17+
import cloudpickle
18+
from flyteidl.core.execution_pb2 import TaskExecution
19+
from flytekitplugins.awssagemaker_inference.boto3_mixin import (
20+
Boto3ConnectorMixin,
21+
CustomException,
22+
)
23+
24+
from flytekit.extend.backend.base_connector import (
25+
AsyncConnectorBase,
26+
ConnectorRegistry,
27+
Resource,
28+
ResourceMeta,
29+
)
30+
from flytekit.models.literals import LiteralMap
31+
from flytekit.models.task import TaskTemplate
32+
33+
34+
@dataclass
35+
class SageMakerTransformJobMetadata(ResourceMeta):
36+
config: Dict[str, Any]
37+
region: Optional[str] = None
38+
inputs: Optional[LiteralMap] = None
39+
40+
def encode(self) -> bytes:
41+
return cloudpickle.dumps(self)
42+
43+
@classmethod
44+
def decode(cls, data: bytes) -> "SageMakerTransformJobMetadata":
45+
return cloudpickle.loads(data)
46+
47+
48+
_STATE_MAP = {
49+
"InProgress": TaskExecution.RUNNING,
50+
"Stopping": TaskExecution.RUNNING,
51+
"Completed": TaskExecution.SUCCEEDED,
52+
"Failed": TaskExecution.FAILED,
53+
"Stopped": TaskExecution.FAILED,
54+
}
55+
56+
57+
def _isoformat(value: Any) -> Any:
58+
if isinstance(value, datetime):
59+
return value.isoformat()
60+
return value
61+
62+
63+
def _build_outputs(describe_response: Dict[str, Any]) -> Dict[str, Any]:
64+
"""Project describe_transform_job down to a stable, downstream-friendly dict."""
65+
transform_output = describe_response.get("TransformOutput") or {}
66+
return {
67+
"TransformJobArn": describe_response.get("TransformJobArn"),
68+
"TransformJobName": describe_response.get("TransformJobName"),
69+
"ModelName": describe_response.get("ModelName"),
70+
"TransformOutput": {"S3OutputPath": transform_output.get("S3OutputPath")},
71+
"TransformStartTime": _isoformat(describe_response.get("TransformStartTime")),
72+
"TransformEndTime": _isoformat(describe_response.get("TransformEndTime")),
73+
}
74+
75+
76+
class SageMakerTransformJobConnector(Boto3ConnectorMixin, AsyncConnectorBase):
77+
"""Long-running connector for SageMaker batch-transform jobs."""
78+
79+
name = "SageMaker Transform Job Connector"
80+
81+
def __init__(self):
82+
super().__init__(
83+
service="sagemaker",
84+
task_type_name="sagemaker-transform-job",
85+
metadata_type=SageMakerTransformJobMetadata,
86+
)
87+
88+
async def create(
89+
self, task_template: TaskTemplate, inputs: Optional[LiteralMap] = None, **kwargs
90+
) -> SageMakerTransformJobMetadata:
91+
custom = task_template.custom
92+
config = custom.get("config")
93+
region = custom.get("region")
94+
95+
try:
96+
await self._call(
97+
method="create_transform_job",
98+
config=config,
99+
inputs=inputs,
100+
region=region,
101+
)
102+
except CustomException as e:
103+
original_exception = e.original_exception
104+
error_code = original_exception.response["Error"]["Code"]
105+
error_message = original_exception.response["Error"]["Message"]
106+
107+
if e.idempotence_token and (
108+
error_code == "ResourceInUse"
109+
or (error_code == "ValidationException" and "Cannot create already existing" in error_message)
110+
):
111+
return SageMakerTransformJobMetadata(config=config, region=region, inputs=inputs)
112+
raise e
113+
114+
return SageMakerTransformJobMetadata(config=config, region=region, inputs=inputs)
115+
116+
async def get(self, resource_meta: SageMakerTransformJobMetadata, **kwargs) -> Resource:
117+
describe_response, _ = await self._call(
118+
method="describe_transform_job",
119+
config={"TransformJobName": resource_meta.config.get("TransformJobName")},
120+
inputs=resource_meta.inputs,
121+
region=resource_meta.region,
122+
)
123+
124+
current_state = describe_response.get("TransformJobStatus")
125+
flyte_phase = _STATE_MAP.get(current_state, TaskExecution.RUNNING)
126+
127+
message: Optional[str] = None
128+
if current_state in ("Failed", "Stopped"):
129+
message = describe_response.get("FailureReason")
130+
131+
outputs: Optional[Dict[str, Any]] = None
132+
if current_state == "Completed":
133+
outputs = {"result": _build_outputs(describe_response)}
134+
135+
return Resource(phase=flyte_phase, outputs=outputs, message=message)
136+
137+
async def delete(self, resource_meta: SageMakerTransformJobMetadata, **kwargs):
138+
try:
139+
await self._call(
140+
method="stop_transform_job",
141+
config={"TransformJobName": resource_meta.config.get("TransformJobName")},
142+
region=resource_meta.region,
143+
inputs=resource_meta.inputs,
144+
)
145+
except CustomException as e:
146+
original_exception = e.original_exception
147+
error_code = original_exception.response["Error"]["Code"]
148+
error_message = original_exception.response["Error"]["Message"]
149+
150+
if error_code == "ResourceNotFound" or (
151+
error_code == "ValidationException" and "non-running" in error_message
152+
):
153+
return
154+
raise e
155+
156+
157+
ConnectorRegistry.register(SageMakerTransformJobConnector())
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""User-facing tasks for SageMaker batch-transform jobs."""
2+
3+
from typing import Any, Dict, Optional, Type
4+
5+
from flytekitplugins.awssagemaker_inference.boto3_task import BotoConfig, BotoTask
6+
7+
from flytekit import kwtypes
8+
from flytekit.configuration import SerializationSettings
9+
from flytekit.core.base_task import PythonTask
10+
from flytekit.core.interface import Interface
11+
from flytekit.extend.backend.base_connector import AsyncConnectorExecutorMixin
12+
13+
14+
class SageMakerTransformJobTask(AsyncConnectorExecutorMixin, PythonTask):
15+
"""Run a SageMaker batch-transform job and emit the predictions ``S3OutputPath``.
16+
17+
Outputs a single ``result: dict`` literal containing ``TransformJobArn``,
18+
``TransformJobName``, ``ModelName``, ``TransformOutput.S3OutputPath`` (the S3
19+
prefix where SageMaker wrote one ``<input>.out`` per input object — feed this
20+
into a downstream Flyte task to consume the predictions), ``TransformStartTime``
21+
and ``TransformEndTime``.
22+
23+
Set ``DataProcessing.JoinSource: "Input"`` in the config for tabular predictive
24+
workloads so each output line carries the original input fields alongside the
25+
prediction (otherwise rows have no key to join back).
26+
27+
``name`` identifies the Flyte task. ``config`` is the boto3
28+
``create_transform_job`` request and may contain ``{inputs.X}`` and
29+
``{idempotence_token}`` placeholders. ``region`` selects the AWS region, and
30+
``inputs`` maps input placeholders to Flyte types.
31+
"""
32+
33+
_TASK_TYPE = "sagemaker-transform-job"
34+
35+
def __init__(
36+
self,
37+
name: str,
38+
config: Dict[str, Any],
39+
region: Optional[str] = None,
40+
inputs: Optional[Dict[str, Type]] = None,
41+
**kwargs,
42+
):
43+
super().__init__(
44+
name=name,
45+
task_type=self._TASK_TYPE,
46+
interface=Interface(inputs=inputs, outputs=kwtypes(result=dict)),
47+
**kwargs,
48+
)
49+
self._config = config
50+
self._region = region
51+
52+
def get_custom(self, settings: SerializationSettings) -> Dict[str, Any]:
53+
return {"config": self._config, "region": self._region}
54+
55+
56+
class SageMakerStopTransformJobTask(BotoTask):
57+
"""Sync helper task that stops a running SageMaker transform job by name."""
58+
59+
def __init__(
60+
self,
61+
name: str,
62+
config: Dict[str, Any],
63+
region: Optional[str] = None,
64+
inputs: Optional[Dict[str, Type]] = None,
65+
**kwargs,
66+
):
67+
super().__init__(
68+
name=name,
69+
task_config=BotoConfig(
70+
service="sagemaker",
71+
method="stop_transform_job",
72+
config=config,
73+
region=region,
74+
),
75+
inputs=inputs,
76+
**kwargs,
77+
)
78+
79+
80+
class SageMakerDescribeTransformJobTask(BotoTask):
81+
"""Sync helper task that returns the full ``describe_transform_job`` response."""
82+
83+
def __init__(
84+
self,
85+
name: str,
86+
config: Dict[str, Any],
87+
region: Optional[str] = None,
88+
inputs: Optional[Dict[str, Type]] = None,
89+
**kwargs,
90+
):
91+
super().__init__(
92+
name=name,
93+
task_config=BotoConfig(
94+
service="sagemaker",
95+
method="describe_transform_job",
96+
config=config,
97+
region=region,
98+
),
99+
inputs=inputs,
100+
**kwargs,
101+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
.. currentmodule:: flytekitplugins.awssagemaker_hyperparameter_tuning
3+
4+
.. autosummary::
5+
:template: custom.rst
6+
:toctree: generated/
7+
8+
SageMakerHyperParameterTuningJobConnector
9+
SageMakerHyperParameterTuningJobTask
10+
SageMakerStopHyperParameterTuningJobTask
11+
SageMakerDescribeHyperParameterTuningJobTask
12+
"""
13+
14+
from .connector import (
15+
SageMakerHyperParameterTuningJobConnector,
16+
SageMakerHyperParameterTuningJobMetadata,
17+
)
18+
from .task import (
19+
SageMakerDescribeHyperParameterTuningJobTask,
20+
SageMakerHyperParameterTuningJobTask,
21+
SageMakerStopHyperParameterTuningJobTask,
22+
)
23+
24+
__all__ = [
25+
"SageMakerHyperParameterTuningJobConnector",
26+
"SageMakerHyperParameterTuningJobMetadata",
27+
"SageMakerHyperParameterTuningJobTask",
28+
"SageMakerStopHyperParameterTuningJobTask",
29+
"SageMakerDescribeHyperParameterTuningJobTask",
30+
]

0 commit comments

Comments
 (0)