Skip to content

fix(serve): make torch an optional dependency - #6166

Closed
goelakash wants to merge 2 commits into
aws:masterfrom
goelakash:fix-optional-torch-serve
Closed

goelakash wants to merge 2 commits into
aws:masterfrom
goelakash:fix-optional-torch-serve

Conversation

@goelakash

Copy link
Copy Markdown
Contributor

Fixes #5531

sagemaker-serve lists torch>=2.0.0 as a required dependency, so any install of sagemaker, sagemaker-serve, or sagemaker-mlops pulls torch (and on Linux, the CUDA/cuDNN/NCCL stack) even for API-only use. sagemaker-core already treats torch as an extra.

The blocker was DEFAULT_SERIALIZERS_BY_FRAMEWORK in serve/constants.py, which instantiated TorchTensorSerializer() at module scope. That runs from torch import Tensor on any import sagemaker.serve. Every other torch reference in the package is already lazy.

Changes

  • Store serializer/deserializer classes in DEFAULT_SERIALIZERS_BY_FRAMEWORK; instantiate at lookup in _fetch_serializer_and_deserializer_for_framework.
  • Move torch>=2.0.0 to a torch extra, matching sagemaker-core.
  • Update the two docstring examples and the affected unit tests.
  • Add tests/unit/test_optional_torch_dependency.py, mirroring the existing sagemaker-core subprocess pattern.

Installing with the torch extra is unchanged. TorchTensorSerializer() still raises the same ImportError if torch is missing when actually used.

Testing

  • tests/unit in a venv with and without torch: no new failures vs. master (53 pre-existing failures in both).
  • New tests fail on master and pass with this change.
  • Verified in a container with no torch installed: from sagemaker.serve import ModelBuilder and import sagemaker.mlops both succeed.
  • Image size for all four packages on Amazon Linux 2023: 1.45 GB -> 536 MB.

Installing sagemaker-serve (or the umbrella sagemaker, which depends on
it) pulled torch, and on Linux the ~2.9 GB CUDA/cuDNN/NCCL closure, even
for API-only use. sagemaker-core already declared torch an extra;
sagemaker-serve declared it required, so serve users paid regardless.

The import-time blocker was DEFAULT_SERIALIZERS_BY_FRAMEWORK in
serve/constants.py, which instantiated TorchTensorSerializer() at module
scope, running `from torch import Tensor` on any `import sagemaker.serve`.

Changes:

- Store serializer/deserializer classes (not instances) in
  DEFAULT_SERIALIZERS_BY_FRAMEWORK; instantiate on lookup. Removes the
  import-time torch dependency.
- Move torch>=2.0.0 to a `torch` extra in sagemaker-serve, matching
  sagemaker-core. Add a `torch` extra to the umbrella sagemaker package
  (sagemaker-serve[torch]) so `pip install sagemaker[torch]` works.
- Duck-type the tensor check in TorchTensorSerializer: serialization only
  needs the object's own detach()/numpy(), so it no longer imports torch
  at all and works whenever the caller holds a tensor.
- Make the in-process model server's torch import lazy with a CPU
  fallback, so importing it no longer requires torch.
- Fix the error messages that told serve users to install the wrong
  package: TorchTensorDeserializer, the Triton translator, and the ONNX
  export path now name sagemaker-serve[torch].
- Update and add tests asserting the whole surface imports and serializes
  without torch, and that the paths that genuinely construct torch
  objects (deserializer, Triton translator) still raise a clear error
  naming the extra.

Fixes aws#5531

Breaking change: users who installed plain sagemaker-serve and relied on
a torch code path (deserializing to tensor/pt, or ONNX/Triton export of a
PyTorch model) must now install sagemaker-serve[torch]. Both paths
inherently require a torch object the caller supplies, so torch is
already present in practice; a GitHub-wide search found no external
callers of these paths that do not already import torch.

---

X-AI-Prompt: address the reviewer concern on whether making torch optional breaks existing customers; find ways to reduce the torch dependency and update pyproject and docs; consolidate all changes in the goelakash93 fork for a possible major version bump.
X-AI-Tool: Claude Code
@goelakash
goelakash force-pushed the fix-optional-torch-serve branch from 38b75f5 to 299438e Compare August 7, 2026 20:49
@goelakash
goelakash deployed to manual-approval August 7, 2026 20:49 — with GitHub Actions Active
jam-jee added a commit that referenced this pull request Aug 17, 2026
The reviewer aborted on every fork PR with "Actor does not have write
permissions to the repository" (e.g. run 31217496013 on #6166), so it
only ever ran for collaborators.

claude-code-action checks that the PR author has write access before
doing anything. That default protects its normal @claude usage, where a
read-only user's comment becomes the prompt. It does not apply here:
pull_request_target always runs the base-branch copy of this workflow,
so the prompt is fixed by maintainers and a fork cannot supply it.

Set allowed_non_write_users so the review actually runs, and harden the
prompt-injection surface it exposes (untrusted diff/PR text entering
context):

- deny Read on /proc, /sys, ~/.aws, the Actions _temp dir and .git/config
  so an injected instruction cannot use the review comment as a
  secret-exfiltration channel
- instruct the model to treat all contributor-authored content as data,
  never as instructions, and to report attempted injection

Fork PRs continue to require maintainer approval via the manual-approval
environment, Bash/Write/Edit remain unavailable, and the assumed role is
still limited to bedrock:InvokeModel on a single inference profile.
@goelakash
goelakash deployed to manual-approval August 18, 2026 20:42 — with GitHub Actions Active
mohamedzeidan2021 pushed a commit to mohamedzeidan2021/sagemaker-python-sdk that referenced this pull request Oct 23, 2026
aws#6512 made the sagemaker-serve serving backends extras, but four gaps kept
torch from being genuinely optional (aws#5531). All four were diagnosed first in
aws#6166.

Selecting the PyTorch default serializer still required torch.
``TorchTensorSerializer.__init__`` imported torch, so the lazy registry only
deferred the failure to ``_fetch_serializer_and_deserializer_for_framework``.
A client deploying a prebuilt PyTorch container detects the framework from the
image URI and never touches a tensor, so it hit an ImportError for a dependency
it had no use for. The type check now consults ``sys.modules``: a torch.Tensor
cannot exist unless torch has already been imported, so the check stays exact
without triggering an import. ``torch_tensor`` becomes a property, keeping the
attribute and its ImportError for callers that want the type itself.

Membership tests built the pair they were asking about. The registry inherited
``Mapping.__contains__``, which answers by calling ``__getitem__`` - defeating
the laziness and raising a backend's ImportError out of an ``in`` test rather
than returning True.

``TorchTensorTranslator`` raised a bare ModuleNotFoundError, leaving the caller
to guess which extra to install.

``pip install sagemaker[torch]`` did not work. The top-level package now passes
sagemaker-serve's backend extras through. ``all`` is unchanged, so no existing
install starts pulling torch.

Co-authored-by: goelakash <goelakash93@gmail.com>
@mohamedzeidan2021

Copy link
Copy Markdown
Collaborator

Thanks for this @goelakash, and apologies for how long it sat. Doing a proper deep dive on it now — here's where things landed.

Your diagnosis was right, and it was the correct read of the problem: DEFAULT_SERIALIZERS_BY_FRAMEWORK instantiating TorchTensorSerializer() at module scope was the actual blocker, and every other torch reference in the package was already lazy.

While this PR was waiting, #6512 landed (Oct 21) and independently fixed the same root cause with a wider scope — it also made onnx, onnxruntime, tritonclient[http] and mlflow optional, and used a _LazySerializerRegistry(Mapping) so that DEFAULT_SERIALIZERS_BY_FRAMEWORK[framework] keeps returning instances. That avoids the API change of handing callers classes, which is what forced the churn in test_constants.py and test_model_builder_utils_new.py here. That's purely an accident of timing and review latency on our side, not a judgment on this PR — and it's the reason all four of the current conflicts are in regions where #6512's version is the broader fix.

So rather than ask you to resolve conflicts into a change that would mostly be reverted, I've opened #6531 to carry forward the parts of this PR that #6512 did not cover, with you credited as co-author on the commit:

  1. TorchTensorSerializer.__init__ still imports torch. The lazy registry only defers the failure to _fetch_serializer_and_deserializer_for_framework, so a torch-free client deploying a prebuilt PyTorch container (framework detected from the image URI, no tensor ever handled) still broke. This was the most valuable catch in your PR and fix: make sagemaker-serve test tooling and serving backends optional #6512 missed it.
  2. TorchTensorTranslator's bare import torch — your friendly ImportError naming the extra.
  3. pip install sagemaker[torch] — your root pyproject.toml passthrough extra, extended to onnx/triton/mlflow for symmetry.
  4. Plus one defect in fix: make sagemaker-serve test tooling and serving backends optional #6512 itself that your PR's structure would have avoided: the registry inherited Mapping.__contains__, which answers via __getitem__, so framework in DEFAULT_SERIALIZERS_BY_FRAMEWORK instantiated the pair as a side effect and raised ImportError out of a membership test.

One change I made to your approach on (1): instead of the structural check

type(data).__module__.split(".")[0] == "torch" and callable(getattr(data, "detach", None)) ...

#6531 uses sys.modules.get("torch") and then a real isinstance. A torch.Tensor can't exist unless torch is already imported, so that's exactly as strict as the original isinstance — no widening to objects that merely look like tensors — while still never triggering an import.

Closing this in favor of #6531. Thank you for the diagnosis, the measurements (1.45 GB → 536 MB is what made the case concrete), and for your patience with the delay.

mohamedzeidan2021 added a commit that referenced this pull request Oct 23, 2026
#6512 made the sagemaker-serve serving backends extras, but four gaps kept
torch from being genuinely optional (#5531). All four were diagnosed first in
#6166.

Selecting the PyTorch default serializer still required torch.
``TorchTensorSerializer.__init__`` imported torch, so the lazy registry only
deferred the failure to ``_fetch_serializer_and_deserializer_for_framework``.
A client deploying a prebuilt PyTorch container detects the framework from the
image URI and never touches a tensor, so it hit an ImportError for a dependency
it had no use for. The type check now consults ``sys.modules``: a torch.Tensor
cannot exist unless torch has already been imported, so the check stays exact
without triggering an import. ``torch_tensor`` becomes a property, keeping the
attribute and its ImportError for callers that want the type itself.

Membership tests built the pair they were asking about. The registry inherited
``Mapping.__contains__``, which answers by calling ``__getitem__`` - defeating
the laziness and raising a backend's ImportError out of an ``in`` test rather
than returning True.

``TorchTensorTranslator`` raised a bare ModuleNotFoundError, leaving the caller
to guess which extra to install.

``pip install sagemaker[torch]`` did not work. The top-level package now passes
sagemaker-serve's backend extras through. ``all`` is unchanged, so no existing
install starts pulling torch.

Co-authored-by: Mohamed Zeidan <zeidmo@amazon.com>
Co-authored-by: goelakash <goelakash93@gmail.com>

This branch is waiting to be deployed

1 waiting deployment
manual-approval — 7455f0b1 Waiting Oct 12, 2026 by mohamedzeidan2021 via wait-for-approval #1983
Sign up for free to 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.

Make torch an optional dependency / extra

2 participants