|
17 | 17 | import os |
18 | 18 | import io |
19 | 19 | import logging |
| 20 | +import shutil |
| 21 | +import subprocess |
| 22 | + |
| 23 | +import pytest |
20 | 24 |
|
21 | 25 | from unittest.mock import patch |
22 | 26 |
|
|
87 | 91 |
|
88 | 92 | # flake8: noqa |
89 | 93 | EXPECTED_ENV = """ |
90 | | -export SM_MODEL_DIR='/opt/ml/model' |
91 | | -export SM_INPUT_DIR='/opt/ml/input' |
92 | | -export SM_INPUT_DATA_DIR='/opt/ml/input/data' |
93 | | -export SM_INPUT_CONFIG_DIR='/opt/ml/input/config' |
94 | | -export SM_OUTPUT_DIR='/opt/ml/output' |
95 | | -export SM_OUTPUT_FAILURE='/opt/ml/output/failure' |
96 | | -export SM_OUTPUT_DATA_DIR='/opt/ml/output/data' |
97 | | -export SM_LOG_LEVEL='20' |
98 | | -export SM_MASTER_ADDR='algo-1' |
99 | | -export SM_MASTER_PORT='7777' |
100 | | -export SM_SOURCE_DIR='/opt/ml/input/data/code' |
101 | | -export SM_ENTRY_SCRIPT='train.py' |
102 | | -export SM_DISTRIBUTED_DRIVER_DIR='/opt/ml/input/data/sm_drivers/distributed_drivers' |
| 94 | +export SM_MODEL_DIR=/opt/ml/model |
| 95 | +export SM_INPUT_DIR=/opt/ml/input |
| 96 | +export SM_INPUT_DATA_DIR=/opt/ml/input/data |
| 97 | +export SM_INPUT_CONFIG_DIR=/opt/ml/input/config |
| 98 | +export SM_OUTPUT_DIR=/opt/ml/output |
| 99 | +export SM_OUTPUT_FAILURE=/opt/ml/output/failure |
| 100 | +export SM_OUTPUT_DATA_DIR=/opt/ml/output/data |
| 101 | +export SM_LOG_LEVEL=20 |
| 102 | +export SM_MASTER_ADDR=algo-1 |
| 103 | +export SM_MASTER_PORT=7777 |
| 104 | +export SM_SOURCE_DIR=/opt/ml/input/data/code |
| 105 | +export SM_ENTRY_SCRIPT=train.py |
| 106 | +export SM_DISTRIBUTED_DRIVER_DIR=/opt/ml/input/data/sm_drivers/distributed_drivers |
103 | 107 | export SM_DISTRIBUTED_CONFIG='{"process_count_per_node": 2}' |
104 | | -export SM_CHANNEL_TRAIN='/opt/ml/input/data/train' |
105 | | -export SM_CHANNEL_VALIDATION='/opt/ml/input/data/validation' |
| 108 | +export SM_CHANNEL_TRAIN=/opt/ml/input/data/train |
| 109 | +export SM_CHANNEL_VALIDATION=/opt/ml/input/data/validation |
106 | 110 | export SM_CHANNELS='["train", "validation"]' |
107 | | -export SM_HP_BATCH_SIZE='32' |
108 | | -export SM_HP_LEARNING_RATE='0.001' |
| 111 | +export SM_HP_BATCH_SIZE=32 |
| 112 | +export SM_HP_LEARNING_RATE=0.001 |
109 | 113 | export SM_HP_HOSTS='["algo-1", "algo-2"]' |
110 | 114 | export SM_HP_MP_PARAMETERS='{"microbatches": 2, "partitions": 2, "pipeline": "interleaved", "optimize": "memory", "horovod": true}' |
111 | 115 | export SM_HPS='{"batch_size": 32, "learning_rate": 0.001, "hosts": ["algo-1", "algo-2"], "mp_parameters": {"microbatches": 2, "partitions": 2, "pipeline": "interleaved", "optimize": "memory", "horovod": true}}' |
112 | | -export SM_CURRENT_HOST='algo-1' |
113 | | -export SM_CURRENT_INSTANCE_TYPE='ml.p3.16xlarge' |
| 116 | +export SM_CURRENT_HOST=algo-1 |
| 117 | +export SM_CURRENT_INSTANCE_TYPE=ml.p3.16xlarge |
114 | 118 | export SM_HOSTS='["algo-1", "algo-2", "algo-3"]' |
115 | | -export SM_NETWORK_INTERFACE_NAME='eth0' |
116 | | -export SM_HOST_COUNT='3' |
117 | | -export SM_CURRENT_HOST_RANK='0' |
118 | | -export SM_NUM_CPUS='8' |
119 | | -export SM_NUM_GPUS='0' |
120 | | -export SM_NUM_NEURONS='0' |
| 119 | +export SM_NETWORK_INTERFACE_NAME=eth0 |
| 120 | +export SM_HOST_COUNT=3 |
| 121 | +export SM_CURRENT_HOST_RANK=0 |
| 122 | +export SM_NUM_CPUS=8 |
| 123 | +export SM_NUM_GPUS=0 |
| 124 | +export SM_NUM_NEURONS=0 |
121 | 125 | export SM_RESOURCE_CONFIG='{"current_host": "algo-1", "hosts": ["algo-1", "algo-2", "algo-3"], "current_group_name": "train1", "current_instance_type": "ml.p3.16xlarge", "instance_groups": [{"instance_group_name": "train1", "instance_type": "ml.p3.16xlarge", "hosts": ["algo-1", "algo-2"]}, {"instance_group_name": "train2", "instance_type": "ml.p3.8xlarge", "hosts": ["algo-3"]}], "network_interface_name": "eth0"}' |
122 | 126 | export SM_INPUT_DATA_CONFIG='{"train": {"ContentType": "trainingContentType", "TrainingInputMode": "File", "S3DistributionType": "FullyReplicated", "RecordWrapperType": "None"}, "validation": {"TrainingInputMode": "File", "S3DistributionType": "FullyReplicated", "RecordWrapperType": "None"}}' |
123 | 127 | export SM_TRAINING_ENV='{"channel_input_dirs": {"train": "/opt/ml/input/data/train", "validation": "/opt/ml/input/data/validation"}, "current_host": "algo-1", "current_instance_type": "ml.p3.16xlarge", "hosts": ["algo-1", "algo-2", "algo-3"], "master_addr": "algo-1", "master_port": 7777, "hyperparameters": {"batch_size": 32, "learning_rate": 0.001, "hosts": ["algo-1", "algo-2"], "mp_parameters": {"microbatches": 2, "partitions": 2, "pipeline": "interleaved", "optimize": "memory", "horovod": true}}, "input_data_config": {"train": {"ContentType": "trainingContentType", "TrainingInputMode": "File", "S3DistributionType": "FullyReplicated", "RecordWrapperType": "None"}, "validation": {"TrainingInputMode": "File", "S3DistributionType": "FullyReplicated", "RecordWrapperType": "None"}}, "input_config_dir": "/opt/ml/input/config", "input_data_dir": "/opt/ml/input/data", "input_dir": "/opt/ml/input", "job_name": "test-job", "log_level": 20, "model_dir": "/opt/ml/model", "network_interface_name": "eth0", "num_cpus": 8, "num_gpus": 0, "num_neurons": 0, "output_data_dir": "/opt/ml/output/data", "resource_config": {"current_host": "algo-1", "hosts": ["algo-1", "algo-2", "algo-3"], "current_group_name": "train1", "current_instance_type": "ml.p3.16xlarge", "instance_groups": [{"instance_group_name": "train1", "instance_type": "ml.p3.16xlarge", "hosts": ["algo-1", "algo-2"]}, {"instance_group_name": "train2", "instance_type": "ml.p3.8xlarge", "hosts": ["algo-3"]}], "network_interface_name": "eth0"}}' |
@@ -212,3 +216,59 @@ def test_log_env_variables(): |
212 | 216 | def _remove_extra_lines(string): |
213 | 217 | """Removes extra blank lines from a string.""" |
214 | 218 | return "\n".join([line for line in string.splitlines() if line.strip()]) |
| 219 | + |
| 220 | + |
| 221 | +_ENV_PATCHES = [ |
| 222 | + patch( |
| 223 | + "sagemaker.train.container_drivers.scripts.environment.read_source_code_json", |
| 224 | + return_value={}, |
| 225 | + ), |
| 226 | + patch( |
| 227 | + "sagemaker.train.container_drivers.scripts.environment.read_distributed_json", |
| 228 | + return_value={}, |
| 229 | + ), |
| 230 | + patch("sagemaker.train.container_drivers.scripts.environment.num_cpus", return_value=1), |
| 231 | + patch("sagemaker.train.container_drivers.scripts.environment.num_gpus", return_value=0), |
| 232 | + patch("sagemaker.train.container_drivers.scripts.environment.num_neurons", return_value=0), |
| 233 | +] |
| 234 | + |
| 235 | + |
| 236 | +def _write_env(hyperparameters, output_file): |
| 237 | + """Run set_env with the given hyperparameters and no source code or distributed config.""" |
| 238 | + with patch.dict(os.environ, {"TRAINING_JOB_NAME": "test-job"}): |
| 239 | + for p in _ENV_PATCHES: |
| 240 | + p.start() |
| 241 | + try: |
| 242 | + set_env( |
| 243 | + resource_config=RESOURCE_CONFIG, |
| 244 | + input_data_config=INPUT_DATA_CONFIG, |
| 245 | + hyperparameters_config=hyperparameters, |
| 246 | + output_file=output_file, |
| 247 | + ) |
| 248 | + finally: |
| 249 | + for p in _ENV_PATCHES: |
| 250 | + p.stop() |
| 251 | + |
| 252 | + |
| 253 | +@pytest.mark.skipif(shutil.which("bash") is None, reason="needs bash to source the env file") |
| 254 | +@pytest.mark.parametrize( |
| 255 | + "payload", |
| 256 | + ["it's 0.001'; touch {marker}; echo '", "'$(touch {marker})'", "x';touch {marker};#"], |
| 257 | +) |
| 258 | +def test_set_env_hyperparameter_value_survives_source_verbatim(tmp_path, payload): |
| 259 | + """A hyperparameter value must round-trip through `source` and never be executed.""" |
| 260 | + marker = tmp_path / "marker" |
| 261 | + payload = payload.format(marker=marker) |
| 262 | + output_file = str(tmp_path / "sm_training.env") |
| 263 | + |
| 264 | + _write_env({"learning_rate": payload}, output_file) |
| 265 | + |
| 266 | + result = subprocess.run( |
| 267 | + ["bash", "-c", f'source "{output_file}"; printf "%s" "$SM_HP_LEARNING_RATE"'], |
| 268 | + capture_output=True, |
| 269 | + text=True, |
| 270 | + check=True, |
| 271 | + ) |
| 272 | + |
| 273 | + assert result.stdout == payload |
| 274 | + assert not marker.exists() |
0 commit comments