Skip to content

Commit b94f673

Browse files
nayan3107nayan3107
andauthored
fix(train): add PipelineSession support to V3 trainers (SFT/DPO/RLAIF/RLVR) (#6213)
When a PipelineSession is passed as sagemaker_session, the serverless training path in SFTTrainer, DPOTrainer, RLAIFTrainer, and RLVRTrainer now intercepts the CreateTrainingJob request and returns step arguments instead of immediately launching a training job. This enables V3 trainers to be used with SageMaker Pipelines TrainingStep, matching the existing behavior of ModelTrainer, Processor, Transformer, and HyperparameterTuner. The fix follows the established SDK pattern: isinstance check for PipelineSession, call _intercept_create_request with the request args, and return session.context (the captured step arguments). Fixes: #6163 Co-authored-by: nayan3107 <nayancho@amazon.com>
1 parent 1b14335 commit b94f673

8 files changed

Lines changed: 314 additions & 0 deletions

File tree

‎sagemaker-train/src/sagemaker/train/dpo_trainer.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sagemaker.train.common import TrainingType, CustomizationTechnique, JOB_TYPE
66
from sagemaker.core.resources import TrainingJob, ModelPackageGroup, ModelPackage
77
from sagemaker.core.shapes import VpcConfig
8+
from sagemaker.core.workflow.pipeline_context import PipelineSession
89
from sagemaker.train.defaults import TrainDefaults
910
from sagemaker.train.utils import _get_unique_name, _get_jumpstart_tags
1011
from sagemaker.train.configs import StoppingCondition
@@ -369,6 +370,14 @@ def train(self,
369370
if self.stopping_condition is not None:
370371
create_args["stopping_condition"] = self.stopping_condition
371372

373+
# If running within a PipelineSession, intercept the request and store
374+
# step arguments instead of launching a training job.
375+
# This must come before data path validation since in pipeline mode
376+
# the data path may be a pipeline parameter that doesn't exist yet.
377+
if isinstance(sagemaker_session, PipelineSession):
378+
sagemaker_session._intercept_create_request(create_args, None, "train")
379+
return sagemaker_session.context
380+
372381
# Validate data paths exist before submission
373382
effective_training = training_dataset or self.training_dataset
374383
effective_validation = validation_dataset or self.validation_dataset

‎sagemaker-train/src/sagemaker/train/rlaif_trainer.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sagemaker.train.common import TrainingType, CustomizationTechnique, JOB_TYPE
55
from sagemaker.core.resources import TrainingJob, ModelPackageGroup, MlflowTrackingServer, ModelPackage
66
from sagemaker.core.shapes import VpcConfig
7+
from sagemaker.core.workflow.pipeline_context import PipelineSession
78
from sagemaker.train.defaults import TrainDefaults
89
from sagemaker.train.utils import _get_unique_name, _get_jumpstart_tags
910
from sagemaker.train.common_utils.recipe_utils import _get_hub_content_metadata
@@ -335,6 +336,14 @@ def train(self, training_dataset: Optional[Union[str, DataSet]] = None, validati
335336
if self.stopping_condition is not None:
336337
create_args["stopping_condition"] = self.stopping_condition
337338

339+
# If running within a PipelineSession, intercept the request and store
340+
# step arguments instead of launching a training job.
341+
# This must come before data path validation since in pipeline mode
342+
# the data path may be a pipeline parameter that doesn't exist yet.
343+
if isinstance(sagemaker_session, PipelineSession):
344+
sagemaker_session._intercept_create_request(create_args, None, "train")
345+
return sagemaker_session.context
346+
338347
# Validate data paths exist before submission
339348
effective_training = training_dataset or self.training_dataset
340349
effective_validation = validation_dataset or self.validation_dataset

‎sagemaker-train/src/sagemaker/train/rlvr_trainer.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sagemaker.train.common import TrainingType, CustomizationTechnique, JOB_TYPE
77
from sagemaker.core.resources import TrainingJob, ModelPackageGroup, MlflowTrackingServer, ModelPackage
88
from sagemaker.core.shapes import VpcConfig
9+
from sagemaker.core.workflow.pipeline_context import PipelineSession
910
from sagemaker.train.defaults import TrainDefaults
1011
from sagemaker.train.utils import _get_unique_name, _get_jumpstart_tags
1112
from sagemaker.ai_registry.dataset import DataSet
@@ -555,6 +556,14 @@ def train(self, training_dataset: Optional[Union[str, DataSet]] = None,
555556
if self.stopping_condition is not None:
556557
create_args["stopping_condition"] = self.stopping_condition
557558

559+
# If running within a PipelineSession, intercept the request and store
560+
# step arguments instead of launching a training job.
561+
# This must come before data path validation since in pipeline mode
562+
# the data path may be a pipeline parameter that doesn't exist yet.
563+
if isinstance(sagemaker_session, PipelineSession):
564+
sagemaker_session._intercept_create_request(create_args, None, "train")
565+
return sagemaker_session.context
566+
558567
# Validate data paths exist before submission
559568
effective_training = training_dataset or self.training_dataset
560569
effective_validation = validation_dataset or self.validation_dataset

‎sagemaker-train/src/sagemaker/train/sft_trainer.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sagemaker.train.common import TrainingType, CustomizationTechnique, JOB_TYPE
55
from sagemaker.core.resources import TrainingJob, ModelPackageGroup, ModelPackage
66
from sagemaker.core.shapes import VpcConfig
7+
from sagemaker.core.workflow.pipeline_context import PipelineSession
78
from sagemaker.train.defaults import TrainDefaults
89
from sagemaker.train.utils import _get_unique_name, _get_jumpstart_tags
910
from sagemaker.ai_registry.dataset import DataSet
@@ -437,6 +438,14 @@ def train(self, training_dataset: Optional[Union[str, DataSet]] = None, validati
437438
if self.stopping_condition is not None:
438439
create_args["stopping_condition"] = self.stopping_condition
439440

441+
# If running within a PipelineSession, intercept the request and store
442+
# step arguments instead of launching a training job.
443+
# This must come before data path validation since in pipeline mode
444+
# the data path may be a pipeline parameter that doesn't exist yet.
445+
if isinstance(sagemaker_session, PipelineSession):
446+
sagemaker_session._intercept_create_request(create_args, None, "train")
447+
return sagemaker_session.context
448+
440449
# Validate data paths exist before submission
441450
effective_training = training_dataset or self.training_dataset
442451
effective_validation = validation_dataset or self.validation_dataset

‎sagemaker-train/tests/unit/train/test_dpo_trainer.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,72 @@ def test_list_supported_models(self, mock_list):
750750
mock_list.assert_called_once_with(
751751
recipe_type="FineTuning", technique="DPO", session=None
752752
)
753+
754+
class TestDPOTrainerPipelineSession:
755+
"""Test DPOTrainer behavior when PipelineSession is used.
756+
757+
Ref: https://github.com/aws/sagemaker-python-sdk/issues/6163
758+
"""
759+
760+
@patch('sagemaker.train.dpo_trainer._create_model_package_config')
761+
@patch('sagemaker.train.dpo_trainer._create_mlflow_config')
762+
@patch('sagemaker.train.dpo_trainer._create_output_config')
763+
@patch('sagemaker.train.dpo_trainer._create_serverless_config')
764+
@patch('sagemaker.train.dpo_trainer._convert_input_data_to_channels')
765+
@patch('sagemaker.train.dpo_trainer._create_input_data_config')
766+
@patch('sagemaker.train.dpo_trainer._get_unique_name')
767+
@patch('sagemaker.train.dpo_trainer.TrainDefaults.get_role')
768+
@patch('sagemaker.train.dpo_trainer.TrainDefaults.get_sagemaker_session')
769+
@patch('sagemaker.train.dpo_trainer._validate_and_resolve_model_package_group')
770+
@patch('sagemaker.train.dpo_trainer._get_fine_tuning_options_and_model_arn')
771+
@patch('sagemaker.train.dpo_trainer._resolve_model_and_name')
772+
@patch('sagemaker.train.common_utils.finetune_utils._get_beta_session')
773+
@patch('sagemaker.core.resources.TrainingJob.create')
774+
def test_train_with_pipeline_session_does_not_launch_job(
775+
self, mock_training_job_create, mock_beta_session, mock_resolve_model,
776+
mock_finetuning_options, mock_validate_group, mock_get_session, mock_get_role,
777+
mock_unique_name, mock_input_config, mock_convert_channels,
778+
mock_serverless_config, mock_output_config, mock_mlflow_config, mock_model_package_config,
779+
):
780+
"""When PipelineSession is passed, _intercept_create_request traps the args."""
781+
from sagemaker.train.dpo_trainer import DPOTrainer
782+
from sagemaker.core.workflow.pipeline_context import PipelineSession, _JobStepArguments
783+
784+
pipeline_session = Mock(spec=PipelineSession)
785+
pipeline_session.boto_session = Mock()
786+
pipeline_session.boto_session.region_name = "us-west-2"
787+
788+
step_args = _JobStepArguments("train", {"training_job_name": "test-dpo-job-001"})
789+
pipeline_session._intercept_create_request.return_value = None
790+
pipeline_session.context = step_args
791+
mock_get_session.return_value = pipeline_session
792+
793+
mock_resolve_model.return_value = ("test-model", "resolved-model-name")
794+
mock_hyperparams = Mock()
795+
mock_hyperparams.to_dict.return_value = {"param1": "value1"}
796+
mock_hyperparams._specs = {"param1": {"type": "string"}}
797+
mock_hyperparams._user_set = set()
798+
mock_finetuning_options.return_value = (mock_hyperparams, "arn:aws:sagemaker:us-west-2:123456789012:model/test", False)
799+
mock_validate_group.return_value = "test-group"
800+
mock_get_role.return_value = "arn:aws:iam::123456789012:role/Role"
801+
mock_unique_name.return_value = "test-dpo-job-001"
802+
mock_input_config.return_value = {"train": "s3://bucket/data"}
803+
mock_convert_channels.return_value = [{"ChannelName": "train"}]
804+
mock_serverless_config.return_value = {"BaseModelArn": "arn:model"}
805+
mock_output_config.return_value = {"S3OutputPath": "s3://bucket/output"}
806+
mock_mlflow_config.return_value = None
807+
mock_model_package_config.return_value = None
808+
mock_beta_session.return_value = pipeline_session
809+
810+
trainer = DPOTrainer(model="test-model", training_dataset="s3://bucket/data", model_package_group="test-group", sagemaker_session=pipeline_session)
811+
trainer._model_arn = "arn:aws:sagemaker:us-west-2:123456789012:model/test"
812+
trainer._model_name = "test-model"
813+
trainer.accept_eula = True
814+
trainer.hyperparameters = mock_hyperparams
815+
816+
result = trainer.train()
817+
818+
mock_training_job_create.assert_not_called()
819+
pipeline_session._intercept_create_request.assert_called_once()
820+
assert pipeline_session._intercept_create_request.call_args[0][2] == "train"
821+
assert result == step_args

‎sagemaker-train/tests/unit/train/test_rlaif_trainer.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,72 @@ def test_list_supported_models(self, mock_list):
872872
mock_list.assert_called_once_with(
873873
recipe_type="FineTuning", technique="RLAIF", session=None
874874
)
875+
876+
class TestRLAIFTrainerPipelineSession:
877+
"""Test RLAIFTrainer behavior when PipelineSession is used.
878+
879+
Ref: https://github.com/aws/sagemaker-python-sdk/issues/6163
880+
"""
881+
882+
@patch('sagemaker.train.rlaif_trainer._create_model_package_config')
883+
@patch('sagemaker.train.rlaif_trainer._create_mlflow_config')
884+
@patch('sagemaker.train.rlaif_trainer._create_output_config')
885+
@patch('sagemaker.train.rlaif_trainer._create_serverless_config')
886+
@patch('sagemaker.train.rlaif_trainer._convert_input_data_to_channels')
887+
@patch('sagemaker.train.rlaif_trainer._create_input_data_config')
888+
@patch('sagemaker.train.rlaif_trainer._get_unique_name')
889+
@patch('sagemaker.train.rlaif_trainer.TrainDefaults.get_role')
890+
@patch('sagemaker.train.rlaif_trainer.TrainDefaults.get_sagemaker_session')
891+
@patch('sagemaker.train.rlaif_trainer._validate_and_resolve_model_package_group')
892+
@patch('sagemaker.train.rlaif_trainer._get_fine_tuning_options_and_model_arn')
893+
@patch('sagemaker.train.rlaif_trainer._resolve_model_and_name')
894+
@patch('sagemaker.train.common_utils.finetune_utils._get_beta_session')
895+
@patch('sagemaker.core.resources.TrainingJob.create')
896+
def test_train_with_pipeline_session_does_not_launch_job(
897+
self, mock_training_job_create, mock_beta_session, mock_resolve_model,
898+
mock_finetuning_options, mock_validate_group, mock_get_session, mock_get_role,
899+
mock_unique_name, mock_input_config, mock_convert_channels,
900+
mock_serverless_config, mock_output_config, mock_mlflow_config, mock_model_package_config,
901+
):
902+
"""When PipelineSession is passed, _intercept_create_request traps the args."""
903+
from sagemaker.train.rlaif_trainer import RLAIFTrainer
904+
from sagemaker.core.workflow.pipeline_context import PipelineSession, _JobStepArguments
905+
906+
pipeline_session = Mock(spec=PipelineSession)
907+
pipeline_session.boto_session = Mock()
908+
pipeline_session.boto_session.region_name = "us-west-2"
909+
910+
step_args = _JobStepArguments("train", {"training_job_name": "test-rlaif-job-001"})
911+
pipeline_session._intercept_create_request.return_value = None
912+
pipeline_session.context = step_args
913+
mock_get_session.return_value = pipeline_session
914+
915+
mock_resolve_model.return_value = ("test-model", "resolved-model-name")
916+
mock_hyperparams = Mock()
917+
mock_hyperparams.to_dict.return_value = {"param1": "value1"}
918+
mock_hyperparams._specs = {"param1": {"type": "string"}}
919+
mock_hyperparams._user_set = set()
920+
mock_finetuning_options.return_value = (mock_hyperparams, "arn:aws:sagemaker:us-west-2:123456789012:model/test", False)
921+
mock_validate_group.return_value = "test-group"
922+
mock_get_role.return_value = "arn:aws:iam::123456789012:role/Role"
923+
mock_unique_name.return_value = "test-rlaif-job-001"
924+
mock_input_config.return_value = {"train": "s3://bucket/data"}
925+
mock_convert_channels.return_value = [{"ChannelName": "train"}]
926+
mock_serverless_config.return_value = {"BaseModelArn": "arn:model"}
927+
mock_output_config.return_value = {"S3OutputPath": "s3://bucket/output"}
928+
mock_mlflow_config.return_value = None
929+
mock_model_package_config.return_value = None
930+
mock_beta_session.return_value = pipeline_session
931+
932+
trainer = RLAIFTrainer(model="test-model", training_dataset="s3://bucket/data", model_package_group="test-group", sagemaker_session=pipeline_session)
933+
trainer._model_arn = "arn:aws:sagemaker:us-west-2:123456789012:model/test"
934+
trainer._model_name = "test-model"
935+
trainer.accept_eula = True
936+
trainer.hyperparameters = mock_hyperparams
937+
938+
result = trainer.train()
939+
940+
mock_training_job_create.assert_not_called()
941+
pipeline_session._intercept_create_request.assert_called_once()
942+
assert pipeline_session._intercept_create_request.call_args[0][2] == "train"
943+
assert result == step_args

‎sagemaker-train/tests/unit/train/test_rlvr_trainer.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,3 +775,71 @@ def test_list_supported_models(self, mock_list):
775775
mock_list.assert_called_once_with(
776776
recipe_type="FineTuning", technique="RLVR", session=None
777777
)
778+
779+
class TestRLVRTrainerPipelineSession:
780+
"""Test RLVRTrainer behavior when PipelineSession is used.
781+
782+
Ref: https://github.com/aws/sagemaker-python-sdk/issues/6163
783+
"""
784+
785+
@patch('sagemaker.train.rlvr_trainer._create_model_package_config')
786+
@patch('sagemaker.train.rlvr_trainer._create_mlflow_config')
787+
@patch('sagemaker.train.rlvr_trainer._create_output_config')
788+
@patch('sagemaker.train.rlvr_trainer._convert_input_data_to_channels')
789+
@patch('sagemaker.train.rlvr_trainer._create_input_data_config')
790+
@patch('sagemaker.train.rlvr_trainer._get_unique_name')
791+
@patch('sagemaker.train.rlvr_trainer.TrainDefaults.get_role')
792+
@patch('sagemaker.train.rlvr_trainer.TrainDefaults.get_sagemaker_session')
793+
@patch('sagemaker.train.rlvr_trainer._validate_and_resolve_model_package_group')
794+
@patch('sagemaker.train.rlvr_trainer._get_fine_tuning_options_and_model_arn')
795+
@patch('sagemaker.train.rlvr_trainer._resolve_model_and_name')
796+
@patch('sagemaker.train.common_utils.finetune_utils._get_beta_session')
797+
@patch('sagemaker.core.resources.TrainingJob.create')
798+
def test_train_with_pipeline_session_does_not_launch_job(
799+
self, mock_training_job_create, mock_beta_session, mock_resolve_model,
800+
mock_finetuning_options, mock_validate_group, mock_get_session, mock_get_role,
801+
mock_unique_name, mock_input_config, mock_convert_channels,
802+
mock_output_config, mock_mlflow_config, mock_model_package_config,
803+
):
804+
"""When PipelineSession is passed, _intercept_create_request traps the args."""
805+
from sagemaker.train.rlvr_trainer import RLVRTrainer
806+
from sagemaker.core.workflow.pipeline_context import PipelineSession, _JobStepArguments
807+
808+
pipeline_session = Mock(spec=PipelineSession)
809+
pipeline_session.boto_session = Mock()
810+
pipeline_session.boto_session.region_name = "us-west-2"
811+
812+
step_args = _JobStepArguments("train", {"training_job_name": "test-rlvr-job-001"})
813+
pipeline_session._intercept_create_request.return_value = None
814+
pipeline_session.context = step_args
815+
mock_get_session.return_value = pipeline_session
816+
817+
mock_resolve_model.return_value = ("test-model", "resolved-model-name")
818+
mock_hyperparams = Mock()
819+
mock_hyperparams.to_dict.return_value = {"param1": "value1"}
820+
mock_hyperparams._specs = {"param1": {"type": "string"}}
821+
mock_hyperparams._user_set = set()
822+
mock_finetuning_options.return_value = (mock_hyperparams, "arn:aws:sagemaker:us-west-2:123456789012:model/test", False)
823+
mock_validate_group.return_value = "test-group"
824+
mock_get_role.return_value = "arn:aws:iam::123456789012:role/Role"
825+
mock_unique_name.return_value = "test-rlvr-job-001"
826+
mock_input_config.return_value = {"train": "s3://bucket/data"}
827+
mock_convert_channels.return_value = [{"ChannelName": "train"}]
828+
mock_output_config.return_value = {"S3OutputPath": "s3://bucket/output"}
829+
mock_mlflow_config.return_value = None
830+
mock_model_package_config.return_value = None
831+
mock_beta_session.return_value = pipeline_session
832+
833+
trainer = RLVRTrainer(model="test-model", training_dataset="s3://bucket/data", model_package_group="test-group", sagemaker_session=pipeline_session)
834+
trainer._model_arn = "arn:aws:sagemaker:us-west-2:123456789012:model/test"
835+
trainer._model_name = "test-model"
836+
trainer.accept_eula = True
837+
trainer.hyperparameters = mock_hyperparams
838+
trainer.custom_reward_function = None
839+
840+
result = trainer.train()
841+
842+
mock_training_job_create.assert_not_called()
843+
pipeline_session._intercept_create_request.assert_called_once()
844+
assert pipeline_session._intercept_create_request.call_args[0][2] == "train"
845+
assert result == step_args

0 commit comments

Comments
 (0)