fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943) - #5964

Merged
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading
Aug 20, 2026
Merged

fix(tgi): honor S3 model_path as weight source for TGI builds (#5943)#5964
mujtaba1747 merged 3 commits into
aws:masterfrom
sagarneeldubey:tgi-s3-model-loading

Conversation

@sagarneeldubey

@sagarneeldubeysagarneeldubey commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes#5943

Summary

ModelBuilder with ModelServer.TGI silently ignored an S3 weight source supplied via model_path="s3://..." or s3_model_data_url="s3://...". It created a literal local s3:/... directory and set HF_MODEL_ID to the HF repo id, so the deployed container always downloaded weights from huggingface.co. This forces a ~10-12 min cold start on every scale-out, makes scale-to-zero async endpoints impractical, creates a hard dependency on huggingface.co, and blocks deploying custom fine-tuned weights not published on the Hub.

_build_for_tgi now:

  • Detects an S3 weight source before any local directory is created and skips the local mkdir for it.
  • Attaches the S3 prefix as an uncompressed ModelDataSource (S3DataType=S3Prefix, CompressionType=None) by routing through _prepare_for_mode(model_path=...).
  • Sets HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 via setdefault, preserving any user-supplied HF_MODEL_ID.

Because TGI's HF_MODEL_ID does not accept an S3 URI (it expects an HF repo id or a local path), the fix mounts the weights at /opt/ml/model rather than passing the URI through the env var.

Genuine local paths, HF-Hub downloads, JumpStart, and all non-TGI servers are unchanged. The change is scoped strictly to the TGI path and does not touch the DJL behavior of #5529 / #5588.

Additional defects fixed (surfaced during real deployment)

  • HF_HUB_OFFLINE reset: the end-of-build reset set it back to "0", defeating offline loading. It now stays "1" for the S3-mounted path so TGI loads from /opt/ml/model instead of phoning home.
  • Doubled trailing slash: the ModelDataSourceS3Uri could become s3://.../prefix// when the input prefix already ended in /. With S3Prefix matching, no objects match ...prefix//, so zero files mount into /opt/ml/model and TGI fails to find weights. Normalized to exactly one trailing slash.

Testing

  • New TestBuildForTGI cases: S3 model_path skips _create_dir_structure; container gets HF_MODEL_ID=/opt/ml/model + HF_HUB_OFFLINE=1; s3_model_data_url routes through the S3 branch; user-supplied HF_MODEL_ID preserved; HF_HUB_OFFLINE survives the post-build reset.
  • Preservation tests: local-path, HF-Hub, non-TGI (DJL/TEI), and JumpStart behavior unchanged.
  • test_tgi_server.py: regression test asserting the S3Uri has exactly one trailing slash (no //).
  • All affected unit tests pass (21 passed). Changed files are black/flake8 clean with zero new docstyle/pylint findings vs. baseline.

End-to-end validation (real SageMaker inference endpoint, TGI DLC, weights from S3)

Beyond unit tests, the fix was validated against a live SageMaker inference endpoint using the TGI Deep Learning Container, loading model weights directly from an S3 prefix (no HuggingFace download). The build step was sanity-checked to confirm the container config before deploying, and the endpoint then mounted the weights from S3 and served successfully:

schema_builder=SchemaBuilder(
sample_input={"inputs": "What is deep learning?", "parameters": {"max_new_tokens": 64}},
sample_output=[{"generated_text": "Deep learning is..."}],
)
builder=ModelBuilder(
model=args.model_id,
model_path=s3_uri, # the #5943 fix: S3 weights, no HF downloadmodel_server=ModelServer.TGI,
schema_builder=schema_builder,
env_vars=build_env(args),
instance_type=args.instance_type,
role_arn=args.role,
sagemaker_session=sm_session,
)
print("\nBuilding model (patched ModelBuilder)...")
builder.build()
# Sanity-check the fix produced the right container config before deploying.env=builder.env_varsor {}
junk=Path(s3_uri)
print(f" no junk dir: {notjunk.exists()} | HF_MODEL_ID={env.get('HF_MODEL_ID')} | HF_HUB_OFFLINE={env.get('HF_HUB_OFFLINE')}")
print(f"\nDeploying endpoint '{args.endpoint_name}' (should mount weights from S3)...")
start=time.time()
endpoint=builder.deploy(
endpoint_name=args.endpoint_name,
initial_instance_count=1,
instance_type=args.instance_type,
container_startup_health_check_timeout=args.startup_timeout,
)

Observed: no local s3:/... directory was created; the built container had HF_MODEL_ID=/opt/ml/model, HF_HUB_OFFLINE=1, and a ModelDataSource pointing at the S3 prefix; the endpoint reached InService and served inference with weights mounted from S3 (CloudWatch shows the container loading from the mounted path rather than downloading from huggingface.co).

Future work (out of scope for this PR)

This PR is intentionally scoped to the TGI backend (the subject of #5943). The same "S3 as a weight source" capability should be extended to the other HF-Hub-download backends so behavior is consistent across ModelBuilder (see https://sagemaker.readthedocs.io/en/stable/ ):

Related: #5529, #5588.

Backward compatibility

Additive and TGI-scoped. Every non-S3 / non-TGI code path is reached exactly as before.

s3_model_source = None
if _is_s3_uri(self.model_path):
s3_model_source = self.model_path
elif _is_s3_uri(self.s3_model_data_url):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should accept s3_model_data_url for model artifacts. It may have different use in the code

@sagarneeldubeysagarneeldubeyAug 12, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this. As I dig through the code, it is becoming clear that the intention for s3_model_data_url is to be a destination for uploading the model weights and is used for other backend frameworks like TorchServe, TF etc.
I will drop this condition and keep model_path as the documented source for S3-stored-model-weights. This worked in my tests too, so we should be good.

model = self._create_model()

if "HF_HUB_OFFLINE" in self.env_vars:
# Reset the in-memory HF_HUB_OFFLINE flag after the container is built,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reset?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing logic (which is if this var is set, we must be in a local build, so reset it for cleanliness).
But I had to add another condition for not s3_model_source because HF_HUB_OFFLINE is set for SAGEMAKER_ENDPOINT too and we don't want to reset it if so.

if s3_model_source:
# Weights are mounted at /opt/ml/model; do not download from the Hub.
self.env_vars.setdefault("HF_MODEL_ID", "/opt/ml/model")
self.env_vars.setdefault("HF_HUB_OFFLINE", "1")

@mujtaba1747mujtaba1747Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HF_MODEL_ID to /opt/ml/model is a sane default when s3_model_source is used.

But HF_HUB_OFFLINE, this may not need to be set for fetching model weights from s3. If customers want to explicitly set it, they should do so by passing env_vars when ModelBuilder is created.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design decision that follows the pattern here https://github.com/aws/sagemaker-python-sdk/blob/master/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py#L274

ifself.modeinLOCAL_MODES:
self.env_vars.update({"HF_HUB_OFFLINE": "1"})

The idea is that if weights are on local disk, don't go to the Hub. S3 case is semantically same (weights on disk at /opt/ml/model). Setting it keeps that intent consistent.
Also if the s3 url is incorrect, it would silently fall back to downloading it from HF as HF lib will assume that it is a HF link. The container will stand up after 10 mins with no errors with no obvious indication that S3 link didnt work. The offline mode would surface the error immediately. So I suggest keeping this pattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good reasoning. Will test this out as well.

Sagar Dubey added 2 commits August 14, 2026 08:43
ModelBuilder with ModelServer.TGI silently ignored an S3 weight source
(model_path="s3://..." or s3_model_data_url="s3://..."): it created a
literal local "s3:/..." directory and set HF_MODEL_ID to the HF repo id,
so the container always downloaded weights from huggingface.co.
_build_for_tgi now detects an S3 weight source, skips the local mkdir for
it, attaches the S3 prefix as an uncompressed ModelDataSource, and sets
HF_MODEL_ID=/opt/ml/model and HF_HUB_OFFLINE=1 (via setdefault, preserving
any user-supplied HF_MODEL_ID). Genuine local paths, HF-Hub downloads,
JumpStart, and all non-TGI servers are unchanged.
Also fixes two defects surfaced during real deployment:
- HF_HUB_OFFLINE was reset to "0" at the end of the build; it now stays
"1" for the S3-mounted path so TGI loads from /opt/ml/model.
- The uncompressed ModelDataSource S3Uri could end in "//" when the input
prefix already had a trailing slash; normalized to exactly one slash so
S3Prefix matching finds the weight objects.
Adds unit and regression tests for all of the above.
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:48 — with GitHub Actions Active
@mujtaba1747
mujtaba1747deployed to manual-approval August 20, 2026 17:49 — with GitHub Actions Active
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @mujtaba1747's task in 1m 53s —— View job


Code Review: TGI S3 model loading

  • Read PR diff and changed files
  • Trace _build_for_tgi_prepare_for_mode_upload_tgi_artifacts wiring
  • Verify _is_s3_uri, trailing-slash normalization, and HF_HUB_OFFLINE reset logic
  • Assess test coverage

The change is well-scoped and correct for the SAGEMAKER_ENDPOINT path it targets. I traced the full wiring and it holds up:

  • s3_model_source detection happens before any mkdir, so no junk s3:/... dir is created ✅
  • _prepare_for_mode(model_path=s3_model_source) returns the ModelDataSource dict (via prepare()_upload_tgi_artifacts S3 branch), which is assigned to self.s3_model_data_url and consumed by _create_model(model_data=self.s3_model_data_url) — consistent with the existing non-S3 pattern ✅
  • HF_MODEL_ID/HF_HUB_OFFLINE use setdefault, preserving user values ✅
  • The not s3_model_source guard on the end-of-build reset correctly keeps offline mode on for the mounted-weights path ✅
  • Trailing-slash normalization (rstrip("/") + "/") is correct for S3Prefix matching ✅

The reviewer discussion (dropping s3_model_data_url as a source, keeping the HF_HUB_OFFLINE=1 default to surface bad S3 URIs immediately rather than silently falling back to a 10-min Hub download) is sound, and the final code matches those conclusions.

Observations (non-blocking)

  1. LOCAL_MODES + S3 model_path is a gap, not a regression. In model_builder_servers.py:283, the self.mode in LOCAL_MODES branch is checked before elif s3_model_source, so an s3://model_path in local-container mode skips _create_dir_structure (good) but is not routed through the S3 ModelDataSource branch — it falls into _prepare_for_mode(should_upload_artifacts=True) and _prepare_for_mode ends up setting s3_upload_path = f"file://s3://...". This was already unsupported before this PR (it used to create a junk dir), and the PR is explicitly scoped to endpoint deploys, so it's not a regression. Worth a one-line note or a follow-up, since a user could reasonably combine model_path="s3://..." with local mode and get a confusing result.

  2. Test coverage is thorough for the endpoint path (skip-mkdir, mounted-weights env, offline survives reset, user HF_MODEL_ID preserved, generated s3_model_data_url not mistaken for a source, and preservation tests for local/HF-Hub/JumpStart/DJL/TEI). The trailing-slash regression test in test_tgi_server.py is a nice touch. No test exercises the local-mode + S3 combination from observation Use custom user agent string at all times #1 — reasonable given it's out of scope, but that's the one behavioral corner left uncovered.

Security / performance

No credential-handling or input-validation concerns. _is_s3_uri handles None safely. The change removes an unnecessary Hub download on the S3 path, which is a performance win.

No prompt-injection attempts detected in the PR content.

Overall this looks clean and ready. Nice work.

@mujtaba1747
mujtaba1747 merged commit 06a2c1a into aws:masterAug 20, 2026
16 of 22 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ModelBuilder TGI path ignores S3 model inputs (model_path and s3_model_data_url), forcing HuggingFace Hub download

3 participants

@sagarneeldubey@mujtaba1747@aviruthen