Skip to content

Commit ae1f6cf

Browse files
committed
fix: ModelBuilder resolves private hub artifacts correctly
Fix three 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: derive hub_arn inline after setting hub_name. Defect 2: _build_for_jumpstart does not forward hub_arn or sagemaker_session to get_init_kwargs, causing model data to resolve from the public catalog. Fix: pass hub_arn (when set) and sagemaker_session to all get_init_kwargs call sites in the build path. Defect 3: The v3 Session class (sagemaker.core.helper.session_helper.Session) is missing hub API methods (describe_hub_content, list_hub_content_versions, list_hub_contents) that the JumpStart cache calls during hub content resolution. Fix: add these methods as thin wrappers around the boto3 sagemaker_client calls. 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 new unit tests covering hub_arn derivation, forwarding, and e2e flow - 2 existing unit tests updated to match new call signatures
1 parent 78bb11e commit ae1f6cf

7 files changed

Lines changed: 670 additions & 3 deletions

File tree

‎sagemaker-core/src/sagemaker/core/helper/session_helper.py‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1985,7 +1985,75 @@ def expand_role(self, role):
19851985
if "/" in role:
19861986
return role
19871987
return self.boto_session.resource("iam").Role(role).arn
1988-
1988+
1989+
# ========================================
1990+
# Hub Operations
1991+
# ========================================
1992+
1993+
def describe_hub_content(
1994+
self, hub_name, hub_content_name, hub_content_version, hub_content_type, **kwargs
1995+
):
1996+
"""Describe hub content in a SageMaker Hub.
1997+
1998+
Args:
1999+
hub_name (str): The name or ARN of the hub.
2000+
hub_content_name (str): The name of the hub content.
2001+
hub_content_version (str): The version of the hub content.
2002+
hub_content_type (str): The type of hub content (Model, ModelReference, Notebook).
2003+
2004+
Returns:
2005+
dict: Response from the DescribeHubContent API.
2006+
"""
2007+
return self.sagemaker_client.describe_hub_content(
2008+
HubName=hub_name,
2009+
HubContentName=hub_content_name,
2010+
HubContentVersion=hub_content_version,
2011+
HubContentType=hub_content_type,
2012+
**kwargs,
2013+
)
2014+
2015+
def list_hub_content_versions(self, hub_name, hub_content_name, hub_content_type, **kwargs):
2016+
"""List versions of hub content in a SageMaker Hub.
2017+
2018+
Args:
2019+
hub_name (str): The name or ARN of the hub.
2020+
hub_content_name (str): The name of the hub content.
2021+
hub_content_type (str): The type of hub content.
2022+
**kwargs: Additional arguments (e.g., next_token for pagination).
2023+
2024+
Returns:
2025+
dict: Response from the ListHubContentVersions API.
2026+
"""
2027+
request = {
2028+
"HubName": hub_name,
2029+
"HubContentName": hub_content_name,
2030+
"HubContentType": hub_content_type,
2031+
}
2032+
next_token = kwargs.get("next_token")
2033+
if next_token:
2034+
request["NextToken"] = next_token
2035+
return self.sagemaker_client.list_hub_content_versions(**request)
2036+
2037+
def list_hub_contents(self, hub_name, hub_content_type, **kwargs):
2038+
"""List hub contents in a SageMaker Hub.
2039+
2040+
Args:
2041+
hub_name (str): The name or ARN of the hub.
2042+
hub_content_type (str): The type of hub content to list.
2043+
**kwargs: Additional arguments (e.g., next_token for pagination).
2044+
2045+
Returns:
2046+
dict: Response from the ListHubContents API.
2047+
"""
2048+
request = {
2049+
"HubName": hub_name,
2050+
"HubContentType": hub_content_type,
2051+
}
2052+
next_token = kwargs.get("next_token")
2053+
if next_token:
2054+
request["NextToken"] = next_token
2055+
return self.sagemaker_client.list_hub_contents(**request)
2056+
19892057

19902058
def _expand_container_def(c_def):
19912059
"""Placeholder docstring"""

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3895,6 +3895,16 @@ def from_jumpstart_config(
38953895
mb_instance.resource_requirements = resource_requirements
38963896
mb_instance.model_kms_key = model_kms_key
38973897
mb_instance.hub_name = jumpstart_config.hub_name
3898+
if mb_instance.hub_name and not getattr(mb_instance, "hub_arn", None):
3899+
from sagemaker.core.jumpstart.hub.utils import (
3900+
generate_hub_arn_for_init_kwargs,
3901+
)
3902+
3903+
mb_instance.hub_arn = generate_hub_arn_for_init_kwargs(
3904+
hub_name=mb_instance.hub_name,
3905+
region=mb_instance.region,
3906+
session=mb_instance.sagemaker_session,
3907+
)
38983908
mb_instance.config_name = jumpstart_config.inference_config_name
38993909
mb_instance.accept_eula = jumpstart_config.accept_eula
39003910
mb_instance.tolerate_vulnerable_model = tolerate_vulnerable_model

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,15 +975,20 @@ def _build_for_jumpstart(self) -> Model:
975975
self.secret_key = ""
976976

977977
# Get JumpStart model configuration
978-
init_kwargs = get_init_kwargs(
978+
init_kwargs_params = dict(
979979
model_id=self.model,
980980
model_version=self.model_version or "*",
981981
region=self.region,
982982
instance_type=self.instance_type,
983+
sagemaker_session=self.sagemaker_session,
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),
986987
)
988+
hub_arn = getattr(self, "hub_arn", None)
989+
if hub_arn:
990+
init_kwargs_params["hub_arn"] = hub_arn
991+
init_kwargs = get_init_kwargs(**init_kwargs_params)
987992

988993
# Configure image URI and environment variables
989994
self.image_uri = self.image_uri or init_kwargs.image_uri

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,19 @@ def _detect_jumpstart_image(self) -> None:
629629
ValueError: If image URI cannot be determined or JumpStart lookup fails.
630630
"""
631631
try:
632-
init_kwargs = get_init_kwargs(
632+
detect_kwargs = dict(
633633
model_id=self.model,
634634
model_version=getattr(self, "model_version", None) or "*",
635635
region=self.region,
636636
instance_type=getattr(self, "instance_type", None),
637+
sagemaker_session=getattr(self, "sagemaker_session", None),
637638
tolerate_vulnerable_model=getattr(self, "tolerate_vulnerable_model", None),
638639
tolerate_deprecated_model=getattr(self, "tolerate_deprecated_model", None),
639640
)
641+
hub_arn = getattr(self, "hub_arn", None)
642+
if hub_arn:
643+
detect_kwargs["hub_arn"] = hub_arn
644+
init_kwargs = get_init_kwargs(**detect_kwargs)
640645

641646
self.image_uri = init_kwargs.get("image_uri")
642647
if not self.image_uri:
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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. Skips gracefully if
21+
the test environment lacks permissions to create hubs.
22+
"""
23+
from __future__ import absolute_import
24+
25+
import os
26+
import uuid
27+
import time
28+
import logging
29+
30+
import boto3
31+
import pytest
32+
from botocore.exceptions import ClientError
33+
34+
from sagemaker.serve.model_builder import ModelBuilder
35+
from sagemaker.core.jumpstart.configs import JumpStartConfig
36+
from sagemaker.train.configs import Compute
37+
38+
logger = logging.getLogger(__name__)
39+
40+
# Use a small/fast JumpStart model for the test
41+
TEST_MODEL_ID = "huggingface-llm-gemma-2b"
42+
TEST_MODEL_VERSION = "*"
43+
TEST_INSTANCE_TYPE = "ml.g5.xlarge"
44+
TEST_REGION = os.environ.get("TEST_REGION", "us-east-1")
45+
46+
HUB_NAME_PREFIX = "sdk-integ-test-private-hub"
47+
48+
49+
@pytest.fixture(scope="module")
50+
def private_hub():
51+
"""Create a private hub with a ModelReference, tear down after tests."""
52+
sm = boto3.client("sagemaker", region_name=TEST_REGION)
53+
hub_name = f"{HUB_NAME_PREFIX}-{uuid.uuid4().hex[:8]}"
54+
55+
# --- Setup ---
56+
logger.info("Creating private hub: %s", hub_name)
57+
try:
58+
sm.create_hub(
59+
HubName=hub_name,
60+
HubDescription="SDK integration test for private hub artifact resolution",
61+
Tags=[
62+
{"Key": "Purpose", "Value": "sdk-integ-test"},
63+
{"Key": "AutoCleanup", "Value": "true"},
64+
],
65+
)
66+
except ClientError as e:
67+
pytest.skip(f"Cannot create hub (likely missing permissions): {e}")
68+
69+
# Wait for hub to be ready
70+
for _ in range(30):
71+
resp = sm.describe_hub(HubName=hub_name)
72+
if resp["HubStatus"] == "InService":
73+
break
74+
time.sleep(2)
75+
else:
76+
pytest.skip(f"Hub {hub_name} did not reach InService state")
77+
78+
# Add a ModelReference to the public JumpStart content
79+
public_arn = (
80+
f"arn:aws:sagemaker:{TEST_REGION}:aws:hub-content/"
81+
f"SageMakerPublicHub/Model/{TEST_MODEL_ID}"
82+
)
83+
logger.info("Creating hub content reference to: %s", public_arn)
84+
try:
85+
sm.create_hub_content_reference(
86+
HubName=hub_name,
87+
SageMakerPublicHubContentArn=public_arn,
88+
)
89+
except ClientError as e:
90+
pytest.skip(f"Cannot create hub content reference: {e}")
91+
92+
# Wait for content reference to be available
93+
for _ in range(60):
94+
try:
95+
contents = sm.list_hub_contents(
96+
HubName=hub_name,
97+
HubContentType="ModelReference",
98+
)
99+
summaries = contents.get("HubContentSummaries", [])
100+
if any(
101+
s["HubContentName"] == TEST_MODEL_ID and s.get("HubContentStatus") == "Available"
102+
for s in summaries
103+
):
104+
break
105+
except Exception:
106+
pass
107+
time.sleep(3)
108+
else:
109+
pytest.skip(
110+
f"ModelReference for {TEST_MODEL_ID} not available in "
111+
f"hub {hub_name} after 3 minutes"
112+
)
113+
114+
yield hub_name
115+
116+
# --- Teardown ---
117+
logger.info("Cleaning up hub: %s", hub_name)
118+
try:
119+
contents = sm.list_hub_contents(
120+
HubName=hub_name,
121+
HubContentType="ModelReference",
122+
)
123+
for content in contents.get("HubContentSummaries", []):
124+
try:
125+
sm.delete_hub_content_reference(
126+
HubName=hub_name,
127+
HubContentName=content["HubContentName"],
128+
HubContentType="ModelReference",
129+
)
130+
except Exception as e:
131+
logger.warning("Failed to delete content ref: %s", e)
132+
133+
sm.delete_hub(HubName=hub_name)
134+
logger.info("Hub %s deleted", hub_name)
135+
except Exception as e:
136+
logger.warning("Hub cleanup failed: %s", e)
137+
138+
139+
@pytest.fixture(scope="module")
140+
def sagemaker_session():
141+
"""Create a SageMaker session using default credentials."""
142+
from sagemaker.core.helper.session_helper import Session
143+
144+
return Session(boto_session=boto3.Session(region_name=TEST_REGION))
145+
146+
147+
@pytest.fixture(scope="module")
148+
def execution_role():
149+
"""Get a SageMaker execution role from the caller's identity."""
150+
sts = boto3.client("sts", region_name=TEST_REGION)
151+
identity = sts.get_caller_identity()
152+
account_id = identity["Account"]
153+
return f"arn:aws:iam::{account_id}:role/Admin"
154+
155+
156+
@pytest.mark.slow_test
157+
def test_from_jumpstart_config_derives_hub_arn(private_hub, sagemaker_session):
158+
"""Verify from_jumpstart_config correctly derives hub_arn from hub_name."""
159+
js_config = JumpStartConfig(
160+
model_id=TEST_MODEL_ID,
161+
model_version=TEST_MODEL_VERSION,
162+
hub_name=private_hub,
163+
)
164+
165+
mb = ModelBuilder.from_jumpstart_config(
166+
jumpstart_config=js_config,
167+
compute=Compute(instance_type=TEST_INSTANCE_TYPE),
168+
sagemaker_session=sagemaker_session,
169+
)
170+
171+
assert (
172+
mb.hub_arn is not None
173+
), f"hub_arn is None after from_jumpstart_config with hub_name={private_hub}"
174+
assert private_hub in mb.hub_arn
175+
logger.info("hub_arn correctly derived: %s", mb.hub_arn)
176+
177+
178+
@pytest.mark.slow_test
179+
def test_build_resolves_artifacts_via_private_hub(private_hub, execution_role, sagemaker_session):
180+
"""Verify build() resolves model data through the private hub."""
181+
js_config = JumpStartConfig(
182+
model_id=TEST_MODEL_ID,
183+
model_version=TEST_MODEL_VERSION,
184+
hub_name=private_hub,
185+
accept_eula=True,
186+
)
187+
188+
mb = ModelBuilder.from_jumpstart_config(
189+
jumpstart_config=js_config,
190+
role_arn=execution_role,
191+
compute=Compute(instance_type=TEST_INSTANCE_TYPE),
192+
sagemaker_session=sagemaker_session,
193+
)
194+
195+
mb.build()
196+
197+
# After build, model data URI must NOT reference the public cache
198+
model_data = getattr(mb, "s3_model_data_url", None)
199+
assert model_data is not None, "No model_data found after build()"
200+
201+
model_data_str = str(model_data)
202+
assert "jumpstart-cache-prod" not in model_data_str, (
203+
f"Model data still points to public JumpStart cache: "
204+
f"{model_data_str}. Expected private hub artifact resolution."
205+
)
206+
logger.info("Model data resolved to: %s", model_data_str)

‎sagemaker-serve/tests/unit/servers/test_model_builder_servers.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,7 @@ def test_build_passes_config_name_to_get_init_kwargs(
948948
model_version="*",
949949
region=self.builder.region,
950950
instance_type=self.builder.instance_type,
951+
sagemaker_session=self.builder.sagemaker_session,
951952
tolerate_vulnerable_model=None,
952953
tolerate_deprecated_model=None,
953954
config_name="lmi-optimized",
@@ -979,6 +980,7 @@ def test_build_passes_none_config_name_when_not_set(
979980
model_version="*",
980981
region=self.builder.region,
981982
instance_type=self.builder.instance_type,
983+
sagemaker_session=self.builder.sagemaker_session,
982984
tolerate_vulnerable_model=None,
983985
tolerate_deprecated_model=None,
984986
config_name=None,

0 commit comments

Comments
 (0)