Skip to content

Commit a96a93f

Browse files
committed
fix(train): Fall back to public hub when private hub lacks base model
When resolving a model package's base model, the reconstructed hub-content ARN honored SAGEMAKER_HUB_NAME but never verified the base model actually existed in that private hub, and never fell back. If the private hub had not mirrored the base model (or its ModelReference was cleaned up), server-side CreateTrainingJob failed with "Hub content ... does not exist", breaking evaluation integ tests (benchmark, custom scorer, and LLM-as-judge, including custom scorer with a built-in metric). Verify the base model exists in the configured private hub and fall back to SageMakerPublicHub when it is absent, mirroring the existing fallback in _resolve_jumpstart_model. Base models are always published to the public hub, so this keeps evaluation working regardless of private-hub contents. The public-hub path is unchanged and does no extra lookups.
1 parent 1a22381 commit a96a93f

2 files changed

Lines changed: 111 additions & 5 deletions

File tree

‎sagemaker-train/src/sagemaker/train/common_utils/model_resolution.py‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import json
10+
import logging
1011
import boto3
1112
from typing import Union, Optional, Dict, Any
1213
from dataclasses import dataclass
@@ -16,6 +17,8 @@
1617
from sagemaker.train.constants import get_sagemaker_hub_name
1718
from sagemaker.core.utils.utils import Unassigned
1819

20+
_logger = logging.getLogger(__name__)
21+
1922

2023
class _ModelType(Enum):
2124
"""Internal enum for model type classification."""
@@ -368,7 +371,10 @@ def _resolve_model_package_object(self, model_package: 'ModelPackage') -> _Model
368371
# integ-test hub) resolve correctly. Public-hub content is
369372
# account-less ("aws"); private-hub content lives under the
370373
# model package's own account.
371-
hub_name = get_sagemaker_hub_name()
374+
#
375+
hub_name = self._resolve_base_model_hub(
376+
hub_content_name, hub_content_version, region
377+
)
372378
hub_account = "aws" if hub_name == "SageMakerPublicHub" else account
373379
base_model_arn = f"arn:aws:sagemaker:{region}:{hub_account}:hub-content/{hub_name}/Model/{hub_content_name}/{hub_content_version}"
374380

@@ -464,6 +470,54 @@ def _validate_model_package_arn(self, arn: str) -> bool:
464470
)
465471
return True
466472

473+
def _resolve_base_model_hub(
474+
self, hub_content_name: str, hub_content_version: str, region: str
475+
) -> str:
476+
"""Pick the hub that actually contains the base model's hub content.
477+
478+
Prefers the hub from ``SAGEMAKER_HUB_NAME`` (defaults to
479+
``SageMakerPublicHub``). When a non-public hub is configured but does
480+
not contain the base model, falls back to ``SageMakerPublicHub`` since
481+
base models are always published there. This keeps evaluation working
482+
when a private/custom hub never mirrored the base model (or its
483+
ModelReference was cleaned up).
484+
485+
Args:
486+
hub_content_name: Base model hub content name.
487+
hub_content_version: Base model hub content version.
488+
region: AWS region of the model package.
489+
490+
Returns:
491+
The hub name whose content should back the base model ARN.
492+
"""
493+
hub_name = get_sagemaker_hub_name()
494+
if hub_name == "SageMakerPublicHub":
495+
return hub_name
496+
497+
from sagemaker.core.resources import HubContent
498+
499+
try:
500+
session = self._get_session()
501+
HubContent.get(
502+
hub_name=hub_name,
503+
hub_content_type="Model",
504+
hub_content_name=hub_content_name,
505+
hub_content_version=hub_content_version,
506+
session=session.boto_session,
507+
region=region,
508+
)
509+
return hub_name
510+
except Exception as e:
511+
_logger.info(
512+
"Base model '%s' (v%s) not found in hub '%s' (%s); "
513+
"falling back to SageMakerPublicHub.",
514+
hub_content_name,
515+
hub_content_version,
516+
hub_name,
517+
e,
518+
)
519+
return "SageMakerPublicHub"
520+
467521
def _get_session(self):
468522
"""
469523
Get or create SageMaker session.

‎sagemaker-train/tests/unit/train/common_utils/test_model_resolution.py‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,19 +440,23 @@ def test_resolve_arn_construct_hub_content_arn(self, mock_validate, mock_get_ses
440440
assert result.base_model_name == "base-model"
441441
assert result.hub_content_name == "base-model"
442442

443+
@patch('sagemaker.core.resources.HubContent')
443444
@patch('sagemaker.core.resources.ModelPackage')
444445
@patch('sagemaker.train.common_utils.model_resolution._ModelResolver._get_session')
445446
@patch('sagemaker.train.common_utils.model_resolution._ModelResolver._validate_model_package_arn')
446-
def test_resolve_arn_construct_hub_content_arn_private_hub(self, mock_validate, mock_get_session, mock_model_package_class):
447-
"""When SAGEMAKER_HUB_NAME points at a private hub, the reconstructed
448-
base-model ARN targets that hub under the model package's own account
449-
(not the account-less public hub)."""
447+
def test_resolve_arn_construct_hub_content_arn_private_hub(self, mock_validate, mock_get_session, mock_model_package_class, mock_hub_content_class):
448+
"""When SAGEMAKER_HUB_NAME points at a private hub that DOES contain the
449+
base model, the reconstructed base-model ARN targets that hub under the
450+
model package's own account (not the account-less public hub)."""
450451
arn = "arn:aws:sagemaker:us-west-2:123456789012:model-package/my-model/1"
451452

452453
mock_session = MagicMock()
453454
mock_session.boto_session.region_name = 'us-west-2'
454455
mock_get_session.return_value = mock_session
455456

457+
# Private hub contains the base model, so verification succeeds.
458+
mock_hub_content_class.get.return_value = MagicMock()
459+
456460
# Mock ModelPackage without hub_content_arn (needs to be constructed)
457461
mock_package = MagicMock()
458462
mock_package.model_package_arn = arn
@@ -478,6 +482,54 @@ def test_resolve_arn_construct_hub_content_arn_private_hub(self, mock_validate,
478482
assert result.base_model_arn == expected_arn
479483
assert result.base_model_name == "mock-oss-test"
480484
assert result.hub_content_name == "mock-oss-test"
485+
486+
@patch('sagemaker.core.resources.HubContent')
487+
@patch('sagemaker.core.resources.ModelPackage')
488+
@patch('sagemaker.train.common_utils.model_resolution._ModelResolver._get_session')
489+
@patch('sagemaker.train.common_utils.model_resolution._ModelResolver._validate_model_package_arn')
490+
def test_resolve_arn_construct_hub_content_arn_private_hub_fallback_public(self, mock_validate, mock_get_session, mock_model_package_class, mock_hub_content_class):
491+
"""When SAGEMAKER_HUB_NAME points at a private hub that does NOT contain
492+
the base model (e.g. it never mirrored it or was cleaned up), the
493+
reconstructed base-model ARN falls back to the account-less public hub.
494+
495+
Regression test: without this fallback the server-side CreateTrainingJob
496+
fails with 'Hub content ... does not exist' during evaluation."""
497+
arn = "arn:aws:sagemaker:us-west-2:123456789012:model-package/my-model/1"
498+
499+
mock_session = MagicMock()
500+
mock_session.boto_session.region_name = 'us-west-2'
501+
mock_get_session.return_value = mock_session
502+
503+
# Private hub does NOT contain the base model -> verification raises.
504+
mock_hub_content_class.get.side_effect = Exception(
505+
"Hub content with name mock-oss-test does not exist."
506+
)
507+
508+
# Mock ModelPackage without hub_content_arn (needs to be constructed)
509+
mock_package = MagicMock()
510+
mock_package.model_package_arn = arn
511+
512+
mock_container = MagicMock()
513+
mock_base_model = MagicMock()
514+
mock_base_model.hub_content_name = 'mock-oss-test'
515+
mock_base_model.hub_content_version = '0.0.1'
516+
mock_base_model.hub_content_arn = None # Not provided, needs construction
517+
mock_container.base_model = mock_base_model
518+
519+
mock_package.inference_specification = MagicMock()
520+
mock_package.inference_specification.containers = [mock_container]
521+
522+
mock_model_package_class.get.return_value = mock_package
523+
524+
resolver = _ModelResolver()
525+
with patch.dict(os.environ, {"SAGEMAKER_HUB_NAME": "sdktest"}):
526+
result = resolver._resolve_model_package_arn(arn)
527+
528+
# Fell back to the account-less public hub since the private hub lacks it.
529+
expected_arn = "arn:aws:sagemaker:us-west-2:aws:hub-content/SageMakerPublicHub/Model/mock-oss-test/0.0.1"
530+
assert result.base_model_arn == expected_arn
531+
assert result.base_model_name == "mock-oss-test"
532+
assert result.hub_content_name == "mock-oss-test"
481533

482534
@patch('sagemaker.core.resources.ModelPackage')
483535
@patch('sagemaker.train.common_utils.model_resolution._ModelResolver._get_session')

0 commit comments

Comments
 (0)