From 52c765f9e057a9338133e88d80bd0804d0638d65 Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Mon, 14 Sep 2026 15:22:02 -0700 Subject: [PATCH 1/6] test commit --- sagemaker-mlops/src/sagemaker/mlops/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sagemaker-mlops/src/sagemaker/mlops/__init__.py b/sagemaker-mlops/src/sagemaker/mlops/__init__.py index 18527db318..e4864f2539 100644 --- a/sagemaker-mlops/src/sagemaker/mlops/__init__.py +++ b/sagemaker-mlops/src/sagemaker/mlops/__init__.py @@ -15,7 +15,7 @@ Example usage: from sagemaker.mlops import ModelBuilder from sagemaker.mlops.workflow import Pipeline, TrainingStep -""" +""" from __future__ import absolute_import __version__ = "0.1.0" From 39c127a7e53f71727fc22aaaddbf1313b1e00db7 Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Mon, 14 Sep 2026 15:40:59 -0700 Subject: [PATCH 2/6] test PR checks --- sagemaker-core/src/sagemaker/__init__.py | 2 +- sagemaker-serve/src/sagemaker/__init__.py | 2 +- sagemaker-train/src/sagemaker/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sagemaker-core/src/sagemaker/__init__.py b/sagemaker-core/src/sagemaker/__init__.py index 34badfccd5..666416bd00 100644 --- a/sagemaker-core/src/sagemaker/__init__.py +++ b/sagemaker-core/src/sagemaker/__init__.py @@ -1,4 +1,4 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sagemaker-serve/src/sagemaker/__init__.py b/sagemaker-serve/src/sagemaker/__init__.py index 71038bb89b..38a9c08eb1 100644 --- a/sagemaker-serve/src/sagemaker/__init__.py +++ b/sagemaker-serve/src/sagemaker/__init__.py @@ -1,2 +1,2 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/sagemaker-train/src/sagemaker/__init__.py b/sagemaker-train/src/sagemaker/__init__.py index 71038bb89b..38a9c08eb1 100644 --- a/sagemaker-train/src/sagemaker/__init__.py +++ b/sagemaker-train/src/sagemaker/__init__.py @@ -1,2 +1,2 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__('pkgutil').extend_path(__path__, __name__) From 57edfbd67818cfc7afceac82bd0ce2070be7b2ef Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Tue, 15 Sep 2026 14:04:52 -0700 Subject: [PATCH 3/6] Fix flaky unit tests --- ...st_compute_requirements_deploy_fallback.py | 178 ++++++++++++++++++ .../unit/test_model_builder_coverage_boost.py | 22 +++ .../tests/unit/ai_registry/test_dataset.py | 19 +- .../ai_registry/test_dataset_domain_id.py | 18 +- .../tests/unit/ai_registry/test_evaluator.py | 33 +++- .../ai_registry/test_evaluator_domain_id.py | 36 +++- 6 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py diff --git a/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py b/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py new file mode 100644 index 0000000000..c079dcdce6 --- /dev/null +++ b/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py @@ -0,0 +1,178 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Compute requirements fallback when build() never cached them. + +``build(reuse_resources=True)`` returns as soon as it finds a reusable Model, so the +recipe hosting config — and with it ``_cached_compute_requirements`` — is never applied. +``_deploy_model_customization`` must resolve the requirements on demand instead of +raising ``AttributeError``. +""" +from __future__ import absolute_import + +from unittest.mock import Mock, patch + +from sagemaker.core.shapes import InferenceComponentComputeResourceRequirements +from sagemaker.serve.model_builder import ModelBuilder + + +RESOLVED = InferenceComponentComputeResourceRequirements( + min_memory_required_in_mb=24576, number_of_accelerator_devices_required=4 +) + + +def _model_package(): + package = Mock() + package.model_package_arn = ( + "arn:aws:sagemaker:us-west-2:123456789012:model-package/test-package" + ) + container = Mock() + container.base_model = Mock() + container.base_model.recipe_name = "test-base-model" + container.model_data_source = Mock() + container.model_data_source.s3_data_source = Mock() + container.model_data_source.s3_data_source.s3_uri = "s3://bucket/fine-tuned-model/" + package.inference_specification = Mock() + package.inference_specification.containers = [container] + return package + + +class TestComputeRequirementsDeployFallback: + """Deploy resolves compute requirements when the build-time cache is unset.""" + + def test_cached_compute_requirements_defaults_to_none(self): + # Declared as a dataclass field, so reading it never raises AttributeError. + assert ModelBuilder()._cached_compute_requirements is None + + def test_get_compute_requirements_prefers_cache(self): + builder = ModelBuilder(instance_type="ml.g5.12xlarge") + builder._cached_compute_requirements = RESOLVED + + with patch.object(ModelBuilder, "_resolve_compute_requirements") as mock_resolve: + assert builder._get_compute_requirements_for_deploy() is RESOLVED + + mock_resolve.assert_not_called() + + @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) + def test_get_compute_requirements_resolves_and_caches(self, mock_resolve): + builder = ModelBuilder(instance_type="ml.g5.12xlarge") + + assert builder._get_compute_requirements_for_deploy() is RESOLVED + # Second call is served from the cache — resolution happens at most once. + assert builder._get_compute_requirements_for_deploy() is RESOLVED + + mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") + assert builder._cached_compute_requirements is RESOLVED + + @patch("sagemaker.core.resources.InferenceComponent.get") + @patch("sagemaker.core.resources.InferenceComponent.create") + @patch("sagemaker.core.resources.Endpoint.create") + @patch("sagemaker.core.resources.EndpointConfig.create") + @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) + @patch.object(ModelBuilder, "_fetch_model_package_arn") + @patch.object(ModelBuilder, "_fetch_model_package") + @patch.object(ModelBuilder, "_fetch_peft", return_value=None) + @patch.object(ModelBuilder, "_does_endpoint_exist", return_value=False) + @patch.object(ModelBuilder, "_is_nova_model", return_value=False) + def test_deploy_without_cache_resolves_requirements( + self, + mock_is_nova, + mock_endpoint_exists, + mock_fetch_peft, + mock_fetch_package, + mock_fetch_package_arn, + mock_resolve, + mock_endpoint_config_create, + mock_endpoint_create, + mock_ic_create, + mock_ic_get, + ): + package = _model_package() + mock_fetch_package.return_value = package + mock_fetch_package_arn.return_value = package.model_package_arn + mock_endpoint_create.return_value = Mock(wait_for_status=Mock()) + mock_ic_get.return_value = Mock( + inference_component_arn=( + "arn:aws:sagemaker:us-west-2:123456789012:inference-component/test-ic" + ) + ) + + builder = ModelBuilder( + model=package, + role_arn="arn:aws:iam::123456789012:role/SageMakerRole", + instance_type="ml.g5.12xlarge", + ) + builder.built_model = Mock(model_name="test-model") + # No build() call, so nothing was cached — mirrors the reuse_resources path. + assert builder._cached_compute_requirements is None + + with patch("sagemaker.core.resources.Action"), patch( + "sagemaker.core.resources.Association" + ), patch("sagemaker.core.resources.Artifact"): + builder._deploy_model_customization(endpoint_name="test-endpoint") + + mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") + spec = mock_ic_create.call_args[1]["specification"] + assert spec.compute_resource_requirements is RESOLVED + + @patch("sagemaker.core.resources.InferenceComponent.get_all", return_value=[]) + @patch("sagemaker.core.resources.InferenceComponent.get") + @patch("sagemaker.core.resources.InferenceComponent.create") + @patch("sagemaker.core.resources.Endpoint.create") + @patch("sagemaker.core.resources.EndpointConfig.create") + @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) + @patch.object(ModelBuilder, "_fetch_model_package_arn") + @patch.object(ModelBuilder, "_fetch_model_package") + @patch.object(ModelBuilder, "_fetch_peft", return_value="LORA") + @patch.object(ModelBuilder, "_does_endpoint_exist", return_value=False) + @patch.object(ModelBuilder, "_is_nova_model", return_value=False) + def test_lora_base_component_without_cache_resolves_requirements( + self, + mock_is_nova, + mock_endpoint_exists, + mock_fetch_peft, + mock_fetch_package, + mock_fetch_package_arn, + mock_resolve, + mock_endpoint_config_create, + mock_endpoint_create, + mock_ic_create, + mock_ic_get, + mock_ic_get_all, + ): + package = _model_package() + mock_fetch_package.return_value = package + mock_fetch_package_arn.return_value = package.model_package_arn + mock_endpoint_create.return_value = Mock(wait_for_status=Mock()) + mock_ic_get.return_value = Mock( + wait_for_status=Mock(), + inference_component_arn=( + "arn:aws:sagemaker:us-west-2:123456789012:inference-component/test-ic" + ), + ) + + builder = ModelBuilder( + model=package, + role_arn="arn:aws:iam::123456789012:role/SageMakerRole", + instance_type="ml.g5.12xlarge", + ) + builder.built_model = Mock(model_name="test-model") + builder._adapter_s3_uri = "s3://bucket/adapter/" + + with patch("sagemaker.core.resources.Action"), patch( + "sagemaker.core.resources.Association" + ), patch("sagemaker.core.resources.Artifact"): + builder._deploy_model_customization(endpoint_name="test-endpoint") + + mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") + base_spec = mock_ic_create.call_args_list[0][1]["specification"] + assert base_spec.compute_resource_requirements is RESOLVED diff --git a/sagemaker-serve/tests/unit/test_model_builder_coverage_boost.py b/sagemaker-serve/tests/unit/test_model_builder_coverage_boost.py index d6c9f35dbf..ecfa241b73 100644 --- a/sagemaker-serve/tests/unit/test_model_builder_coverage_boost.py +++ b/sagemaker-serve/tests/unit/test_model_builder_coverage_boost.py @@ -8,6 +8,8 @@ from dataclasses import dataclass import tempfile +import pytest + from sagemaker.serve.model_builder import ModelBuilder from sagemaker.serve.mode.function_pointers import Mode from sagemaker.serve.utils.types import ModelServer @@ -16,6 +18,26 @@ from sagemaker.core.inference_config import AsyncInferenceConfig from botocore.exceptions import ClientError +TEST_ROLE_ARN = "arn:aws:iam::123456789012:role/SageMakerRole" + + +@pytest.fixture(autouse=True) +def stub_role_resolution(): + """Keep ModelBuilder construction offline. + + ``ModelBuilder.__post_init__`` auto-resolves a serving role when no + ``role_arn`` is given, which calls sts:GetCallerIdentity and the paginated + iam:SimulatePrincipalPolicy. Tests here construct ``ModelBuilder`` without a + role, so unpatched they issue live IAM calls and fail on throttling + (SimulatePrincipalPolicy "Rate exceeded") rather than on the behavior under + test. Tests that patch the resolver themselves still override this. + """ + with patch( + "sagemaker.serve.model_builder.resolve_and_validate_role", + side_effect=lambda provided_role=None, **kwargs: provided_role or TEST_ROLE_ARN, + ): + yield + class TestModelBuilderInit(unittest.TestCase): """Test ModelBuilder initialization.""" diff --git a/sagemaker-train/tests/unit/ai_registry/test_dataset.py b/sagemaker-train/tests/unit/ai_registry/test_dataset.py index 28641567a6..b03e5f31d8 100644 --- a/sagemaker-train/tests/unit/ai_registry/test_dataset.py +++ b/sagemaker-train/tests/unit/ai_registry/test_dataset.py @@ -296,10 +296,27 @@ def mock_exists(path): assert document['DatasetS3Prefix'] == 'path/to/dataset.jsonl' assert document['DatasetS3Bucket'] == 'test-bucket' + @patch('sagemaker.ai_registry.air_hub_entity.AIRHub.get_hub_name', return_value="test-hub") + @patch('sagemaker.ai_registry.dataset._get_default_bucket', return_value="test-bucket") + @patch('sagemaker.train.defaults.TrainDefaults.get_role', return_value="arn:aws:iam::123456789012:role/SageMakerRole") + @patch('sagemaker.train.defaults.TrainDefaults.get_sagemaker_session') + @patch('sagemaker.ai_registry.dataset._get_current_domain_id', return_value=None) + @patch('sagemaker.ai_registry.dataset.Session') @patch('sagemaker.ai_registry.dataset.DataSet._validate_dataset_file') @patch('sagemaker.ai_registry.dataset.DataSet._validate_dataset_format') @patch('sagemaker.ai_registry.dataset.AIRHub') - def test_create_with_local_file(self, mock_air_hub, mock_validate_format, mock_validate_file): + def test_create_with_local_file( + self, + mock_air_hub, + mock_validate_format, + mock_validate_file, + mock_session, + mock_get_domain_id, + mock_get_session, + mock_get_role, + mock_default_bucket, + mock_get_hub_name, + ): mock_air_hub.upload_to_s3.return_value = "s3://bucket/path" mock_air_hub.import_hub_content.return_value = {"HubContentArn": "test-arn"} mock_air_hub.describe_hub_content.return_value = { diff --git a/sagemaker-train/tests/unit/ai_registry/test_dataset_domain_id.py b/sagemaker-train/tests/unit/ai_registry/test_dataset_domain_id.py index da2d8ceca7..c567acf0aa 100644 --- a/sagemaker-train/tests/unit/ai_registry/test_dataset_domain_id.py +++ b/sagemaker-train/tests/unit/ai_registry/test_dataset_domain_id.py @@ -46,7 +46,23 @@ def sample_dataset_file(): class TestDataSetDomainId: """Test domain-id is added to SearchKeywords when available.""" - + + @pytest.fixture(autouse=True) + def stub_account_lookups(self): + """Keep these unit tests offline. + + Uploading a local dataset derives the default bucket, and constructing the entity + derives the hub name — both call STS ``GetCallerIdentity``. ``AIRHub`` is patched on + the base-entity module because ``AIRHubEntity.__init__`` resolves the hub name + through its own import, not the one patched on ``dataset``. + """ + with patch( + 'sagemaker.ai_registry.dataset._get_default_bucket', return_value='test-bucket' + ), patch( + 'sagemaker.ai_registry.air_hub_entity.AIRHub.get_hub_name', return_value='test-hub' + ): + yield + @patch('sagemaker.core.helper.session_helper.Session') @patch('sagemaker.ai_registry.dataset._get_current_domain_id') @patch('sagemaker.ai_registry.dataset.AIRHub') diff --git a/sagemaker-train/tests/unit/ai_registry/test_evaluator.py b/sagemaker-train/tests/unit/ai_registry/test_evaluator.py index eeeedfce58..170528d32c 100644 --- a/sagemaker-train/tests/unit/ai_registry/test_evaluator.py +++ b/sagemaker-train/tests/unit/ai_registry/test_evaluator.py @@ -23,6 +23,9 @@ ) +DUMMY_ROLE = "arn:aws:iam::123456789012:role/SageMakerRole" + + def _keywords_from_import_call(mock_air_hub): """Extract the SearchKeyword strings passed to import_hub_content.""" tags = mock_air_hub.import_hub_content.call_args.kwargs["tags"] @@ -30,6 +33,33 @@ def _keywords_from_import_call(mock_air_hub): class TestEvaluator: + @pytest.fixture(autouse=True) + def stub_aws_resolution(self): + """Keep these unit tests offline. + + ``Evaluator.create`` builds a default ``Session``, auto-detects the Studio domain + ID (STS ``GetCallerIdentity``) and resolves/validates an execution role (IAM + ``SimulatePrincipalPolicy``); constructing the entity also derives the hub name + from the caller's account. Unmocked, those reach real AWS and make the suite slow + and flaky — CI hit ``Throttling: Rate exceeded`` on ``SimulatePrincipalPolicy``. + + ``AIRHub`` is patched on the base-entity module as well as on ``evaluator``, since + ``AIRHubEntity.__init__`` resolves the hub name through its own import. + """ + session = MagicMock() + with patch("sagemaker.ai_registry.evaluator.Session", return_value=session), patch( + "sagemaker.ai_registry.evaluator._get_current_domain_id", return_value=None + ), patch( + "sagemaker.train.defaults.TrainDefaults.get_sagemaker_session", + return_value=session, + ), patch( + "sagemaker.train.defaults.TrainDefaults.get_role", return_value=DUMMY_ROLE + ), patch( + "sagemaker.ai_registry.air_hub_entity.AIRHub.get_hub_name", + return_value="test-hub", + ): + yield + @patch('sagemaker.ai_registry.evaluator.AIRHub') def test_create_with_lambda_arn(self, mock_air_hub): mock_air_hub.import_hub_content.return_value = {"HubContentArn": "test-arn"} @@ -52,9 +82,10 @@ def test_create_with_lambda_arn(self, mock_air_hub): assert evaluator.method == EvaluatorMethod.LAMBDA mock_air_hub.import_hub_content.assert_called_once() + @patch('sagemaker.ai_registry.evaluator._get_default_bucket', return_value="test-bucket") @patch('sagemaker.ai_registry.evaluator.boto3') @patch('sagemaker.ai_registry.evaluator.AIRHub') - def test_create_with_byoc(self, mock_air_hub, mock_boto3): + def test_create_with_byoc(self, mock_air_hub, mock_boto3, mock_default_bucket): mock_lambda_client = MagicMock() mock_boto3.client.return_value = mock_lambda_client mock_lambda_client.create_function.return_value = {"FunctionArn": "lambda-arn"} diff --git a/sagemaker-train/tests/unit/ai_registry/test_evaluator_domain_id.py b/sagemaker-train/tests/unit/ai_registry/test_evaluator_domain_id.py index f6cdaf44af..6e96f1fc45 100644 --- a/sagemaker-train/tests/unit/ai_registry/test_evaluator_domain_id.py +++ b/sagemaker-train/tests/unit/ai_registry/test_evaluator_domain_id.py @@ -18,18 +18,38 @@ class TestEvaluatorDomainId: """Test domain-id is added to SearchKeywords when available.""" - - @patch('sagemaker.core.helper.session_helper.Session') + + @pytest.fixture(autouse=True) + def stub_aws_resolution(self): + """Keep these unit tests offline. + + ``Evaluator.create`` builds a default ``Session``, resolves/validates an execution + role via IAM ``SimulatePrincipalPolicy``, and derives the hub name from the + caller's account. Unmocked, those reach real AWS and made CI fail with + ``Throttling: Rate exceeded``. + """ + session = Mock() + with patch("sagemaker.ai_registry.evaluator.Session", return_value=session), patch( + "sagemaker.train.defaults.TrainDefaults.get_sagemaker_session", + return_value=session, + ), patch( + "sagemaker.train.defaults.TrainDefaults.get_role", + return_value="arn:aws:iam::123456789012:role/test-role", + ), patch( + "sagemaker.ai_registry.air_hub_entity.AIRHub.get_hub_name", + return_value="test-hub", + ): + yield + @patch('sagemaker.ai_registry.evaluator._get_current_domain_id') @patch('sagemaker.ai_registry.evaluator.AIRHub') def test_domain_id_added_when_available( - self, mock_air_hub, mock_get_domain_id, mock_session + self, mock_air_hub, mock_get_domain_id ): """Test that domain-id is added to tags when available.""" # Setup mocks mock_domain_id = "d-test123456" mock_get_domain_id.return_value = mock_domain_id - mock_session.return_value = Mock() # Mock AIRHub methods mock_air_hub.import_hub_content = Mock() @@ -62,16 +82,14 @@ def test_domain_id_added_when_available( # Verify domain-id is in tags assert any(tag[0] == '@domain' and tag[1] == mock_domain_id for tag in tags) - @patch('sagemaker.core.helper.session_helper.Session') @patch('sagemaker.ai_registry.evaluator._get_current_domain_id') @patch('sagemaker.ai_registry.evaluator.AIRHub') def test_domain_id_not_added_when_unavailable( - self, mock_air_hub, mock_get_domain_id, mock_session + self, mock_air_hub, mock_get_domain_id ): """Test that domain-id is not added when unavailable (non-Studio).""" # Setup mocks - domain_id returns None mock_get_domain_id.return_value = None - mock_session.return_value = Mock() # Mock AIRHub methods mock_air_hub.import_hub_content = Mock() @@ -104,18 +122,16 @@ def test_domain_id_not_added_when_unavailable( # Verify domain-id is NOT in tags assert not any(tag[0] == '@domain' for tag in tags) - @patch('sagemaker.core.helper.session_helper.Session') @patch('sagemaker.ai_registry.evaluator._get_current_domain_id') @patch('sagemaker.ai_registry.evaluator.AIRHub') def test_explicit_domain_id_used_without_auto_detection( - self, mock_air_hub, mock_get_domain_id, mock_session + self, mock_air_hub, mock_get_domain_id ): """An explicit domain_id is tagged and auto-detection is not invoked. Covers the non-Studio case (P467494019) where the domain cannot be inferred and must be supplied by the caller. """ - mock_session.return_value = Mock() mock_air_hub.import_hub_content = Mock() mock_air_hub.describe_hub_content = Mock(return_value={ 'HubContentName': 'test-evaluator', From 8ce66864f0c334fb69de7e60104c421ebcece518 Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Tue, 15 Sep 2026 14:05:49 -0700 Subject: [PATCH 4/6] Revert "test PR checks" This reverts commit 39c127a7e53f71727fc22aaaddbf1313b1e00db7. --- sagemaker-core/src/sagemaker/__init__.py | 2 +- sagemaker-serve/src/sagemaker/__init__.py | 2 +- sagemaker-train/src/sagemaker/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sagemaker-core/src/sagemaker/__init__.py b/sagemaker-core/src/sagemaker/__init__.py index 666416bd00..34badfccd5 100644 --- a/sagemaker-core/src/sagemaker/__init__.py +++ b/sagemaker-core/src/sagemaker/__init__.py @@ -1,4 +1,4 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sagemaker-serve/src/sagemaker/__init__.py b/sagemaker-serve/src/sagemaker/__init__.py index 38a9c08eb1..71038bb89b 100644 --- a/sagemaker-serve/src/sagemaker/__init__.py +++ b/sagemaker-serve/src/sagemaker/__init__.py @@ -1,2 +1,2 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/sagemaker-train/src/sagemaker/__init__.py b/sagemaker-train/src/sagemaker/__init__.py index 38a9c08eb1..71038bb89b 100644 --- a/sagemaker-train/src/sagemaker/__init__.py +++ b/sagemaker-train/src/sagemaker/__init__.py @@ -1,2 +1,2 @@ -"""Namespace package for SageMaker.""" +"""Namespace package for SageMaker.""" __path__ = __import__('pkgutil').extend_path(__path__, __name__) From e9b7a48ed587835c395ac28e3df56e325df717e1 Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Tue, 15 Sep 2026 14:05:54 -0700 Subject: [PATCH 5/6] Revert "test commit" This reverts commit 52c765f9e057a9338133e88d80bd0804d0638d65. --- sagemaker-mlops/src/sagemaker/mlops/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sagemaker-mlops/src/sagemaker/mlops/__init__.py b/sagemaker-mlops/src/sagemaker/mlops/__init__.py index e4864f2539..18527db318 100644 --- a/sagemaker-mlops/src/sagemaker/mlops/__init__.py +++ b/sagemaker-mlops/src/sagemaker/mlops/__init__.py @@ -15,7 +15,7 @@ Example usage: from sagemaker.mlops import ModelBuilder from sagemaker.mlops.workflow import Pipeline, TrainingStep -""" +""" from __future__ import absolute_import __version__ = "0.1.0" From 1572a52d6b7de7eeadbe7dd6e0414085912193db Mon Sep 17 00:00:00 2001 From: jzhaoqwa Date: Tue, 15 Sep 2026 16:56:53 -0700 Subject: [PATCH 6/6] Update unit tests --- ...st_compute_requirements_deploy_fallback.py | 178 ------------------ 1 file changed, 178 deletions(-) delete mode 100644 sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py diff --git a/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py b/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py deleted file mode 100644 index c079dcdce6..0000000000 --- a/sagemaker-serve/tests/unit/test_compute_requirements_deploy_fallback.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Compute requirements fallback when build() never cached them. - -``build(reuse_resources=True)`` returns as soon as it finds a reusable Model, so the -recipe hosting config — and with it ``_cached_compute_requirements`` — is never applied. -``_deploy_model_customization`` must resolve the requirements on demand instead of -raising ``AttributeError``. -""" -from __future__ import absolute_import - -from unittest.mock import Mock, patch - -from sagemaker.core.shapes import InferenceComponentComputeResourceRequirements -from sagemaker.serve.model_builder import ModelBuilder - - -RESOLVED = InferenceComponentComputeResourceRequirements( - min_memory_required_in_mb=24576, number_of_accelerator_devices_required=4 -) - - -def _model_package(): - package = Mock() - package.model_package_arn = ( - "arn:aws:sagemaker:us-west-2:123456789012:model-package/test-package" - ) - container = Mock() - container.base_model = Mock() - container.base_model.recipe_name = "test-base-model" - container.model_data_source = Mock() - container.model_data_source.s3_data_source = Mock() - container.model_data_source.s3_data_source.s3_uri = "s3://bucket/fine-tuned-model/" - package.inference_specification = Mock() - package.inference_specification.containers = [container] - return package - - -class TestComputeRequirementsDeployFallback: - """Deploy resolves compute requirements when the build-time cache is unset.""" - - def test_cached_compute_requirements_defaults_to_none(self): - # Declared as a dataclass field, so reading it never raises AttributeError. - assert ModelBuilder()._cached_compute_requirements is None - - def test_get_compute_requirements_prefers_cache(self): - builder = ModelBuilder(instance_type="ml.g5.12xlarge") - builder._cached_compute_requirements = RESOLVED - - with patch.object(ModelBuilder, "_resolve_compute_requirements") as mock_resolve: - assert builder._get_compute_requirements_for_deploy() is RESOLVED - - mock_resolve.assert_not_called() - - @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) - def test_get_compute_requirements_resolves_and_caches(self, mock_resolve): - builder = ModelBuilder(instance_type="ml.g5.12xlarge") - - assert builder._get_compute_requirements_for_deploy() is RESOLVED - # Second call is served from the cache — resolution happens at most once. - assert builder._get_compute_requirements_for_deploy() is RESOLVED - - mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") - assert builder._cached_compute_requirements is RESOLVED - - @patch("sagemaker.core.resources.InferenceComponent.get") - @patch("sagemaker.core.resources.InferenceComponent.create") - @patch("sagemaker.core.resources.Endpoint.create") - @patch("sagemaker.core.resources.EndpointConfig.create") - @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) - @patch.object(ModelBuilder, "_fetch_model_package_arn") - @patch.object(ModelBuilder, "_fetch_model_package") - @patch.object(ModelBuilder, "_fetch_peft", return_value=None) - @patch.object(ModelBuilder, "_does_endpoint_exist", return_value=False) - @patch.object(ModelBuilder, "_is_nova_model", return_value=False) - def test_deploy_without_cache_resolves_requirements( - self, - mock_is_nova, - mock_endpoint_exists, - mock_fetch_peft, - mock_fetch_package, - mock_fetch_package_arn, - mock_resolve, - mock_endpoint_config_create, - mock_endpoint_create, - mock_ic_create, - mock_ic_get, - ): - package = _model_package() - mock_fetch_package.return_value = package - mock_fetch_package_arn.return_value = package.model_package_arn - mock_endpoint_create.return_value = Mock(wait_for_status=Mock()) - mock_ic_get.return_value = Mock( - inference_component_arn=( - "arn:aws:sagemaker:us-west-2:123456789012:inference-component/test-ic" - ) - ) - - builder = ModelBuilder( - model=package, - role_arn="arn:aws:iam::123456789012:role/SageMakerRole", - instance_type="ml.g5.12xlarge", - ) - builder.built_model = Mock(model_name="test-model") - # No build() call, so nothing was cached — mirrors the reuse_resources path. - assert builder._cached_compute_requirements is None - - with patch("sagemaker.core.resources.Action"), patch( - "sagemaker.core.resources.Association" - ), patch("sagemaker.core.resources.Artifact"): - builder._deploy_model_customization(endpoint_name="test-endpoint") - - mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") - spec = mock_ic_create.call_args[1]["specification"] - assert spec.compute_resource_requirements is RESOLVED - - @patch("sagemaker.core.resources.InferenceComponent.get_all", return_value=[]) - @patch("sagemaker.core.resources.InferenceComponent.get") - @patch("sagemaker.core.resources.InferenceComponent.create") - @patch("sagemaker.core.resources.Endpoint.create") - @patch("sagemaker.core.resources.EndpointConfig.create") - @patch.object(ModelBuilder, "_resolve_compute_requirements", return_value=RESOLVED) - @patch.object(ModelBuilder, "_fetch_model_package_arn") - @patch.object(ModelBuilder, "_fetch_model_package") - @patch.object(ModelBuilder, "_fetch_peft", return_value="LORA") - @patch.object(ModelBuilder, "_does_endpoint_exist", return_value=False) - @patch.object(ModelBuilder, "_is_nova_model", return_value=False) - def test_lora_base_component_without_cache_resolves_requirements( - self, - mock_is_nova, - mock_endpoint_exists, - mock_fetch_peft, - mock_fetch_package, - mock_fetch_package_arn, - mock_resolve, - mock_endpoint_config_create, - mock_endpoint_create, - mock_ic_create, - mock_ic_get, - mock_ic_get_all, - ): - package = _model_package() - mock_fetch_package.return_value = package - mock_fetch_package_arn.return_value = package.model_package_arn - mock_endpoint_create.return_value = Mock(wait_for_status=Mock()) - mock_ic_get.return_value = Mock( - wait_for_status=Mock(), - inference_component_arn=( - "arn:aws:sagemaker:us-west-2:123456789012:inference-component/test-ic" - ), - ) - - builder = ModelBuilder( - model=package, - role_arn="arn:aws:iam::123456789012:role/SageMakerRole", - instance_type="ml.g5.12xlarge", - ) - builder.built_model = Mock(model_name="test-model") - builder._adapter_s3_uri = "s3://bucket/adapter/" - - with patch("sagemaker.core.resources.Action"), patch( - "sagemaker.core.resources.Association" - ), patch("sagemaker.core.resources.Artifact"): - builder._deploy_model_customization(endpoint_name="test-endpoint") - - mock_resolve.assert_called_once_with(instance_type="ml.g5.12xlarge") - base_spec = mock_ic_create.call_args_list[0][1]["specification"] - assert base_spec.compute_resource_requirements is RESOLVED