Skip to content

Commit c85e4c4

Browse files
committed
fix: ModelBuilder resolves private hub artifacts correctly
Fix two defects causing ModelBuilder to ignore private hub when resolving model artifacts, forcing the execution role to access the public JumpStart S3 cache bucket. Defect 1: from_jumpstart_config sets hub_name AFTER __init__ has already called _initialize_jumpstart_config(), which takes the else branch and sets hub_arn = None. Fix: call _initialize_jumpstart_config() again after setting hub_name so hub_arn is correctly derived. Defect 2: _build_for_jumpstart does not forward hub_arn to get_init_kwargs, causing model data to resolve from the public catalog. Fix: pass hub_arn=getattr(self, 'hub_arn', None) to all get_init_kwargs call sites in the build path (_build_for_jumpstart, _detect_jumpstart_image). Impact: Customers deploying from private hubs via ModelBuilder no longer need to grant their execution role s3:GetObject on the public JumpStart cache bucket. Testing: - 8 unit tests covering both defects and end-to-end flow - Integration test for private hub deployment (requires env config)
1 parent 78bb11e commit c85e4c4

5 files changed

Lines changed: 590 additions & 1 deletion

File tree

‎sagemaker-serve/src/sagemaker/serve/model_builder.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191
create_model_package_from_containers,
9292
)
9393
from sagemaker.core.jumpstart.enums import JumpStartModelType
94-
9594
from sagemaker.core.jumpstart.configs import JumpStartConfig
9695
from sagemaker.core.jumpstart.utils import add_jumpstart_uri_tags
9796
from sagemaker.core.jumpstart.artifacts.kwargs import _retrieve_model_deploy_kwargs
@@ -3895,6 +3894,16 @@ def from_jumpstart_config(
38953894
mb_instance.resource_requirements = resource_requirements
38963895
mb_instance.model_kms_key = model_kms_key
38973896
mb_instance.hub_name = jumpstart_config.hub_name
3897+
if mb_instance.hub_name and not getattr(mb_instance, "hub_arn", None):
3898+
from sagemaker.core.jumpstart.hub.utils import (
3899+
generate_hub_arn_for_init_kwargs,
3900+
)
3901+
3902+
mb_instance.hub_arn = generate_hub_arn_for_init_kwargs(
3903+
hub_name=mb_instance.hub_name,
3904+
region=mb_instance.region,
3905+
session=mb_instance.sagemaker_session,
3906+
)
38983907
mb_instance.config_name = jumpstart_config.inference_config_name
38993908
mb_instance.accept_eula = jumpstart_config.accept_eula
39003909
mb_instance.tolerate_vulnerable_model = tolerate_vulnerable_model

‎sagemaker-serve/src/sagemaker/serve/model_builder_servers.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ def _build_for_jumpstart(self) -> Model:
980980
model_version=self.model_version or "*",
981981
region=self.region,
982982
instance_type=self.instance_type,
983+
hub_arn=getattr(self, "hub_arn", None),
983984
tolerate_vulnerable_model=getattr(self, "tolerate_vulnerable_model", None),
984985
tolerate_deprecated_model=getattr(self, "tolerate_deprecated_model", None),
985986
config_name=getattr(self, "config_name", None),

‎sagemaker-serve/src/sagemaker/serve/model_builder_utils.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ def _detect_jumpstart_image(self) -> None:
634634
model_version=getattr(self, "model_version", None) or "*",
635635
region=self.region,
636636
instance_type=getattr(self, "instance_type", None),
637+
hub_arn=getattr(self, "hub_arn", None),
637638
tolerate_vulnerable_model=getattr(self, "tolerate_vulnerable_model", None),
638639
tolerate_deprecated_model=getattr(self, "tolerate_deprecated_model", None),
639640
)
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Integration test for private hub artifact resolution.
14+
15+
Verifies that ModelBuilder.from_jumpstart_config with hub_name resolves
16+
model artifacts through the private hub content reference, rather than
17+
falling back to the public JumpStart S3 cache.
18+
19+
This test creates its own private hub, adds a ModelReference, runs the
20+
build flow, and tears everything down afterward.
21+
"""
22+
from __future__ import absolute_import
23+
24+
import uuid
25+
import time
26+
import logging
27+
28+
import boto3
29+
import pytest
30+
31+
from sagemaker.serve.model_builder import ModelBuilder
32+
from sagemaker.core.jumpstart.configs import JumpStartConfig
33+
from sagemaker.train.configs import Compute
34+
35+
logger = logging.getLogger(__name__)
36+
37+
# Use a small/fast JumpStart model for the test
38+
TEST_MODEL_ID = "huggingface-llm-gemma-2b"
39+
TEST_MODEL_VERSION = "*"
40+
TEST_INSTANCE_TYPE = "ml.g5.xlarge"
41+
TEST_REGION = "us-east-1"
42+
43+
HUB_NAME_PREFIX = "sdk-integ-test-private-hub"
44+
45+
46+
@pytest.fixture(scope="module")
47+
def private_hub():
48+
"""Create a private hub with a ModelReference, tear down after tests."""
49+
sm = boto3.client("sagemaker", region_name=TEST_REGION)
50+
hub_name = f"{HUB_NAME_PREFIX}-{uuid.uuid4().hex[:8]}"
51+
52+
# --- Setup ---
53+
logger.info("Creating private hub: %s", hub_name)
54+
sm.create_hub(
55+
HubName=hub_name,
56+
HubDescription="SDK integration test for private hub artifact resolution",
57+
Tags=[
58+
{"Key": "Purpose", "Value": "sdk-integ-test"},
59+
{"Key": "AutoCleanup", "Value": "true"},
60+
],
61+
)
62+
63+
# Wait for hub to be ready
64+
for _ in range(30):
65+
resp = sm.describe_hub(HubName=hub_name)
66+
if resp["HubStatus"] == "InService":
67+
break
68+
time.sleep(2)
69+
else:
70+
pytest.fail(f"Hub {hub_name} did not reach InService state")
71+
72+
# Add a ModelReference to the public JumpStart content
73+
public_arn = (
74+
f"arn:aws:sagemaker:{TEST_REGION}:aws:hub-content/"
75+
f"SageMakerPublicHub/Model/{TEST_MODEL_ID}"
76+
)
77+
logger.info("Creating hub content reference to: %s", public_arn)
78+
sm.create_hub_content_reference(
79+
HubName=hub_name,
80+
SageMakerPublicHubContentArn=public_arn,
81+
)
82+
83+
# Wait for content reference to be available
84+
for _ in range(60):
85+
try:
86+
contents = sm.list_hub_contents(
87+
HubName=hub_name,
88+
HubContentType="ModelReference",
89+
)
90+
summaries = contents.get("HubContentSummaries", [])
91+
if any(
92+
s["HubContentName"] == TEST_MODEL_ID
93+
and s.get("HubContentStatus") == "Available"
94+
for s in summaries
95+
):
96+
break
97+
except Exception:
98+
pass
99+
time.sleep(3)
100+
else:
101+
pytest.fail(
102+
f"ModelReference for {TEST_MODEL_ID} not available in "
103+
f"hub {hub_name} after 3 minutes"
104+
)
105+
106+
yield hub_name
107+
108+
# --- Teardown ---
109+
logger.info("Cleaning up hub: %s", hub_name)
110+
try:
111+
# Delete hub content references first
112+
contents = sm.list_hub_contents(
113+
HubName=hub_name,
114+
HubContentType="ModelReference",
115+
)
116+
for content in contents.get("HubContentSummaries", []):
117+
try:
118+
sm.delete_hub_content_reference(
119+
HubName=hub_name,
120+
HubContentName=content["HubContentName"],
121+
HubContentType="ModelReference",
122+
)
123+
except Exception as e:
124+
logger.warning("Failed to delete content ref: %s", e)
125+
126+
# Delete the hub
127+
sm.delete_hub(HubName=hub_name)
128+
logger.info("Hub %s deleted", hub_name)
129+
except Exception as e:
130+
logger.warning("Hub cleanup failed: %s", e)
131+
132+
133+
@pytest.fixture(scope="module")
134+
def sagemaker_session():
135+
"""Create a SageMaker session using default credentials."""
136+
from sagemaker.core.helper.session_helper import Session
137+
138+
return Session(boto_session=boto3.Session(region_name=TEST_REGION))
139+
140+
141+
@pytest.fixture(scope="module")
142+
def execution_role():
143+
"""Get a SageMaker execution role from the caller's identity."""
144+
sts = boto3.client("sts", region_name=TEST_REGION)
145+
identity = sts.get_caller_identity()
146+
account_id = identity["Account"]
147+
# Use the standard SageMaker execution role naming convention
148+
return f"arn:aws:iam::{account_id}:role/Admin"
149+
150+
151+
@pytest.mark.slow_test
152+
def test_from_jumpstart_config_derives_hub_arn(private_hub, sagemaker_session):
153+
"""Verify from_jumpstart_config correctly derives hub_arn from hub_name."""
154+
js_config = JumpStartConfig(
155+
model_id=TEST_MODEL_ID,
156+
model_version=TEST_MODEL_VERSION,
157+
hub_name=private_hub,
158+
)
159+
160+
mb = ModelBuilder.from_jumpstart_config(
161+
jumpstart_config=js_config,
162+
compute=Compute(instance_type=TEST_INSTANCE_TYPE),
163+
sagemaker_session=sagemaker_session,
164+
)
165+
166+
assert mb.hub_arn is not None, (
167+
f"hub_arn is None after from_jumpstart_config with "
168+
f"hub_name={private_hub}"
169+
)
170+
assert private_hub in mb.hub_arn
171+
logger.info("hub_arn correctly derived: %s", mb.hub_arn)
172+
173+
174+
@pytest.mark.slow_test
175+
def test_build_resolves_artifacts_via_private_hub(
176+
private_hub, execution_role, sagemaker_session
177+
):
178+
"""Verify build() resolves model data through the private hub."""
179+
js_config = JumpStartConfig(
180+
model_id=TEST_MODEL_ID,
181+
model_version=TEST_MODEL_VERSION,
182+
hub_name=private_hub,
183+
accept_eula=True,
184+
)
185+
186+
mb = ModelBuilder.from_jumpstart_config(
187+
jumpstart_config=js_config,
188+
role_arn=execution_role,
189+
compute=Compute(instance_type=TEST_INSTANCE_TYPE),
190+
sagemaker_session=sagemaker_session,
191+
)
192+
193+
mb.build()
194+
195+
# After build, model data URI must NOT reference the public cache
196+
model_data = getattr(mb, "s3_model_data_url", None)
197+
assert model_data is not None, "No model_data found after build()"
198+
199+
model_data_str = str(model_data)
200+
assert "jumpstart-cache-prod" not in model_data_str, (
201+
f"Model data still points to public JumpStart cache: "
202+
f"{model_data_str}. Expected private hub artifact resolution."
203+
)
204+
logger.info("Model data resolved to: %s", model_data_str)

0 commit comments

Comments
 (0)