Skip to content

Commit 051c502

Browse files
committed
feat: swap FrameworkProcessor bash shell to sh
Some lightweight Linux distributions oriented towards containers (e.g. Alpine) might not include bash by default: The POSIX-compliant /bin/sh shell is more portable and a less stringent requirement on custom containers to work with FrameworkProcessor. Also expanded scope of tests.
1 parent ff67bb3 commit 051c502

File tree

7 files changed

+107
-15
lines changed

7 files changed

+107
-15
lines changed

src/sagemaker/processing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ class FeatureStoreOutput(ApiObject):
13181318
class FrameworkProcessor(ScriptProcessor):
13191319
"""Handles Amazon SageMaker processing tasks for jobs using a machine learning framework."""
13201320

1321-
framework_entrypoint_command = ["/bin/bash"]
1321+
framework_entrypoint_command = ["/bin/sh"]
13221322

13231323
# Added new (kw)args for estimator. The rest are from ScriptProcessor with same defaults.
13241324
def __init__(
@@ -1811,15 +1811,15 @@ def _generate_framework_script(self, user_script: str) -> str:
18111811
"""
18121812
return dedent(
18131813
"""\
1814-
#!/bin/bash
1814+
#!/bin/sh
18151815
18161816
cd /opt/ml/processing/input/code/
18171817
tar -xzf sourcedir.tar.gz
18181818
18191819
# Exit on any error. SageMaker uses error code to mark failed job.
18201820
set -e
18211821
1822-
if [[ -f 'requirements.txt' ]]; then
1822+
if [ -f 'requirements.txt' ]; then
18231823
# Some py3 containers has typing, which may breaks pip install
18241824
pip uninstall --yes typing
18251825
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""A dummy Python module to check importing local files works OK"""
2+
DUMMY_CONSTANT = 1
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""A dummy SageMaker job script testing local imports"""
2+
3+
print("This is the print output from dummy_code_bundle_no_reqs/main_script.py")
4+
5+
print("Trying to import local module...")
6+
import local_module
7+
8+
print("Done")

tests/integ/test_local_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def test_local_processing_sklearn(sagemaker_local_session_no_local_code, sklearn
353353
assert job_description["ProcessingResources"]["ClusterConfig"]["InstanceCount"] == 1
354354
assert job_description["ProcessingResources"]["ClusterConfig"]["InstanceType"] == "local"
355355
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
356-
"/bin/bash",
356+
"/bin/sh",
357357
"/opt/ml/processing/input/entrypoint/runproc.sh",
358358
]
359359
assert job_description["RoleArn"] == "<no_role>"

tests/integ/test_processing.py

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def test_sklearn(sagemaker_session, sklearn_latest_version, cpu_instance_type):
146146
assert job_description["ProcessingResources"]["ClusterConfig"]["VolumeSizeInGB"] == 30
147147
assert job_description["StoppingCondition"] == {"MaxRuntimeInSeconds": 86400}
148148
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
149-
"/bin/bash",
149+
"/bin/sh",
150150
"/opt/ml/processing/input/entrypoint/runproc.sh",
151151
]
152152
assert ROLE in job_description["RoleArn"]
@@ -157,6 +157,7 @@ def test_sklearn_with_customizations(
157157
sagemaker_session, image_uri, sklearn_latest_version, cpu_instance_type, output_kms_key
158158
):
159159
input_file_path = os.path.join(DATA_DIR, "dummy_input.txt")
160+
code_bundle_path = os.path.join(DATA_DIR, "dummy_code_bundle_with_reqs")
160161

161162
sklearn_processor = SKLearnProcessor(
162163
framework_version=sklearn_latest_version,
@@ -175,7 +176,8 @@ def test_sklearn_with_customizations(
175176
)
176177

177178
sklearn_processor.run(
178-
code=os.path.join(DATA_DIR, "dummy_script.py"),
179+
code="main_script.py",
180+
source_dir=code_bundle_path,
179181
inputs=[
180182
ProcessingInput(
181183
source=input_file_path,
@@ -221,7 +223,7 @@ def test_sklearn_with_customizations(
221223

222224
assert job_description["AppSpecification"]["ContainerArguments"] == ["-v"]
223225
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
224-
"/bin/bash",
226+
"/bin/sh",
225227
"/opt/ml/processing/input/entrypoint/runproc.sh",
226228
]
227229
assert job_description["AppSpecification"]["ImageUri"] == image_uri
@@ -309,7 +311,7 @@ def test_sklearn_with_custom_default_bucket(
309311

310312
assert job_description["AppSpecification"]["ContainerArguments"] == ["-v"]
311313
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
312-
"/bin/bash",
314+
"/bin/sh",
313315
"/opt/ml/processing/input/entrypoint/runproc.sh",
314316
]
315317
assert job_description["AppSpecification"]["ImageUri"] == image_uri
@@ -362,7 +364,7 @@ def test_sklearn_with_no_inputs_or_outputs(
362364

363365
assert job_description["AppSpecification"]["ContainerArguments"] == ["-v"]
364366
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
365-
"/bin/bash",
367+
"/bin/sh",
366368
"/opt/ml/processing/input/entrypoint/runproc.sh",
367369
]
368370
assert job_description["AppSpecification"]["ImageUri"] == image_uri
@@ -452,6 +454,80 @@ def test_script_processor(sagemaker_session, image_uri, cpu_instance_type, outpu
452454
assert job_description["StoppingCondition"] == {"MaxRuntimeInSeconds": 3600}
453455

454456

457+
@pytest.mark.release
458+
def test_script_processor_with_source_dir(sagemaker_session, image_uri, cpu_instance_type, output_kms_key):
459+
input_file_path = os.path.join(DATA_DIR, "dummy_input.txt")
460+
source_dir=os.path.join(DATA_DIR, "dummy_code_bundle_no_reqs")
461+
462+
script_processor = ScriptProcessor(
463+
role=ROLE,
464+
image_uri=image_uri,
465+
command=["python3"],
466+
instance_count=1,
467+
instance_type=cpu_instance_type,
468+
volume_kms_key=None,
469+
output_kms_key=output_kms_key,
470+
max_runtime_in_seconds=600,
471+
base_job_name="test-script-processor",
472+
sagemaker_session=sagemaker_session,
473+
)
474+
475+
script_processor.run(
476+
code="main_script.py",
477+
source_dir=source_dir,
478+
inputs=[
479+
ProcessingInput(
480+
source=input_file_path,
481+
destination="/opt/ml/processing/input/container/path/",
482+
input_name="dummy_input",
483+
s3_data_type="S3Prefix",
484+
s3_input_mode="File",
485+
s3_data_distribution_type="FullyReplicated",
486+
s3_compression_type="None",
487+
)
488+
],
489+
outputs=[
490+
ProcessingOutput(
491+
source="/opt/ml/processing/output/container/path/",
492+
output_name="dummy_output",
493+
s3_upload_mode="EndOfJob",
494+
)
495+
],
496+
arguments=["-v"],
497+
wait=True,
498+
logs=True,
499+
)
500+
501+
job_description = script_processor.latest_job.describe()
502+
503+
assert job_description["ProcessingInputs"][0]["InputName"] == "dummy_input"
504+
505+
assert job_description["ProcessingInputs"][1]["InputName"] == "code"
506+
507+
assert job_description["ProcessingJobName"].startswith("test-script-processor")
508+
509+
assert job_description["ProcessingJobStatus"] == "Completed"
510+
511+
assert job_description["ProcessingOutputConfig"]["KmsKeyId"] == output_kms_key
512+
assert job_description["ProcessingOutputConfig"]["Outputs"][0]["OutputName"] == "dummy_output"
513+
514+
assert job_description["ProcessingResources"]["ClusterConfig"]["InstanceCount"] == 1
515+
assert (
516+
job_description["ProcessingResources"]["ClusterConfig"]["InstanceType"] == cpu_instance_type
517+
)
518+
519+
assert job_description["AppSpecification"]["ContainerArguments"] == ["-v"]
520+
assert job_description["AppSpecification"]["ContainerEntrypoint"] == [
521+
"python3",
522+
"/opt/ml/processing/input/code/main_script.py",
523+
]
524+
assert job_description["AppSpecification"]["ImageUri"] == image_uri
525+
526+
assert ROLE in job_description["RoleArn"]
527+
528+
assert job_description["StoppingCondition"] == {"MaxRuntimeInSeconds": 600}
529+
530+
455531
def test_script_processor_with_no_inputs_or_outputs(
456532
sagemaker_session, image_uri, cpu_instance_type
457533
):
@@ -680,7 +756,7 @@ def test_processor_with_custom_bucket(
680756

681757

682758
def test_sklearn_with_network_config(sagemaker_session, sklearn_latest_version, cpu_instance_type):
683-
script_path = os.path.join(DATA_DIR, "dummy_script.py")
759+
code_bundle_path = os.path.join(DATA_DIR, "dummy_code_bundle_no_reqs")
684760
input_file_path = os.path.join(DATA_DIR, "dummy_input.txt")
685761

686762
sklearn_processor = SKLearnProcessor(
@@ -697,13 +773,17 @@ def test_sklearn_with_network_config(sagemaker_session, sklearn_latest_version,
697773
)
698774

699775
sklearn_processor.run(
700-
code=script_path,
776+
code="main_script.py",
777+
source_dir=code_bundle_path,
701778
inputs=[ProcessingInput(source=input_file_path, destination="/opt/ml/processing/inputs/")],
702-
wait=False,
779+
wait=True,
703780
logs=False,
704781
)
705782

706783
job_description = sklearn_processor.latest_job.describe()
784+
785+
assert job_description["ProcessingJobStatus"] == "Completed"
786+
707787
network_config = job_description["NetworkConfig"]
708788
assert network_config["EnableInterContainerTrafficEncryption"]
709789
assert network_config["EnableNetworkIsolation"]

tests/unit/sagemaker/huggingface/test_processing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def _get_expected_args_modular_code(job_name, code_s3_uri=f"s3://{BUCKET_NAME}")
134134
"app_specification": {
135135
"ImageUri": CUSTOM_IMAGE_URI,
136136
"ContainerEntrypoint": [
137-
"/bin/bash",
137+
"/bin/sh",
138138
"/opt/ml/processing/input/entrypoint/runproc.sh",
139139
],
140140
},

tests/unit/test_processing.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ def test_sklearn_with_all_parameters_via_run_args(
271271

272272
processor.run(
273273
code=run_args.code,
274+
source_dir="/local/path/to/source_dir",
274275
inputs=run_args.inputs,
275276
outputs=run_args.outputs,
276277
arguments=run_args.arguments,
@@ -345,6 +346,7 @@ def test_sklearn_with_all_parameters_via_run_args_called_twice(
345346

346347
processor.run(
347348
code=run_args.code,
349+
source_dir="/local/path/to/source_dir",
348350
inputs=run_args.inputs,
349351
outputs=run_args.outputs,
350352
arguments=run_args.arguments,
@@ -974,7 +976,7 @@ def _get_expected_args_modular_code(job_name, code_s3_uri=f"s3://{BUCKET_NAME}")
974976
"app_specification": {
975977
"ImageUri": CUSTOM_IMAGE_URI,
976978
"ContainerEntrypoint": [
977-
"/bin/bash",
979+
"/bin/sh",
978980
"/opt/ml/processing/input/entrypoint/runproc.sh",
979981
],
980982
},
@@ -1215,7 +1217,7 @@ def _get_expected_args_all_parameters_modular_code(
12151217
"ImageUri": "012345678901.dkr.ecr.us-west-2.amazonaws.com/my-custom-image-uri",
12161218
"ContainerArguments": ["--drop-columns", "'SelfEmployed'"],
12171219
"ContainerEntrypoint": [
1218-
"/bin/bash",
1220+
"/bin/sh",
12191221
"/opt/ml/processing/input/entrypoint/runproc.sh",
12201222
],
12211223
},

0 commit comments

Comments
 (0)