From 9ca799c17f08f88dcfd3e8ac43a1980827322681 Mon Sep 17 00:00:00 2001 From: Molly He Date: Tue, 22 Apr 2025 15:05:14 -0700 Subject: [PATCH 1/5] add s3 uri check to modeltrainer data source --- src/sagemaker/modules/train/model_trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sagemaker/modules/train/model_trainer.py b/src/sagemaker/modules/train/model_trainer.py index aef6e3312b..743b5d3e35 100644 --- a/src/sagemaker/modules/train/model_trainer.py +++ b/src/sagemaker/modules/train/model_trainer.py @@ -407,7 +407,7 @@ def _validate_source_code(self, source_code: Optional[SourceCode]): "If 'requirements' or 'entry_script' is provided in 'source_code', " + "'source_dir' must also be provided.", ) - if not _is_valid_path(source_dir, path_type="Directory"): + if not _is_valid_path(source_dir, path_type="Directory") or _is_valid_s3_uri(source_dir, path_type="Directory"): raise ValueError( f"Invalid 'source_dir' path: {source_dir}. " + "Must be a valid directory.", ) From 18f43d1657482463fb1800fff111ecfcdbf082da Mon Sep 17 00:00:00 2001 From: Molly He Date: Wed, 23 Apr 2025 14:52:03 -0700 Subject: [PATCH 2/5] update ModelTrainer to support s3 uri and tar.gz file as source_dir --- src/sagemaker/modules/configs.py | 3 +- src/sagemaker/modules/train/model_trainer.py | 46 +++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/sagemaker/modules/configs.py b/src/sagemaker/modules/configs.py index 458c596a36..b5cb5f2a90 100644 --- a/src/sagemaker/modules/configs.py +++ b/src/sagemaker/modules/configs.py @@ -88,7 +88,8 @@ class SourceCode(BaseConfig): Parameters: source_dir (Optional[str]): - The local directory containing the source code to be used in the training job container. + The local directory, s3 uri, or path to tar.gz file stored locally or in s3 that contains + the source code to be used in the training job container. requirements (Optional[str]): The path within ``source_dir`` to a ``requirements.txt`` file. If specified, the listed requirements will be installed in the training job container. diff --git a/src/sagemaker/modules/train/model_trainer.py b/src/sagemaker/modules/train/model_trainer.py index 743b5d3e35..bdb00c66b2 100644 --- a/src/sagemaker/modules/train/model_trainer.py +++ b/src/sagemaker/modules/train/model_trainer.py @@ -407,28 +407,26 @@ def _validate_source_code(self, source_code: Optional[SourceCode]): "If 'requirements' or 'entry_script' is provided in 'source_code', " + "'source_dir' must also be provided.", ) - if not _is_valid_path(source_dir, path_type="Directory") or _is_valid_s3_uri(source_dir, path_type="Directory"): + if not _is_valid_path(source_dir) and not _is_valid_s3_uri(source_dir): raise ValueError( f"Invalid 'source_dir' path: {source_dir}. " + "Must be a valid directory.", ) if requirements: - if not _is_valid_path( - f"{source_dir}/{requirements}", - path_type="File", - ): - raise ValueError( - f"Invalid 'requirements': {requirements}. " - + "Must be a valid file within the 'source_dir'.", - ) + if not source_dir.endswith(".tar.gz"): + if (not _is_valid_path(f"{source_dir}/{requirements}", path_type="File") + and not _is_valid_s3_uri(f"{source_dir}/{requirements}", path_type="File")): + raise ValueError( + f"Invalid 'requirements': {requirements}. " + + "Must be a valid file within the 'source_dir'.", + ) if entry_script: - if not _is_valid_path( - f"{source_dir}/{entry_script}", - path_type="File", - ): - raise ValueError( - f"Invalid 'entry_script': {entry_script}. " - + "Must be a valid file within the 'source_dir'.", - ) + if not source_dir.endswith(".tar.gz"): + if (not _is_valid_path(f"{source_dir}/{entry_script}", path_type="File") + and not _is_valid_s3_uri(f"{source_dir}/{entry_script}", path_type="File")): + raise ValueError( + f"Invalid 'entry_script': {entry_script}. " + + "Must be a valid file within the 'source_dir'.", + ) def model_post_init(self, __context: Any): """Post init method to perform custom validation and set default values.""" @@ -838,12 +836,20 @@ def _prepare_train_script( install_requirements = "" if source_code.requirements: - install_requirements = "echo 'Installing requirements'\n" - install_requirements = f"$SM_PIP_CMD install -r {source_code.requirements}" + install_requirements = ( + "echo 'Installing requirements'\n" + + f"$SM_PIP_CMD install -r {source_code.requirements}" + ) working_dir = "" if source_code.source_dir: - working_dir = f"cd {SM_CODE_CONTAINER_PATH}" + working_dir = f"cd {SM_CODE_CONTAINER_PATH} \n" + if source_code.source_dir.endswith(".tar.gz"): + if source_code.source_dir.startswith("s3://"): + tarfile_name = os.path.basename(source_code.source_dir) + else: + tarfile_name = source_code.source_dir + working_dir += f"tar --strip-components=1 -xzf {tarfile_name} \n" if base_command: execute_driver = EXECUTE_BASE_COMMANDS.format(base_command=base_command) From 46ae9c58edffd057306097a6ada86c5715cddc9d Mon Sep 17 00:00:00 2001 From: Molly He Date: Wed, 23 Apr 2025 15:00:48 -0700 Subject: [PATCH 3/5] black-format --- src/sagemaker/modules/configs.py | 2 +- src/sagemaker/modules/train/model_trainer.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/sagemaker/modules/configs.py b/src/sagemaker/modules/configs.py index b5cb5f2a90..ac54e2ad0b 100644 --- a/src/sagemaker/modules/configs.py +++ b/src/sagemaker/modules/configs.py @@ -88,7 +88,7 @@ class SourceCode(BaseConfig): Parameters: source_dir (Optional[str]): - The local directory, s3 uri, or path to tar.gz file stored locally or in s3 that contains + The local directory, s3 uri, or path to tar.gz file stored locally or in s3 that contains the source code to be used in the training job container. requirements (Optional[str]): The path within ``source_dir`` to a ``requirements.txt`` file. If specified, the listed diff --git a/src/sagemaker/modules/train/model_trainer.py b/src/sagemaker/modules/train/model_trainer.py index bdb00c66b2..f3661ff76a 100644 --- a/src/sagemaker/modules/train/model_trainer.py +++ b/src/sagemaker/modules/train/model_trainer.py @@ -413,16 +413,22 @@ def _validate_source_code(self, source_code: Optional[SourceCode]): ) if requirements: if not source_dir.endswith(".tar.gz"): - if (not _is_valid_path(f"{source_dir}/{requirements}", path_type="File") - and not _is_valid_s3_uri(f"{source_dir}/{requirements}", path_type="File")): + if not _is_valid_path( + f"{source_dir}/{requirements}", path_type="File" + ) and not _is_valid_s3_uri( + f"{source_dir}/{requirements}", path_type="File" + ): raise ValueError( f"Invalid 'requirements': {requirements}. " + "Must be a valid file within the 'source_dir'.", ) if entry_script: if not source_dir.endswith(".tar.gz"): - if (not _is_valid_path(f"{source_dir}/{entry_script}", path_type="File") - and not _is_valid_s3_uri(f"{source_dir}/{entry_script}", path_type="File")): + if not _is_valid_path( + f"{source_dir}/{entry_script}", path_type="File" + ) and not _is_valid_s3_uri( + f"{source_dir}/{entry_script}", path_type="File" + ): raise ValueError( f"Invalid 'entry_script': {entry_script}. " + "Must be a valid file within the 'source_dir'.", @@ -837,8 +843,8 @@ def _prepare_train_script( install_requirements = "" if source_code.requirements: install_requirements = ( - "echo 'Installing requirements'\n" + - f"$SM_PIP_CMD install -r {source_code.requirements}" + "echo 'Installing requirements'\n" + + f"$SM_PIP_CMD install -r {source_code.requirements}" ) working_dir = "" From db6160bc7c7ce5e56be0fc958ae16731ab595278 Mon Sep 17 00:00:00 2001 From: Molly He Date: Thu, 24 Apr 2025 14:35:40 -0700 Subject: [PATCH 4/5] add unit and integ tests --- src/sagemaker/modules/train/model_trainer.py | 9 ++--- tests/data/modules/script_mode/code.tar.gz | Bin 0 -> 37983 bytes .../modules/train/test_model_trainer.py | 18 +++++++++ .../modules/train/test_model_trainer.py | 35 +++++++++++++++++- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests/data/modules/script_mode/code.tar.gz diff --git a/src/sagemaker/modules/train/model_trainer.py b/src/sagemaker/modules/train/model_trainer.py index f3661ff76a..8be7e6647b 100644 --- a/src/sagemaker/modules/train/model_trainer.py +++ b/src/sagemaker/modules/train/model_trainer.py @@ -409,7 +409,9 @@ def _validate_source_code(self, source_code: Optional[SourceCode]): ) if not _is_valid_path(source_dir) and not _is_valid_s3_uri(source_dir): raise ValueError( - f"Invalid 'source_dir' path: {source_dir}. " + "Must be a valid directory.", + f"Invalid 'source_dir' path: {source_dir}. " + + "Must be a valid local directory, " + "s3 uri or path to tar.gz file stored locally or in s3.", ) if requirements: if not source_dir.endswith(".tar.gz"): @@ -851,10 +853,7 @@ def _prepare_train_script( if source_code.source_dir: working_dir = f"cd {SM_CODE_CONTAINER_PATH} \n" if source_code.source_dir.endswith(".tar.gz"): - if source_code.source_dir.startswith("s3://"): - tarfile_name = os.path.basename(source_code.source_dir) - else: - tarfile_name = source_code.source_dir + tarfile_name = os.path.basename(source_code.source_dir) working_dir += f"tar --strip-components=1 -xzf {tarfile_name} \n" if base_command: diff --git a/tests/data/modules/script_mode/code.tar.gz b/tests/data/modules/script_mode/code.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7c43f35f576640607e79a6f70ccaf84ce897feaa GIT binary patch literal 37983 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 literal 0 HcmV?d00001 diff --git a/tests/integ/sagemaker/modules/train/test_model_trainer.py b/tests/integ/sagemaker/modules/train/test_model_trainer.py index a1e3106553..332b536d77 100644 --- a/tests/integ/sagemaker/modules/train/test_model_trainer.py +++ b/tests/integ/sagemaker/modules/train/test_model_trainer.py @@ -44,6 +44,24 @@ DEFAULT_CPU_IMAGE = "763104351884.dkr.ecr.us-west-2.amazonaws.com/pytorch-training:2.0.0-cpu-py310" +TAR_FILE_SOURCE_DIR = f"{DATA_DIR}/modules/script_mode/code.tar.gz" +TAR_FILE_SOURCE_CODE = SourceCode( + source_dir=TAR_FILE_SOURCE_DIR, + requirements="requirements.txt", + entry_script="custom_script.py", +) + + +def test_source_dir_local_tar_file(modules_sagemaker_session): + model_trainer = ModelTrainer( + sagemaker_session=modules_sagemaker_session, + training_image=DEFAULT_CPU_IMAGE, + source_code=TAR_FILE_SOURCE_CODE, + base_job_name="source_dir_local_tar_file", + ) + + model_trainer.train() + def test_hp_contract_basic_py_script(modules_sagemaker_session): model_trainer = ModelTrainer( diff --git a/tests/unit/sagemaker/modules/train/test_model_trainer.py b/tests/unit/sagemaker/modules/train/test_model_trainer.py index 13530a3983..a3346d77fd 100644 --- a/tests/unit/sagemaker/modules/train/test_model_trainer.py +++ b/tests/unit/sagemaker/modules/train/test_model_trainer.py @@ -163,13 +163,46 @@ def model_trainer(): }, "should_throw": False, }, + { + "init_params": { + "training_image": DEFAULT_IMAGE, + "source_code": SourceCode( + source_dir=f"{DEFAULT_SOURCE_DIR}/code.tar.gz", + entry_script="custom_script.py", + ), + }, + "should_throw": False, + }, + { + "init_params": { + "training_image": DEFAULT_IMAGE, + "source_code": SourceCode( + source_dir="s3://bucket/code", + entry_script="custom_script.py", + ), + }, + "should_throw": False, + }, + { + "init_params": { + "training_image": DEFAULT_IMAGE, + "source_code": SourceCode( + source_dir="s3://bucket/code/code.tar.gz", + entry_script="custom_script.py", + ), + }, + "should_throw": False, + }, ], ids=[ "no_params", "training_image_and_algorithm_name", "only_training_image", "unsupported_source_code", - "supported_source_code", + "supported_source_code_local_dir", + "supported_source_code_local_tar_file", + "supported_source_code_s3_dir", + "supported_source_code_s3_tar_file", ], ) def test_model_trainer_param_validation(test_case, modules_session): From 2b1052d6d133ced923d196d3404e58bd06472d35 Mon Sep 17 00:00:00 2001 From: Molly He Date: Thu, 24 Apr 2025 17:52:53 -0700 Subject: [PATCH 5/5] update logic and unit test to raise value error if the file is not .tar.gz --- src/sagemaker/modules/train/model_trainer.py | 13 ++++++++++- .../modules/train/test_model_trainer.py | 22 ++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/sagemaker/modules/train/model_trainer.py b/src/sagemaker/modules/train/model_trainer.py index 8be7e6647b..4183fb87cd 100644 --- a/src/sagemaker/modules/train/model_trainer.py +++ b/src/sagemaker/modules/train/model_trainer.py @@ -407,7 +407,18 @@ def _validate_source_code(self, source_code: Optional[SourceCode]): "If 'requirements' or 'entry_script' is provided in 'source_code', " + "'source_dir' must also be provided.", ) - if not _is_valid_path(source_dir) and not _is_valid_s3_uri(source_dir): + if not ( + _is_valid_path(source_dir, path_type="Directory") + or _is_valid_s3_uri(source_dir, path_type="Directory") + or ( + _is_valid_path(source_dir, path_type="File") + and source_dir.endswith(".tar.gz") + ) + or ( + _is_valid_s3_uri(source_dir, path_type="File") + and source_dir.endswith(".tar.gz") + ) + ): raise ValueError( f"Invalid 'source_dir' path: {source_dir}. " + "Must be a valid local directory, " diff --git a/tests/unit/sagemaker/modules/train/test_model_trainer.py b/tests/unit/sagemaker/modules/train/test_model_trainer.py index a3346d77fd..6001c5db36 100644 --- a/tests/unit/sagemaker/modules/train/test_model_trainer.py +++ b/tests/unit/sagemaker/modules/train/test_model_trainer.py @@ -92,9 +92,6 @@ source_dir=DEFAULT_SOURCE_DIR, entry_script="custom_script.py", ) -UNSUPPORTED_SOURCE_CODE = SourceCode( - entry_script="train.py", -) DEFAULT_ENTRYPOINT = ["/bin/bash"] DEFAULT_ARGUMENTS = [ "-c", @@ -152,7 +149,19 @@ def model_trainer(): { "init_params": { "training_image": DEFAULT_IMAGE, - "source_code": UNSUPPORTED_SOURCE_CODE, + "source_code": SourceCode( + entry_script="train.py", + ), + }, + "should_throw": True, + }, + { + "init_params": { + "training_image": DEFAULT_IMAGE, + "source_code": SourceCode( + source_dir="s3://bucket/requirements.txt", + entry_script="custom_script.py", + ), }, "should_throw": True, }, @@ -177,7 +186,7 @@ def model_trainer(): "init_params": { "training_image": DEFAULT_IMAGE, "source_code": SourceCode( - source_dir="s3://bucket/code", + source_dir="s3://bucket/code/", entry_script="custom_script.py", ), }, @@ -198,7 +207,8 @@ def model_trainer(): "no_params", "training_image_and_algorithm_name", "only_training_image", - "unsupported_source_code", + "unsupported_source_code_missing_source_dir", + "unsupported_source_code_s3_other_file", "supported_source_code_local_dir", "supported_source_code_local_tar_file", "supported_source_code_s3_dir",