From a7e4ec0f4eef5716264c99939dde1843674301fb Mon Sep 17 00:00:00 2001 From: Lucas Jia Date: Mon, 27 Jul 2026 15:41:22 -0700 Subject: [PATCH] fix(serve): repack source_code for image_uri/ModelTrainer builds ModelBuilder classified any image_uri build with a model artifact and source_code (no model/inference_spec) as passthrough and dropped the inference code, so build() produced a model.tar.gz without code/ and register() yielded a package that could not serve. This regressed the classic v2 Model(model_data, entry_point, source_dir) repack behavior. In _build_for_passthrough(), distinguish a pure image-only deployment from an "image + model artifact + source_code" build. For the latter, keep the source code and bridge model_path to s3_model_data_url so the existing _upload_code(repack=True) path repacks the code into the model artifact, producing a self-contained tarball. Pure-image (Nova/BYOC) passthrough behavior is unchanged. ModelTrainer builds are covered too since they are normalized to model_path before validation. Also pass script_dependencies (a list derived from SourceCode) to repack_model instead of the deprecated self.dependencies auto-detect dict, which repack_model would otherwise iterate as filesystem paths. --- .../src/sagemaker/serve/model_builder.py | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/sagemaker-serve/src/sagemaker/serve/model_builder.py b/sagemaker-serve/src/sagemaker/serve/model_builder.py index 58761105e1..6c8e19c5d6 100644 --- a/sagemaker-serve/src/sagemaker/serve/model_builder.py +++ b/sagemaker-serve/src/sagemaker/serve/model_builder.py @@ -1879,6 +1879,44 @@ def _build_for_passthrough(self) -> Model: self.secret_key = "" + # Resolve the trained model artifact URI (if any). It may arrive either + # directly as an S3 ``model_path`` or via ``s3_model_data_url``. A + # ModelTrainer / TrainingJob is normalized to ``model_path`` earlier in + # the build flow. + model_artifact_uri = None + if self.model_path and str(self.model_path).startswith("s3://"): + model_artifact_uri = self.model_path + elif isinstance(self.s3_model_data_url, str) and self.s3_model_data_url.startswith( + "s3://" + ): + model_artifact_uri = self.s3_model_data_url + + has_source_code = bool( + getattr(self, "entry_point", None) and getattr(self, "source_dir", None) + ) + + # When custom inference source code is supplied alongside a model + # artifact, this is not a pure image-only passthrough: honor the + # source code by repacking it into the model tarball (mirroring the + # classic v2 ``Model`` + ``_RepackModelStep`` behavior) so that + # ``build()`` produces a self-contained ``model.tar.gz`` (code under + # ``code/``) that is safe to ``register()``. + if has_source_code and model_artifact_uri: + if not ( + isinstance(self.s3_model_data_url, str) + and self.s3_model_data_url.startswith("s3://") + ): + # Bridge model_path -> s3_model_data_url so the repack path in + # _upload_code(repack=True) can locate the original artifact. + self.s3_model_data_url = model_artifact_uri + # Let the repacked artifact drive the container ModelDataUrl. + self.s3_upload_path = None + + if self.mode in LOCAL_MODES: + self._prepare_for_mode() + + return self._create_model() + if self.model_path and self.model_path.startswith("s3://"): self.s3_upload_path = self.model_path else: @@ -2282,6 +2320,12 @@ def _upload_code(self, key_prefix: str, repack: bool = False) -> None: script_name=os.path.basename(self.entry_point), ) + # ``script_dependencies`` (derived from SourceCode.requirements) is a + # list of paths; ``self.dependencies`` is the deprecated auto-detect + # dict ({"auto": True}) and must not be passed to repack_model, which + # iterates dependencies as filesystem paths. + repack_dependencies = self.script_dependencies or [] + logger.info( "Repacking model artifact (%s), script artifact " "(%s), and dependencies (%s) " @@ -2289,14 +2333,14 @@ def _upload_code(self, key_prefix: str, repack: bool = False) -> None: "This may take some time depending on model size...", self.s3_model_data_url, self.source_dir, - self.dependencies, + repack_dependencies, repacked_model_data, ) repack_model( inference_script=self.entry_point, source_directory=self.source_dir, - dependencies=self.dependencies, + dependencies=repack_dependencies, model_uri=self.s3_model_data_url, repacked_model_uri=repacked_model_data, sagemaker_session=self.sagemaker_session,