Skip to content

Commit 2afbe28

Browse files
authored
feat: split MODEL_CUSTOMIZATION telemetry into NOVA and OSS sub-features (#5720)
* feat: split MODEL_CUSTOMIZATION telemetry into NOVA and OSS sub-features * test: add unit tests for NOVA/OSS telemetry sub-feature detection
1 parent 272fdbf commit 2afbe28

File tree

7 files changed

+157
-0
lines changed

7 files changed

+157
-0
lines changed

sagemaker-core/src/sagemaker/core/telemetry/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class Feature(Enum):
3030
MLOPS = 16
3131
FEATURE_STORE = 17
3232
PROCESSING = 18
33+
MODEL_CUSTOMIZATION_NOVA = 19
34+
MODEL_CUSTOMIZATION_OSS = 20
3335

3436
def __str__(self): # pylint: disable=E0307
3537
"""Return the feature name."""

sagemaker-core/src/sagemaker/core/telemetry/telemetry_logging.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
str(Feature.MLOPS): 16,
6363
str(Feature.FEATURE_STORE): 17,
6464
str(Feature.PROCESSING): 18,
65+
str(Feature.MODEL_CUSTOMIZATION_NOVA): 19,
66+
str(Feature.MODEL_CUSTOMIZATION_OSS): 20,
6567
}
6668

6769
STATUS_TO_CODE = {
@@ -115,6 +117,25 @@ def wrapper(*args, **kwargs):
115117
# Construct the feature list to track feature combinations
116118
feature_list: List[int] = [FEATURE_TO_CODE[str(feature)]]
117119

120+
# For MODEL_CUSTOMIZATION, append NOVA or OSS sub-feature
121+
# based on the instance's _is_nova_model_for_telemetry() method
122+
if feature == Feature.MODEL_CUSTOMIZATION and len(args) > 0:
123+
instance = args[0]
124+
try:
125+
if hasattr(instance, "_is_nova_model_for_telemetry"):
126+
if instance._is_nova_model_for_telemetry():
127+
feature_list.append(
128+
FEATURE_TO_CODE[str(Feature.MODEL_CUSTOMIZATION_NOVA)]
129+
)
130+
else:
131+
feature_list.append(
132+
FEATURE_TO_CODE[str(Feature.MODEL_CUSTOMIZATION_OSS)]
133+
)
134+
except Exception: # pylint: disable=W0703
135+
logger.debug(
136+
"Unable to determine NOVA/OSS model type for telemetry."
137+
)
138+
118139
if (
119140
hasattr(sagemaker_session, "sagemaker_config")
120141
and sagemaker_session.sagemaker_config

sagemaker-core/tests/unit/telemetry/test_telemetry_logging.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,108 @@ def test_telemetry_emitter_without_resource_arn(
542542
args = mock_send_telemetry_request.call_args.args
543543
extra_str = str(args[5])
544544
self.assertNotIn("x-resourceArn", extra_str)
545+
546+
@patch("sagemaker.core.telemetry.telemetry_logging._send_telemetry_request")
547+
@patch("sagemaker.core.telemetry.telemetry_logging.resolve_value_from_config")
548+
def test_telemetry_emitter_appends_nova_sub_feature(
549+
self, mock_resolve_config, mock_send_telemetry_request
550+
):
551+
"""Test that MODEL_CUSTOMIZATION_NOVA (19) is appended when instance reports Nova model."""
552+
mock_resolve_config.return_value = False
553+
554+
class NovaModelMock:
555+
def __init__(self):
556+
self.sagemaker_session = MOCK_SESSION
557+
558+
def _is_nova_model_for_telemetry(self):
559+
return True
560+
561+
@_telemetry_emitter(Feature.MODEL_CUSTOMIZATION, "NovaModelMock.train")
562+
def train(self):
563+
pass
564+
565+
NovaModelMock().train()
566+
567+
args = mock_send_telemetry_request.call_args.args
568+
feature_list = args[1]
569+
self.assertIn(15, feature_list) # MODEL_CUSTOMIZATION
570+
self.assertIn(19, feature_list) # MODEL_CUSTOMIZATION_NOVA
571+
self.assertNotIn(20, feature_list) # MODEL_CUSTOMIZATION_OSS should NOT be present
572+
573+
@patch("sagemaker.core.telemetry.telemetry_logging._send_telemetry_request")
574+
@patch("sagemaker.core.telemetry.telemetry_logging.resolve_value_from_config")
575+
def test_telemetry_emitter_appends_oss_sub_feature(
576+
self, mock_resolve_config, mock_send_telemetry_request
577+
):
578+
"""Test that MODEL_CUSTOMIZATION_OSS (20) is appended when instance reports non-Nova model."""
579+
mock_resolve_config.return_value = False
580+
581+
class OssModelMock:
582+
def __init__(self):
583+
self.sagemaker_session = MOCK_SESSION
584+
585+
def _is_nova_model_for_telemetry(self):
586+
return False
587+
588+
@_telemetry_emitter(Feature.MODEL_CUSTOMIZATION, "OssModelMock.train")
589+
def train(self):
590+
pass
591+
592+
OssModelMock().train()
593+
594+
args = mock_send_telemetry_request.call_args.args
595+
feature_list = args[1]
596+
self.assertIn(15, feature_list) # MODEL_CUSTOMIZATION
597+
self.assertIn(20, feature_list) # MODEL_CUSTOMIZATION_OSS
598+
self.assertNotIn(19, feature_list) # MODEL_CUSTOMIZATION_NOVA should NOT be present
599+
600+
@patch("sagemaker.core.telemetry.telemetry_logging._send_telemetry_request")
601+
@patch("sagemaker.core.telemetry.telemetry_logging.resolve_value_from_config")
602+
def test_telemetry_emitter_no_sub_feature_without_detection_method(
603+
self, mock_resolve_config, mock_send_telemetry_request
604+
):
605+
"""Test that no NOVA/OSS sub-feature is appended when instance lacks detection method."""
606+
mock_resolve_config.return_value = False
607+
608+
class NoDetectionMock:
609+
def __init__(self):
610+
self.sagemaker_session = MOCK_SESSION
611+
612+
@_telemetry_emitter(Feature.MODEL_CUSTOMIZATION, "NoDetectionMock.do_work")
613+
def do_work(self):
614+
pass
615+
616+
NoDetectionMock().do_work()
617+
618+
args = mock_send_telemetry_request.call_args.args
619+
feature_list = args[1]
620+
self.assertIn(15, feature_list) # MODEL_CUSTOMIZATION
621+
self.assertNotIn(19, feature_list) # No NOVA
622+
self.assertNotIn(20, feature_list) # No OSS
623+
624+
@patch("sagemaker.core.telemetry.telemetry_logging._send_telemetry_request")
625+
@patch("sagemaker.core.telemetry.telemetry_logging.resolve_value_from_config")
626+
def test_telemetry_emitter_handles_detection_method_exception(
627+
self, mock_resolve_config, mock_send_telemetry_request
628+
):
629+
"""Test that telemetry still works when _is_nova_model_for_telemetry raises an exception."""
630+
mock_resolve_config.return_value = False
631+
632+
class BrokenDetectionMock:
633+
def __init__(self):
634+
self.sagemaker_session = MOCK_SESSION
635+
636+
def _is_nova_model_for_telemetry(self):
637+
raise RuntimeError("detection failed")
638+
639+
@_telemetry_emitter(Feature.MODEL_CUSTOMIZATION, "BrokenDetectionMock.train")
640+
def train(self):
641+
pass
642+
643+
BrokenDetectionMock().train()
644+
645+
args = mock_send_telemetry_request.call_args.args
646+
feature_list = args[1]
647+
self.assertIn(15, feature_list) # MODEL_CUSTOMIZATION still present
648+
self.assertNotIn(19, feature_list) # No NOVA (detection failed gracefully)
649+
self.assertNotIn(20, feature_list) # No OSS

sagemaker-serve/src/sagemaker/serve/bedrock_model_builder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ def _get_sagemaker_client(self):
9595
self._sagemaker_client = self.boto_session.client("sagemaker")
9696
return self._sagemaker_client
9797

98+
def _is_nova_model_for_telemetry(self) -> bool:
99+
"""Check if the model is a Nova model for telemetry tracking."""
100+
try:
101+
if not self.model_package:
102+
return False
103+
container = self.model_package.inference_specification.containers[0]
104+
return _is_nova_model(container)
105+
except Exception:
106+
return False
107+
98108
@_telemetry_emitter(feature=Feature.MODEL_CUSTOMIZATION, func_name="BedrockModelBuilder.deploy")
99109
def deploy(
100110
self,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,13 @@ def _is_nova_model(self) -> bool:
10631063
hub_content_name = getattr(base_model, "hub_content_name", "") or ""
10641064
return "nova" in recipe_name.lower() or "nova" in hub_content_name.lower()
10651065

1066+
def _is_nova_model_for_telemetry(self) -> bool:
1067+
"""Check if the model is a Nova model for telemetry tracking."""
1068+
try:
1069+
return self._is_nova_model()
1070+
except Exception:
1071+
return False
1072+
10661073
def _get_nova_hosting_config(self, instance_type=None):
10671074
"""Get Nova hosting config (image URI, env vars, instance type).
10681075

sagemaker-train/src/sagemaker/train/base_trainer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ def __init__(
6969
self.input_data_config = input_data_config
7070
self.environment = environment or {}
7171

72+
def _is_nova_model_for_telemetry(self) -> bool:
73+
"""Check if the model is a Nova model for telemetry tracking."""
74+
from sagemaker.train.common_utils.recipe_utils import _is_nova_model
75+
model_name = getattr(self, "_model_name", None)
76+
return _is_nova_model(model_name) if model_name else False
77+
7278
@abstractmethod
7379
def train(self, input_data_config: List[InputData], wait: bool = True, logs: bool = True):
7480
"""Common training method that calls the specific implementation."""

sagemaker-train/src/sagemaker/train/evaluate/base_evaluator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ def _source_model_package_arn(self) -> Optional[str]:
414414
info = self._get_resolved_model_info()
415415
return info.source_model_package_arn if info else None
416416

417+
def _is_nova_model_for_telemetry(self) -> bool:
418+
"""Check if the model is a Nova model for telemetry tracking."""
419+
from ..common_utils.recipe_utils import _is_nova_model
420+
base_model_name = self._base_model_name
421+
return _is_nova_model(base_model_name) if base_model_name else False
422+
417423
@property
418424
def _is_jumpstart_model(self) -> bool:
419425
"""Determine if model is a JumpStart model"""

0 commit comments

Comments
 (0)