Skip to content

Commit f99ae71

Browse files
Harsh270519Harsh Thakkar
andauthored
Fix/skip none hyperparameters (#5775)
* fix: skip None hyperparameters in to_dict instead of converting to string 'None' * fix: correct test mocks to use 'default' key matching real _extract_eval_override_options return format --------- Co-authored-by: Harsh Thakkar <harshnj@amazon.com>
1 parent 736781b commit f99ae71

3 files changed

Lines changed: 67 additions & 11 deletions

File tree

sagemaker-train/src/sagemaker/train/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, options_dict: Dict[str, Any]):
3333

3434
def to_dict(self) -> Dict[str, Any]:
3535
"""Convert back to dictionary for hyperparameters with string values."""
36-
return {k: str(getattr(self, k)) for k in self._specs.keys()}
36+
return {k: str(v) for k in self._specs.keys() if (v := getattr(self, k)) is not None}
3737

3838
def __setattr__(self, name: str, value: Any):
3939
if name.startswith('_'):

sagemaker-train/tests/unit/train/evaluate/test_custom_scorer_evaluator.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ def test_custom_scorer_evaluator_get_custom_scorer_template_additions_with_aggre
446446

447447
# Mock recipe utils
448448
mock_get_params.return_value = {'temperature': 0.5}
449-
mock_extract_options.return_value = {'temperature': {'value': 0.5}}
449+
mock_extract_options.return_value = {'temperature': {'default': 0.5}}
450450

451451
evaluator = CustomScorerEvaluator(
452452
evaluator=DEFAULT_EVALUATOR_ARN,
@@ -610,7 +610,7 @@ def test_custom_scorer_evaluator_evaluate_method(
610610

611611
# Mock recipe utils
612612
mock_get_params.return_value = {'temperature': 0.7}
613-
mock_extract_options.return_value = {'temperature': {'value': 0.7}}
613+
mock_extract_options.return_value = {'temperature': {'default': 0.7}}
614614

615615
# Mock Pipeline and execution
616616
mock_pipeline_instance = Mock()
@@ -673,7 +673,7 @@ def test_custom_scorer_evaluator_evaluate_with_model_package(
673673

674674
# Mock recipe utils
675675
mock_get_params.return_value = {'temperature': 0.7}
676-
mock_extract_options.return_value = {'temperature': {'value': 0.7}}
676+
mock_extract_options.return_value = {'temperature': {'default': 0.7}}
677677

678678
# Mock Pipeline and execution
679679
mock_pipeline_instance = Mock()
@@ -839,8 +839,8 @@ def test_custom_scorer_evaluator_hyperparameters_property(mock_artifact, mock_re
839839
# Mock recipe utils
840840
mock_get_params.return_value = {'temperature': 0.7, 'max_new_tokens': 2048}
841841
mock_extract_options.return_value = {
842-
'temperature': {'value': 0.7, 'type': 'float', 'min': 0.0, 'max': 1.0},
843-
'max_new_tokens': {'value': 2048, 'type': 'int', 'min': 1, 'max': 8192}
842+
'temperature': {'default': 0.7, 'type': 'float', 'min': 0.0, 'max': 1.0},
843+
'max_new_tokens': {'default': 2048, 'type': 'int', 'min': 1, 'max': 8192}
844844
}
845845

846846
evaluator = CustomScorerEvaluator(
@@ -933,7 +933,7 @@ def test_custom_scorer_evaluator_get_custom_scorer_template_additions_builtin(
933933

934934
# Mock recipe utils
935935
mock_get_params.return_value = {'temperature': 0.7}
936-
mock_extract_options.return_value = {'temperature': {'value': 0.7}}
936+
mock_extract_options.return_value = {'temperature': {'default': 0.7}}
937937

938938
evaluator = CustomScorerEvaluator(
939939
evaluator=_BuiltInMetric.PRIME_MATH,
@@ -988,8 +988,8 @@ def test_custom_scorer_evaluator_get_custom_scorer_template_additions_custom_arn
988988
# Mock recipe utils
989989
mock_get_params.return_value = {'temperature': 0.5, 'aggregation': 'median'}
990990
mock_extract_options.return_value = {
991-
'temperature': {'value': 0.5},
992-
'aggregation': {'value': 'median'}
991+
'temperature': {'default': 0.5},
992+
'aggregation': {'default': 'median'}
993993
}
994994

995995
evaluator = CustomScorerEvaluator(
@@ -1048,7 +1048,7 @@ def test_custom_scorer_evaluator_lambda_type_for_nova_models(
10481048

10491049
# Mock recipe utils
10501050
mock_get_params.return_value = {'temperature': 0.7}
1051-
mock_extract_options.return_value = {'temperature': {'value': 0.7}}
1051+
mock_extract_options.return_value = {'temperature': {'default': 0.7}}
10521052

10531053
# Mock is_nova_model to return True
10541054
mock_is_nova.return_value = True
@@ -1105,7 +1105,7 @@ def test_custom_scorer_evaluator_no_lambda_type_for_non_nova_models(
11051105

11061106
# Mock recipe utils
11071107
mock_get_params.return_value = {'temperature': 0.7}
1108-
mock_extract_options.return_value = {'temperature': {'value': 0.7}}
1108+
mock_extract_options.return_value = {'temperature': {'default': 0.7}}
11091109

11101110
# Mock is_nova_model to return False
11111111
mock_is_nova.return_value = False
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from sagemaker.train.common import FineTuningOptions
2+
3+
4+
class TestFineTuningOptionsToDict:
5+
"""Tests for FineTuningOptions.to_dict() None value handling."""
6+
7+
def test_to_dict_skips_none_values(self):
8+
"""None-valued hyperparameters should be omitted from to_dict output."""
9+
options = FineTuningOptions({
10+
"learning_rate": {"default": 0.0002, "type": "float"},
11+
"resume_from_path": {"default": None, "type": "string"},
12+
"global_batch_size": {"default": 64, "type": "integer"},
13+
})
14+
result = options.to_dict()
15+
assert "resume_from_path" not in result
16+
assert result == {"learning_rate": "0.0002", "global_batch_size": "64"}
17+
18+
def test_to_dict_includes_non_none_values(self):
19+
"""Non-None values should be included as strings."""
20+
options = FineTuningOptions({
21+
"learning_rate": {"default": 0.001, "type": "float"},
22+
"max_epochs": {"default": 3, "type": "integer"},
23+
"model_name": {"default": "my-model", "type": "string"},
24+
})
25+
result = options.to_dict()
26+
assert result == {
27+
"learning_rate": "0.001",
28+
"max_epochs": "3",
29+
"model_name": "my-model",
30+
}
31+
32+
def test_to_dict_empty_string_is_included(self):
33+
"""Empty string is a valid value and should not be skipped."""
34+
options = FineTuningOptions({
35+
"mlflow_run_id": {"default": "", "type": "string"},
36+
})
37+
result = options.to_dict()
38+
assert result == {"mlflow_run_id": ""}
39+
40+
def test_to_dict_after_user_sets_none_to_value(self):
41+
"""If user overrides a None default with a real value, it should appear."""
42+
options = FineTuningOptions({
43+
"resume_from_path": {"default": None, "type": "string"},
44+
})
45+
options.resume_from_path = "/path/to/checkpoint"
46+
result = options.to_dict()
47+
assert result == {"resume_from_path": "/path/to/checkpoint"}
48+
49+
def test_to_dict_all_none_returns_empty(self):
50+
"""If all values are None, to_dict should return empty dict."""
51+
options = FineTuningOptions({
52+
"param_a": {"default": None, "type": "string"},
53+
"param_b": {"default": None, "type": "string"},
54+
})
55+
result = options.to_dict()
56+
assert result == {}

0 commit comments

Comments
 (0)