Repository navigation
fix: preserve all core container fields in ModelBuilder CreateModel requests - #6488
Conversation
…equests
ModelBuilder._core_container_to_dict() rebuilt each core ContainerDefinition
by hand, copying only Image, ModelDataUrl, Environment and ImageConfig into
container_def(). Every other field was discarded, so for a list of
sagemaker.core.resources.Model objects:
- a model-package reference (ModelPackageName, no Image) lost its only
identifying field and was emitted as {"Image": Unassigned(),
"Environment": {}}, which botocore rejects before the call leaves the
client;
- a container with ModelDataSource kept its image, so creation proceeded
while the request silently lost the model data source;
- ModelDataSource, AdditionalModelDataSources, Mode, ContainerHostname,
InferenceSpecificationName, MultiModelConfig and ContainerMetricsConfig
were dropped, and ImageConfig was passed through as a pydantic shape
object rather than a dict.
The core serializer (sagemaker.core.utils.utils.serialize) already emits the
exact CreateModel request shape for every ContainerDefinition field, so the
container is now routed through it instead of being rebuilt field by field.
A container legitimately has no image when it references a model package, so
the two Image lookups in _create_sagemaker_model() are now a single .get()
instead of a subscript, which would otherwise raise KeyError: 'Image'.
Fixes aws#6476
There was a problem hiding this comment.
🤖 AI code review
This PR fixes ModelBuilder._core_container_to_dict() so it routes core ContainerDefinition objects through sagemaker.core.utils.utils.serialize() instead of rebuilding a fixed subset of fields, preserving ModelPackageName, ModelDataSource, ImageConfig (as a dict) and other fields in CreateModel requests. It also replaces two container_def[...]["Image"] subscripts in _create_sagemaker_model() with a single .get("Image") lookup so model-package references without an Image no longer raise KeyError. The change is correct, consistent with how sagemaker.core builds requests, and is backed by thorough unit tests including real botocore param validation. No blocking issues found.
Reviewed commit 2e87f0d. Automated review; verify before acting.
Issue #, if available: Fixes #6476
Description
ModelBuilder._core_container_to_dict()rebuilt each coreContainerDefinitionby hand, copying onlyImage,ModelDataUrl,EnvironmentandImageConfigintocontainer_def(). Everything else aContainerDefinitioncan carry was discarded. ForModelBuilder(model=[<core Model>, ...])that produced two distinct failures:ModelPackageNameand noImage. The converter droppedModelPackageNameand, becausecontainer_def()always writes anImagekey, emitted{"Image": Unassigned(), "Environment": {}}— the container's only identifying field was gone and the request carried an internal sentinel object in a string field.ModelDataSource. Such a container keeps its image, so model creation proceeded, but the generatedCreateModelrequest silently lost the model data source — the model would be created pointing at no artifacts.While reproducing this I found the same field copy also drops
AdditionalModelDataSources,Mode,ContainerHostname,InferenceSpecificationName,MultiModelConfigandContainerMetricsConfig, and passesImageConfigthrough as a pydantic shape object instead of a dict (also rejected by boto).Why this fix.
sagemaker.core.utils.utils.serialize()already emits the exactCreateModelrequest shape for everyContainerDefinitionfield — it is whatsagemaker.coreitself uses to build requests — so the container is now routed through it rather than rebuilt field by field._core_container_to_dict()is reached only from_prepare_pipeline_container_defs()(the list-of-core-Models path); the single-model and framework paths still go throughcontainer_def()unchanged. I verified thatsagemaker.core.resources.ContainerDefinition is sagemaker.core.shapes.ContainerDefinition, so there is no shadowed-class hazard here, andserialize()is a plain function (notpydantic.validate_call-wrapped), so it accepts the shape as given.A container legitimately has no image when it references a model package, so the two
container_def[...]["Image"]subscripts in_create_sagemaker_model()would now raiseKeyError: 'Image'. They are replaced by a single.get("Image")computed once and reused by both the interactive-session base-name lookup and the Nova network-isolation check (previously the same expression twice).botocore shape validation (before vs after). Using
botocore.validate.ParamValidatoragainst the realCreateModelinput shape frombotocore.session.get_session().get_service_model("sagemaker")— no network, no credentials — over nine container shapes routed through_create_sagemaker_model():Invalid type for parameter Containers[0].Image, value: , type: <class 'sagemaker.core.utils.utils.Unassigned'>, valid types: <class 'str'>[{"ModelPackageName": "arn:...:model-package/example/1"}]ModelDataSourceModelDataSourceabsent from the requestModelDataSourcepreservedImageConfigInvalid type for parameter Containers[0].ImageConfig ... type: <class 'sagemaker.core.shapes.shapes.ImageConfig'>, valid types: <class 'dict'>{"ImageConfig": {"RepositoryAccessMode": "Vpc"}}Containers[1].ImageAdditionalModelDataSourcesMode: MultiModelModelDataUrl+EnvironmentEnvironmentis no longer emitted as an empty dict; it is optional in the API)The only behavior change for containers that already worked is that an empty
Environmentis no longer emitted as{}.CreateModeltreatsEnvironmentas optional, and the only consumers that index["Environment"](sagemaker.core.local.entities,_create_model'sLOCAL_CONTAINERbranch) are fed by the single-modelcontainer_def()path, which is untouched.Testing
All offline. There is no live
CreateModelround-trip in this evidence — I had no usable AWS credentials, so correctness against the service is argued via boto's own client-side parameter validator and the service model, not via a real API call.Reproduced first against installed
3.25.0with the issue's scenario under aPipelineSession: theModelDataSourcecontainer producedContainers: [{"Image": "...", "Environment": {"EXAMPLE": "true"}}]with the data source gone, and the model-package container producedContainers: [{"Image": Unassigned(), "Environment": {}}]. Note: on3.25.0/masterthe model-package case does not raiseKeyError: 'Image'as the issue reports —container_def()always inserts anImagekey, so the failure instead surfaces as a botocoreParamValidationErrorat call time withModelPackageNamealready lost. TheKeyErroris what the issue's suggestedserialize()-only diff would introduce, which is why the.get("Image")guards are part of this change. The defect itself reproduces exactly as described.Added
TestCoreContainerDefinitionFieldstosagemaker-serve/tests/unit/test_model_builder_deploy.py(6 tests), driving the real_create_sagemaker_model()and asserting on thecontainer_defshanded tocreate_model:[{"ModelPackageName": ...}]ModelDataSourcecontainer keeps its data sourceImageConfigreaches the request as a dictPipelineSession(the base-name lookup path)CreateModelinput shape viabotocore.validate.ParamValidatortest_core_container_to_dictin the same file was rewritten: it asserted thatcontainer_defwas called, which is exactly the implementation detail this change removes. It now asserts the resulting dict for a plain container and passes both before and after the fix.Negative check (fix reverted, tests kept): 5 of the 6 new tests fail;
test_plain_image_container_is_unchangedpasses both ways by design, as does the rewrittentest_core_container_to_dict.Full
sagemaker-serveregression suite, with and without the fix:The two failure sets are identical. All 70 are environmental in my sandbox: 65 fail with
ClientError: ExpiredTokenonGetCallerIdentity(no valid credentials) and 5 withModuleNotFoundError: No module named 'tensorflow'. No other package was touched.Lint on both changed files:
black --line-length 100 --checkclean,flake8 --max-line-length 120clean,pydocstyleclean on thesrcfile.pylint --rcfile=.pylintrconsrc/sagemaker/serve/model_builder.py: 9.84/10 before and after, with the 29 findings identical apart from one line number shifting by 1.Merge Checklist
Put an
xin the boxes that apply.General
Tests
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.