aws / aws/amazon-sagemaker-feedback

Training job hyperparameter name configurable via pipeline parameter.

Open
#197 3 comments 4 reactions 0 assignees View on GitHub
feature-request
Dominant language
No language data
Stars
10
Forks
3
PR merge metrics
No merged PRs in 30d

Description

### Product Version

- [ ] Amazon SageMaker Studio Classic
- [ ] Amazon SageMaker Studio
- [x] It is not related to SageMaker Studio

### Product Category

Pipelines

### Description

I'd like to be able to configure the training job hyperparameter name via a pipeline variable. The following code illustrates what I'd like to achieve:
```python
from sagemaker.pytorch import PyTorch
from sagemaker.workflow.parameters import ParameterString
from sagemaker.workflow.steps import TrainingStep
from sagemaker.workflow.pipeline import Pipeline
from sagemaker.workflow.pipeline_context import (
PipelineSession
)

if __name__ == "__main__":
pipeline_session = PipelineSession()

hparam_name = ParameterString("HParamName")
hparam_value = ParameterString("HParamValue")
estimator = PyTorch(
sagemaker_session=pipeline_session,
instance_type='ml.m5.large',
instance_count=1,
framework_version="2.3",
py_version="py311",
source_dir='source',
entry_point='main.py',
hyperparameters={
hparam_name: hparam_value
},
metric_definitions=[
{'Name': 'valid:loss', 'Regex': 'valid_loss=([0-9]+\\.?[0-9]*)'}
]
)

training_step = TrainingStep(
name="Training",
step_args=estimator.fit(),
)

pipeline = Pipeline(
name="TestTrainingPipeline",
parameters=[hparam_name, hparam_value],
steps=[training_step],
sagemaker_session=pipeline_session
)

pipeline.upsert()
pipeline.start(
execution_display_name="TrainingTestParam1",
parameters={
"HParamName": "hparam1",
"HParamValue": 1.0
}
)
pipeline.start(
execution_display_name="TrainingTestParam2",
parameters={
"HParamName": "hparam2",
"HParamValue": 1.0
}
)
```
I would expect this code to create a pipeline with one training step and run it the first time using the hyperparameter hparam1 and the second time using the hyperparameter hparam2.

The above code ends up with exception: `TypeError: Pipeline variables do not support __str__ operation. Please use `.to_string()` to convert it to string type in execution time or use `.expr` to translate it to Json for display purpose in Python SDK.`
Traceback:
```
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ HIDDEN_PATH/test.py:44 in │
│ │
│ 41 │ │ sagemaker_session=pipeline_session │
│ 42 │ ) │
│ 43 │ │
│ ❱ 44 │ pipeline.upsert() │
│ 45 │ pipeline.start( │
│ 46 │ │ execution_display_name="TrainingTestParam1", │
│ 47 │ │ parameters={"HParamName": "hparam1"} │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/pipeline.py:292 in upsert │
│ │
│ 289 │ │ │ # after fetching the config. │
│ 290 │ │ │ raise ValueError("An AWS IAM role is required to create or update a Pipeline │
│ 291 │ │ try: │
│ ❱ 292 │ │ │ response = self.create(role_arn, description, tags, parallelism_config) │
│ 293 │ │ except ClientError as ce: │
│ 294 │ │ │ error_code = ce.response["Error"]["Code"] │
│ 295 │ │ │ error_message = ce.response["Error"]["Message"] │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/pipeline.py:164 in create │
│ │
│ 161 │ │ tags = format_tags(tags) │
│ 162 │ │ tags = _append_project_tags(tags) │
│ 163 │ │ tags = self.sagemaker_session._append_sagemaker_config_tags(tags, PIPELINE_TAGS_ │
│ ❱ 164 │ │ kwargs = self._create_args(role_arn, description, parallelism_config) │
│ 165 │ │ update_args( │
│ 166 │ │ │ kwargs, │
│ 167 │ │ │ Tags=tags, │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/pipeline.py:186 │
│ in _create_args │
│ │
│ 183 │ │ Returns: │
│ 184 │ │ │ A keyword argument dict for calling create_pipeline. │
│ 185 │ │ """ │
│ ❱ 186 │ │ pipeline_definition = self.definition() │
│ 187 │ │ kwargs = dict( │
│ 188 │ │ │ PipelineName=self.name, │
│ 189 │ │ │ RoleArn=role_arn, │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/pipeline.py:392 in definition │
│ │
│ 389 │ │ │ sagemaker_session=self.sagemaker_session, │
│ 390 │ │ │ steps=self.steps, │
│ 391 │ │ │ pipeline_definition_config=self.pipeline_definition_config, │
│ ❱ 392 │ │ ).build() │
│ 393 │ │ │
│ 394 │ │ request_dict = { │
│ 395 │ │ │ "Version": self._version, │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/_steps_compiler.py:406 │
│ in build │
│ │
│ 403 │ │ if self._build_count > 1: │
│ 404 │ │ │ raise RuntimeError("Cannot build a pipeline more than once with the same com │
│ 405 │ │ │
│ ❱ 406 │ │ return self._initialize_queue_and_build(self._input_steps) │
│ 407 │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/_steps_compiler.py:390 │
│ in _initialize_queue_and_build │
│ │
│ 387 │ │ │ if isinstance(step, ConditionStep): │
│ 388 │ │ │ │ compiled_steps.append(self._build_condition_step(step)) │
│ 389 │ │ │ else: │
│ ❱ 390 │ │ │ │ compiled_steps.append(self._build_step(step)) │
│ 391 │ │ │
│ 392 │ │ self._set_serialize_output_to_json_flag(compiled_steps) │
│ 393 │ │ return compiled_steps │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/_steps_compiler.py:331 │
│ in _build_step │
│ │
│ 328 │ │ │ pipeline_build_time=self.pipeline_build_time, │
│ 329 │ │ │ function_step_secret_token=self._function_step_secret_token, │
│ 330 │ │ ) as context: │
│ ❱ 331 │ │ │ request_dict = step.to_request() │
│ 332 │ │ │ │
│ 333 │ │ │ self.upload_runtime_scripts = context.upload_runtime_scripts │
│ 334 │ │ │ self.upload_workspace = context.upload_workspace │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/steps.py:551 in to_request │
│ │
│ 548 │ │
│ 549 │ def to_request(self) -> RequestType: │
│ 550 │ │ """Updates the request dictionary with cache configuration.""" │
│ ❱ 551 │ │ request_dict = super().to_request() │
│ 552 │ │ if self.cache_config: │
│ 553 │ │ │ request_dict.update(self.cache_config.config) │
│ 554 │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/steps.py:390 in to_request │
│ │
│ 387 │ │
│ 388 │ def to_request(self) -> RequestType: │
│ 389 │ │ """Gets the request structure for `ConfigurableRetryStep`.""" │
│ ❱ 390 │ │ step_dict = super().to_request() │
│ 391 │ │ if self.retry_policies: │
│ 392 │ │ │ step_dict["RetryPolicies"] = self._resolve_retry_policy(self.retry_policies) │
│ 393 │ │ return step_dict │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/steps.py:147 in to_request │
│ │
│ 144 │ │ request_dict = { │
│ 145 │ │ │ "Name": self.name, │
│ 146 │ │ │ "Type": self.step_type.value, │
│ ❱ 147 │ │ │ "Arguments": self.arguments, │
│ 148 │ │ } │
│ 149 │ │ if self.depends_on: │
│ 150 │ │ │ request_dict["DependsOn"] = list(self.depends_on) │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/steps.py:522 in arguments │
│ │
│ 519 │ │ if self.step_args: │
│ 520 │ │ │ # execute fit function with saved parameters, │
│ 521 │ │ │ # and store args in PipelineSession's _context │
│ ❱ 522 │ │ │ execute_job_functions(self.step_args) │
│ 523 │ │ │ │
│ 524 │ │ │ # populate request dict with args │
│ 525 │ │ │ estimator = self.step_args.func_args[0] │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/utilities.py:444 │
│ in execute_job_functions │
│ │
│ 441 │ │ │ a pipeline step, contains the necessary function information │
│ 442 │ """ │
│ 443 │ │
│ ❱ 444 │ chained_args = step_args.func(*step_args.func_args, **step_args.func_kwargs) │
│ 445 │ if isinstance(chained_args, _StepArguments): │
│ 446 │ │ execute_job_functions(chained_args) │
│ 447 │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/estimator.py:1373 in fit │
│ │
│ 1370 │ │ self._prepare_for_training(job_name=job_name) │
│ 1371 │ │ │
│ 1372 │ │ experiment_config = check_and_get_run_experiment_config(experiment_config) │
│ ❱ 1373 │ │ self.latest_training_job = _TrainingJob.start_new(self, inputs, experiment_confi │
│ 1374 │ │ self.jobs.append(self.latest_training_job) │
│ 1375 │ │ forward_to_mlflow_tracking_server = False │
│ 1376 │ │ if os.environ.get("MLFLOW_TRACKING_URI") and self.enable_network_isolation(): │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/estimator.py:2511 in start_new │
│ │
│ 2508 │ │ │ sagemaker.estimator._TrainingJob: Constructed object that captures │
│ 2509 │ │ │ all information about the started training job. │
│ 2510 │ │ """ │
│ ❱ 2511 │ │ train_args = cls._get_train_args(estimator, inputs, experiment_config) │
│ 2512 │ │ │
│ 2513 │ │ logger.debug("Train args after processing defaults: %s", train_args) │
│ 2514 │ │ estimator.sagemaker_session.train(**train_args) │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/estimator.py:2556 in _get_train_args │
│ │
│ 2553 │ │ │
│ 2554 │ │ config = _Job._load_config(inputs, estimator) │
│ 2555 │ │ │
│ ❱ 2556 │ │ current_hyperparameters = estimator.hyperparameters() │
│ 2557 │ │ if current_hyperparameters is not None: │
│ 2558 │ │ │ hyperparameters = {str(k): to_string(v) for (k, v) in current_hyperparameter │
│ 2559 │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/pytorch/estimator.py:487 │
│ in hyperparameters │
│ │
│ 484 │ │
│ 485 │ def hyperparameters(self): │
│ 486 │ │ """Return hyperparameters used by your custom PyTorch code during model training │
│ ❱ 487 │ │ hyperparameters = super(PyTorch, self).hyperparameters() │
│ 488 │ │ additional_hyperparameters = self._pytorch_distribution_configuration( │
│ 489 │ │ │ distribution=self.distribution │
│ 490 │ │ ) │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/estimator.py:3725 in hyperparameters │
│ │
│ 3722 │ │ Returns: │
│ 3723 │ │ │ dict[str, str]: The hyperparameters. │
│ 3724 │ │ """ │
│ ❱ 3725 │ │ return EstimatorBase._json_encode_hyperparameters(self._hyperparameters) │
│ 3726 │ │
│ 3727 │ @classmethod │
│ 3728 │ def _prepare_init_params_from_job_description(cls, job_details, model_channel_name=N │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/estimator.py:900 │
│ in _json_encode_hyperparameters │
│ │
│ 897 │ │ current_hyperparameters = hyperparameters │
│ 898 │ │ if current_hyperparameters is not None: │
│ 899 │ │ │ hyperparameters = { │
│ ❱ 900 │ │ │ │ str(k): (v.to_string() if is_pipeline_variable(v) else json.dumps(v)) │
│ 901 │ │ │ │ for (k, v) in current_hyperparameters.items() │
│ 902 │ │ │ } │
│ 903 │ │ return hyperparameters │
│ │
│ HIDDEN_PATH/.venv/lib/python3.12/site-packages/sagemaker/workflow/entities.py:90 in __str__ │ │
│ │
│ 87 │ │
│ 88 │ def __str__(self): │
│ 89 │ │ """Override built-in String function for PipelineVariable""" │
│ ❱ 90 │ │ raise TypeError( │
│ 91 │ │ │ "Pipeline variables do not support __str__ operation. " │
│ 92 │ │ │ "Please use `.to_string()` to convert it to string type in execution time " │
│ 93 │ │ │ "or use `.expr` to translate it to Json for display purpose in Python SDK." │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: Pipeline variables do not support __str__ operation. Please use `.to_string()` to convert it to string type
in execution time or use `.expr` to translate it to Json for display purpose in Python SDK.
```
But it seems that it is not only a problem with the request serialization on the SDK side, but in general CreateTrainingJob request does not support hyperparameters names being pipeline variable expressions.

Why I need that?
I want to train and evaluate several different ML models for the same task within a single pipeline. These models differ in some hyperparameters. Currently, I'm working around this problem by passing the hyperparameters via a file that is one of the inputs to the training step.

### Other Details

_No response_

Contributor guide

Open the contributing guide

Research direction

Reproduce the example with the SageMaker Python SDK, then trace pipeline.py, _steps_compiler.py, and steps.py from Pipeline.upsert() through step serialization. Determine whether a ParameterString can supply a training hyperparameter name, and verify that the pipeline can be created and run with hparam1 and hparam2 across separate executions.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, python
Domain
cloud
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.