Skip to content

Commit 71972f0

Browse files
author
Rishabh Devnani
committed
feat(pipeline): Add inference and lineage step types
Adds four pipeline step classes -- EndpointConfigStep, EndpointStep, InferenceComponentStep and LineageStep -- along with their StepTypeEnum values and package exports. Each step takes step_args captured under a PipelineSession, following the convention used by TrainingStep and ModelStep. Capture goes through one seam: PipelineSession._intercept_create_request returns the captured context, so a producer returns its result directly, and the base Session holds no pipeline-specific branch. Producers: - Session.create_endpoint_config, added here, creates only the endpoint configuration, matching this step's one-resource-per-step shape. endpoint_from_production_variants also creates the endpoint and so is not a producer; the request building is shared between the two. - Session.create_endpoint and Session.create_inference_component. - Action.create(), Artifact.create(), Context.create() for lineage, whose requests Record._invoke_api routes through the same seam. LineageStep records a batch of entities in one step, which is what the pipeline service models: it creates every entity in the step, then adds the associations between them. An association references a sibling by name and type, resolved against the entities that same step creates, or an existing entity by ARN. Because Association.create() takes ARNs only and a sibling has no ARN until the step runs, associations are declared with LineageAssociation and LineageEntityReference. A name and type reference that the step does not create is rejected at construction. Chaining note: the resource created by an EndpointConfig step carries an execution-unique name suffix, so a downstream step must reference it as config_step.properties.EndpointConfigName rather than the requested name. The Endpoint step does not suffix. Testing: 38 unit tests, plus two integration tests run end to end against the service -- a chained EndpointConfig -> Endpoint -> InferenceComponent pipeline, and a LineageStep recording an Action, an Artifact and a Context with associations between them. --- X-AI-Prompt: Add the inference and lineage pipeline step types to the public SDK, reworked per review feedback to use one capture seam and to batch lineage entities per step X-AI-Tool: kiro-cli
1 parent 62dca3b commit 71972f0

11 files changed

Lines changed: 1834 additions & 28 deletions

File tree

‎sagemaker-core/src/sagemaker/core/apiutils/_base_types.py‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,27 @@ def with_boto(self, boto_dict):
219219
)
220220
return self
221221

222+
# Lineage entity creation methods that participate in pipeline step
223+
# composition. Requests for these are routed through the session's
224+
# ``_intercept_create_request`` seam, so a ``PipelineSession`` captures them
225+
# as step arguments while a plain ``Session`` calls the service. Used by
226+
# ``sagemaker.mlops.workflow.LineageStep``.
227+
_PIPELINE_CAPTURABLE_METHODS = frozenset(
228+
{"create_action", "create_artifact", "create_context", "add_association"}
229+
)
230+
222231
def _invoke_api(self, boto_method, boto_method_members):
223232
"""Invoke a SageMaker API."""
224233
api_values = {k: v for k, v in vars(self).items() if k in boto_method_members}
225234
api_kwargs = self.to_boto(api_values)
226-
api_method = getattr(self.sagemaker_session.sagemaker_client, boto_method)
227-
api_boto_response = api_method(**api_kwargs)
228-
return self.with_boto(api_boto_response)
235+
236+
def submit(request):
237+
api_method = getattr(self.sagemaker_session.sagemaker_client, boto_method)
238+
return self.with_boto(api_method(**request))
239+
240+
if boto_method in self._PIPELINE_CAPTURABLE_METHODS:
241+
return self.sagemaker_session._intercept_create_request(
242+
api_kwargs, submit, boto_method
243+
)
244+
245+
return submit(api_kwargs)

‎sagemaker-core/src/sagemaker/core/helper/session_helper.py‎

Lines changed: 138 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,52 @@ def endpoint_from_production_variants(
10061006
str: The name of the created ``Endpoint``.
10071007
"""
10081008

1009+
config_options = self._build_endpoint_config_request(
1010+
name=name,
1011+
production_variants=production_variants,
1012+
tags=tags,
1013+
kms_key=kms_key,
1014+
data_capture_config_dict=data_capture_config_dict,
1015+
async_inference_config_dict=async_inference_config_dict,
1016+
explainer_config_dict=explainer_config_dict,
1017+
vpc_config=vpc_config,
1018+
enable_network_isolation=enable_network_isolation,
1019+
role=role,
1020+
)
1021+
endpoint_tags = _append_project_tags(format_tags(tags))
1022+
1023+
logger.info("Creating endpoint-config with name %s", name)
1024+
self.sagemaker_client.create_endpoint_config(**config_options)
1025+
1026+
return self.create_endpoint(
1027+
endpoint_name=name,
1028+
config_name=name,
1029+
tags=endpoint_tags,
1030+
wait=wait,
1031+
live_logging=live_logging,
1032+
)
1033+
1034+
def _build_endpoint_config_request(
1035+
self,
1036+
name,
1037+
production_variants,
1038+
tags=None,
1039+
kms_key=None,
1040+
data_capture_config_dict=None,
1041+
async_inference_config_dict=None,
1042+
explainer_config_dict=None,
1043+
vpc_config=None,
1044+
enable_network_isolation=None,
1045+
role=None,
1046+
):
1047+
"""Build the ``CreateEndpointConfig`` request, applying config defaults.
1048+
1049+
Shared by ``create_endpoint_config`` and
1050+
``endpoint_from_production_variants`` so the two cannot drift.
1051+
1052+
Returns:
1053+
dict: The ``CreateEndpointConfig`` request.
1054+
"""
10091055
supports_kms = any(
10101056
[
10111057
instance_supports_kms(production_variant["InstanceType"])
@@ -1060,7 +1106,6 @@ def endpoint_from_production_variants(
10601106
role = self.expand_role(role)
10611107
config_options["ExecutionRoleArn"] = role
10621108
endpoint_config_tags = _append_project_tags(format_tags(tags))
1063-
endpoint_tags = _append_project_tags(format_tags(tags))
10641109

10651110
endpoint_config_tags = self._append_sagemaker_config_tags(
10661111
endpoint_config_tags, "{}.{}.{}".format(SAGEMAKER, ENDPOINT_CONFIG, TAGS)
@@ -1090,15 +1135,71 @@ def endpoint_from_production_variants(
10901135
if role is not None:
10911136
config_options["ExecutionRoleArn"] = role
10921137

1093-
logger.info("Creating endpoint-config with name %s", name)
1094-
self.sagemaker_client.create_endpoint_config(**config_options)
1138+
return config_options
10951139

1096-
return self.create_endpoint(
1097-
endpoint_name=name,
1098-
config_name=name,
1099-
tags=endpoint_tags,
1100-
wait=wait,
1101-
live_logging=live_logging,
1140+
def create_endpoint_config(
1141+
self,
1142+
name,
1143+
production_variants,
1144+
tags=None,
1145+
kms_key=None,
1146+
data_capture_config_dict=None,
1147+
async_inference_config_dict=None,
1148+
explainer_config_dict=None,
1149+
vpc_config=None,
1150+
enable_network_isolation=None,
1151+
role=None,
1152+
):
1153+
"""Create an Amazon SageMaker endpoint configuration and nothing else.
1154+
1155+
Unlike ``endpoint_from_production_variants``, which creates an endpoint
1156+
configuration *and* the endpoint that uses it, this creates only the
1157+
configuration. That makes it the producer for a pipeline
1158+
``EndpointConfigStep``, which composes one resource per step.
1159+
1160+
Args:
1161+
name (str): Name of the endpoint configuration to create.
1162+
production_variants (list[dict[str, str]]): The list of production
1163+
variants to host at this endpoint.
1164+
tags (Optional[Tags]): Tags to apply to the endpoint configuration.
1165+
kms_key (str): KMS key ARN for encrypting the volume attached to the
1166+
instances hosting the endpoint.
1167+
data_capture_config_dict (dict): Configuration for capturing
1168+
inference request and response data.
1169+
async_inference_config_dict (dict): Configuration for asynchronous
1170+
inference.
1171+
explainer_config_dict (dict): Configuration for online explainers.
1172+
vpc_config (dict): The VpcConfig for the endpoint configuration.
1173+
enable_network_isolation (bool): Whether to isolate the model
1174+
containers deployed to the endpoint.
1175+
role (str): An execution role ARN, required when the production
1176+
variants carry no model name, as with inference components.
1177+
1178+
Returns:
1179+
str: Name of the endpoint configuration created. Under a
1180+
``PipelineSession`` the captured step arguments are returned
1181+
instead and no service call is made.
1182+
"""
1183+
config_options = self._build_endpoint_config_request(
1184+
name=name,
1185+
production_variants=production_variants,
1186+
tags=tags,
1187+
kms_key=kms_key,
1188+
data_capture_config_dict=data_capture_config_dict,
1189+
async_inference_config_dict=async_inference_config_dict,
1190+
explainer_config_dict=explainer_config_dict,
1191+
vpc_config=vpc_config,
1192+
enable_network_isolation=enable_network_isolation,
1193+
role=role,
1194+
)
1195+
1196+
def submit(request):
1197+
logger.info("Creating endpoint-config with name %s", name)
1198+
self.sagemaker_client.create_endpoint_config(**request)
1199+
return name
1200+
1201+
return self._intercept_create_request(
1202+
config_options, submit, self.create_endpoint_config.__name__
11021203
)
11031204

11041205
def create_endpoint(self, endpoint_name, config_name, tags=None, wait=True, live_logging=False):
@@ -1122,23 +1223,31 @@ def create_endpoint(self, endpoint_name, config_name, tags=None, wait=True, live
11221223
botocore.exceptions.ClientError: If Sagemaker throws an exception while creating
11231224
endpoint.
11241225
"""
1125-
logger.info("Creating endpoint with name %s", endpoint_name)
1126-
11271226
tags = format_tags(tags) or []
11281227
tags = _append_project_tags(tags)
11291228
tags = self._append_sagemaker_config_tags(
11301229
tags, "{}.{}.{}".format(SAGEMAKER, ENDPOINT, TAGS)
11311230
)
1132-
try:
1133-
res = self.sagemaker_client.create_endpoint(
1134-
EndpointName=endpoint_name, EndpointConfigName=config_name, Tags=tags
1135-
)
1231+
create_endpoint_request = {
1232+
"EndpointName": endpoint_name,
1233+
"EndpointConfigName": config_name,
1234+
"Tags": tags,
1235+
}
1236+
1237+
def submit(request):
1238+
logger.info("Creating endpoint with name %s", endpoint_name)
1239+
res = self.sagemaker_client.create_endpoint(**request)
11361240
if res:
11371241
self.endpoint_arn = res["EndpointArn"]
11381242

11391243
if wait:
11401244
self.wait_for_endpoint(endpoint_name, live_logging=live_logging)
11411245
return endpoint_name
1246+
1247+
try:
1248+
return self._intercept_create_request(
1249+
create_endpoint_request, submit, self.create_endpoint.__name__
1250+
)
11421251
except Exception as e:
11431252
troubleshooting = (
11441253
"https://docs.aws.amazon.com/sagemaker/latest/dg/"
@@ -1232,12 +1341,6 @@ def create_inference_component(
12321341
Returns:
12331342
str: Name of the Amazon SageMaker ``InferenceComponent`` if created.
12341343
"""
1235-
LOGGER.info(
1236-
"Creating inference component with name %s for endpoint %s",
1237-
inference_component_name,
1238-
endpoint_name,
1239-
)
1240-
12411344
if runtime_config is None:
12421345
runtime_config = {"CopyCount": 1}
12431346

@@ -1257,10 +1360,20 @@ def create_inference_component(
12571360
if tags and len(tags) != 0:
12581361
request["Tags"] = tags
12591362

1260-
self.sagemaker_client.create_inference_component(**request)
1261-
if wait:
1262-
self.wait_for_inference_component(inference_component_name)
1263-
return inference_component_name
1363+
def submit(req):
1364+
LOGGER.info(
1365+
"Creating inference component with name %s for endpoint %s",
1366+
inference_component_name,
1367+
endpoint_name,
1368+
)
1369+
self.sagemaker_client.create_inference_component(**req)
1370+
if wait:
1371+
self.wait_for_inference_component(inference_component_name)
1372+
return inference_component_name
1373+
1374+
return self._intercept_create_request(
1375+
request, submit, self.create_inference_component.__name__
1376+
)
12641377

12651378
def wait_for_inference_component(self, inference_component_name, poll=20):
12661379
"""Wait for an Amazon SageMaker ``Inference Component`` deployment to complete.

‎sagemaker-core/src/sagemaker/core/workflow/pipeline_context.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ def _intercept_create_request(self, request: Dict, create, func_name: str = None
220220
request (dict): the create job request
221221
create (functor): a functor calls the sagemaker client create method
222222
func_name (str): the name of the function needed intercepting
223+
224+
Returns:
225+
The captured pipeline context, so that a producer method can simply
226+
return the result of this call: under a plain ``Session`` the base
227+
implementation returns the service result, and here it returns the
228+
step arguments instead. This keeps the pipeline branch inside
229+
``PipelineSession`` rather than in the base ``Session``.
223230
"""
224231
if func_name == "create_model":
225232
self.context.create_model_request = request
@@ -229,6 +236,7 @@ def _intercept_create_request(self, request: Dict, create, func_name: str = None
229236
self.context.caller_name = func_name
230237
else:
231238
self.context = _JobStepArguments(func_name, request)
239+
return self.context
232240

233241
def init_model_step_arguments(self, model):
234242
"""Create a `_ModelStepArguments` (if not exist) as pipeline context

‎sagemaker-mlops/src/sagemaker/mlops/workflow/__init__.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
functions, conditions, properties) and can import from sagemaker.train and sagemaker.serve
1515
for orchestration purposes.
1616
"""
17+
1718
from __future__ import absolute_import
1819

1920
__version__ = "0.1.0"
@@ -46,8 +47,15 @@
4647
from sagemaker.mlops.workflow.clarify_check_step import ClarifyCheckStep
4748
from sagemaker.mlops.workflow.condition_step import ConditionStep
4849
from sagemaker.mlops.workflow.emr_step import EMRStep, EMRStepConfig
50+
from sagemaker.mlops.workflow.endpoint_step import EndpointConfigStep, EndpointStep
4951
from sagemaker.mlops.workflow.fail_step import FailStep
52+
from sagemaker.mlops.workflow.inference_component_step import InferenceComponentStep
5053
from sagemaker.mlops.workflow.lambda_step import LambdaStep, LambdaOutput
54+
from sagemaker.mlops.workflow.lineage_step import (
55+
LineageAssociation,
56+
LineageEntityReference,
57+
LineageStep,
58+
)
5159
from sagemaker.mlops.workflow.model_step import ModelStep
5260
from sagemaker.mlops.workflow.monitor_batch_transform_step import MonitorBatchTransformStep
5361
from sagemaker.mlops.workflow.notebook_job_step import NotebookJobStep
@@ -98,9 +106,15 @@
98106
"ConditionStep",
99107
"EMRStep",
100108
"EMRStepConfig",
109+
"EndpointConfigStep",
110+
"EndpointStep",
101111
"FailStep",
112+
"InferenceComponentStep",
102113
"LambdaStep",
103114
"LambdaOutput",
115+
"LineageAssociation",
116+
"LineageEntityReference",
117+
"LineageStep",
104118
"ModelStep",
105119
"MonitorBatchTransformStep",
106120
"NotebookJobStep",

0 commit comments

Comments
 (0)