Skip to content

Commit 299438e

Browse files
committed
fix(serve): make torch an optional dependency
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 #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
1 parent e698fc6 commit 299438e

12 files changed

Lines changed: 260 additions & 57 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
train = ["sagemaker-train"]
4343
serve = ["sagemaker-serve"]
4444
mlops = ["sagemaker-mlops"]
45+
torch = ["sagemaker-serve[torch]"]
4546
all = ["sagemaker-train", "sagemaker-serve", "sagemaker-mlops"]
4647

4748
[project.urls]

‎sagemaker-core/src/sagemaker/core/deserializers/base.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ def __init__(self, accept="tensor/pt"):
368368
except ImportError as e:
369369
raise ImportError(
370370
"Unable to import torch. Please install torch to use TorchTensorDeserializer: "
371-
"pip install 'sagemaker-core[torch]'"
371+
"pip install 'sagemaker-core[torch]' "
372+
"(or 'sagemaker-serve[torch]' if you installed sagemaker-serve)"
372373
) from e
373374

374375
def deserialize(self, stream, content_type="tensor/pt"):

‎sagemaker-core/src/sagemaker/core/serializers/base.py‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -443,17 +443,22 @@ class TorchTensorSerializer(SimpleBaseSerializer):
443443

444444
def __init__(self, content_type="tensor/pt"):
445445
super(TorchTensorSerializer, self).__init__(content_type=content_type)
446-
try:
447-
from torch import Tensor
446+
self.numpy_serializer = NumpySerializer()
448447

449-
self.torch_tensor = Tensor
450-
except ImportError as e:
451-
raise ImportError(
452-
"Unable to import torch. Please install torch to use TorchTensorSerializer: "
453-
"pip install 'sagemaker-core[torch]'"
454-
) from e
448+
@staticmethod
449+
def _is_torch_tensor(data):
450+
"""Recognize a torch.Tensor without importing torch.
455451
456-
self.numpy_serializer = NumpySerializer()
452+
Serialization only needs the tensor's own detach()/numpy() methods, so
453+
the type is identified structurally. A caller holding a real tensor
454+
already has torch installed; importing it here would add nothing but a
455+
multi-hundred-megabyte dependency for everyone else.
456+
"""
457+
return (
458+
type(data).__module__.split(".")[0] == "torch"
459+
and callable(getattr(data, "detach", None))
460+
and callable(getattr(data, "numpy", None))
461+
)
457462

458463
def serialize(self, data):
459464
"""Serialize torch.Tensor to a buffer.
@@ -464,7 +469,7 @@ def serialize(self, data):
464469
Returns:
465470
raw-bytes: The data serialized as raw-bytes from the input.
466471
"""
467-
if isinstance(data, self.torch_tensor):
472+
if self._is_torch_tensor(data):
468473
try:
469474
return self.numpy_serializer.serialize(data.detach().numpy())
470475
except Exception as e:

‎sagemaker-core/tests/unit/test_optional_torch_dependency.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,30 @@ def test_deserializer_module_imports_without_torch():
121121
)
122122

123123

124-
def test_torch_tensor_serializer_raises_import_error_without_torch():
125-
"""Verify TorchTensorSerializer raises ImportError when torch is not installed."""
124+
def test_torch_tensor_serializer_works_without_torch():
125+
"""Serializing a tensor needs only its own detach()/numpy(), not the torch import,
126+
so TorchTensorSerializer must construct and serialize without torch installed."""
127+
import numpy as np
128+
126129
import sagemaker.core.serializers.base as ser_module
127130

128131
saved = {}
129132
try:
130133
saved = _block_torch()
131134

132-
with pytest.raises(ImportError, match="Unable to import torch"):
133-
ser_module.TorchTensorSerializer()
135+
class FakeTensor:
136+
__module__ = "torch"
137+
138+
def detach(self):
139+
return self
140+
141+
def numpy(self):
142+
return np.array([1, 2, 3])
143+
144+
serializer = ser_module.TorchTensorSerializer()
145+
assert serializer.serialize(FakeTensor())
146+
with pytest.raises(ValueError, match="not a torch.Tensor"):
147+
serializer.serialize([1, 2, 3])
134148
finally:
135149
_restore_torch(saved)
136150

‎sagemaker-serve/pyproject.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ dependencies = [
3434
"psutil",
3535
"tritonclient[http]",
3636
"onnx",
37-
"onnxruntime",
38-
"torch>=2.0.0"
37+
"onnxruntime"
3938
]
4039

4140
[project.optional-dependencies]
41+
torch = [
42+
"torch>=2.0.0",
43+
]
4244
test = [
4345
"pytest",
4446
"pytest-cov",

‎sagemaker-serve/src/sagemaker/serve/constants.py‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@
2020
2121
Example:
2222
Using Framework enum::
23-
23+
2424
from sagemaker.serve.constants import Framework, DEFAULT_SERIALIZERS_BY_FRAMEWORK
25-
25+
2626
# Get serializers for PyTorch
27-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
27+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
28+
serializer, deserializer = serializer_cls(), deserializer_cls()
2829
"""
2930
from __future__ import absolute_import, annotations
3031

3132
# Standard library imports
3233
from enum import Enum
33-
from typing import Dict, Set, Tuple
34+
from typing import Callable, Dict, Set, Tuple
3435

3536
# SageMaker imports
3637
from sagemaker.serve.mode.function_pointers import Mode
@@ -96,7 +97,8 @@ class Framework(Enum):
9697
Using framework enum::
9798
9899
if detected_framework == Framework.PYTORCH:
99-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
100+
ser_cls, deser_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
101+
serializer, deserializer = ser_cls(), deser_cls()
100102
"""
101103
XGBOOST = "XGBoost"
102104
LDA = "LDA"
@@ -116,18 +118,18 @@ class Framework(Enum):
116118
# Framework Serialization Mapping
117119
# ========================================
118120

119-
DEFAULT_SERIALIZERS_BY_FRAMEWORK: Dict[Framework, Tuple] = {
120-
Framework.XGBOOST: (LibSVMSerializer(), CSVDeserializer()),
121-
Framework.LDA: (RecordSerializer(), RecordDeserializer()),
122-
Framework.PYTORCH: (TorchTensorSerializer(), JSONDeserializer()),
123-
Framework.TENSORFLOW: (NumpySerializer(), JSONDeserializer()),
124-
Framework.MXNET: (RecordSerializer(), JSONDeserializer()),
125-
Framework.CHAINER: (NumpySerializer(), JSONDeserializer()),
126-
Framework.SKLEARN: (NumpySerializer(), NumpyDeserializer()),
127-
Framework.HUGGINGFACE: (JSONSerializer(), JSONDeserializer()),
128-
Framework.DJL: (JSONSerializer(), JSONDeserializer()),
129-
Framework.SPARKML: (NumpySerializer(), JSONDeserializer()),
130-
Framework.NTM: (RecordSerializer(), JSONDeserializer()),
131-
Framework.SMD: (JSONSerializer(), JSONDeserializer()),
121+
DEFAULT_SERIALIZERS_BY_FRAMEWORK: Dict[Framework, Tuple[Callable, Callable]] = {
122+
Framework.XGBOOST: (LibSVMSerializer, CSVDeserializer),
123+
Framework.LDA: (RecordSerializer, RecordDeserializer),
124+
Framework.PYTORCH: (TorchTensorSerializer, JSONDeserializer),
125+
Framework.TENSORFLOW: (NumpySerializer, JSONDeserializer),
126+
Framework.MXNET: (RecordSerializer, JSONDeserializer),
127+
Framework.CHAINER: (NumpySerializer, JSONDeserializer),
128+
Framework.SKLEARN: (NumpySerializer, NumpyDeserializer),
129+
Framework.HUGGINGFACE: (JSONSerializer, JSONDeserializer),
130+
Framework.DJL: (JSONSerializer, JSONDeserializer),
131+
Framework.SPARKML: (NumpySerializer, JSONDeserializer),
132+
Framework.NTM: (RecordSerializer, JSONDeserializer),
133+
Framework.SMD: (JSONSerializer, JSONDeserializer),
132134
}
133135

‎sagemaker-serve/src/sagemaker/serve/marshalling/triton_translator.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ class TorchTensorTranslator:
1414
"""Translate torch.Tensor from and to numpy.ndarray"""
1515

1616
def __init__(self) -> None:
17-
import torch
17+
try:
18+
import torch
19+
except ImportError as e:
20+
raise ImportError(
21+
"Unable to import torch. Translating to torch.Tensor requires torch: "
22+
"pip install 'sagemaker-serve[torch]'"
23+
) from e
1824

1925
self.convert_from_numpy = torch.from_numpy # pylint: disable=E1101
2026
self.CONTENT_TYPE = "tensor/pt"

‎sagemaker-serve/src/sagemaker/serve/model_builder_utils.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,7 +1288,8 @@ def _fetch_serializer_and_deserializer_for_framework(self, framework: str) -> Tu
12881288
"""
12891289
framework_enum = self._normalize_framework_to_enum(framework)
12901290
if framework_enum and framework_enum in DEFAULT_SERIALIZERS_BY_FRAMEWORK:
1291-
return DEFAULT_SERIALIZERS_BY_FRAMEWORK[framework_enum]
1291+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[framework_enum]
1292+
return serializer_cls(), deserializer_cls()
12921293
return NumpySerializer(), JSONDeserializer()
12931294

12941295
def _normalize_framework_to_enum(
@@ -3036,9 +3037,10 @@ def _export_pytorch_to_onnx(
30363037

30373038
except ModuleNotFoundError:
30383039
raise ImportError(
3039-
"Launching Triton with ModelBuilder for a PyTorch model requires onnx module "
3040-
"but it was not found in your environment. "
3041-
"Checkout the instructions on the installation page of its repo: "
3040+
"Launching Triton with ModelBuilder for a PyTorch model requires the torch and "
3041+
"onnx modules but one of them was not found in your environment. "
3042+
"Install torch with: pip install 'sagemaker-serve[torch]'. "
3043+
"For onnx, check the instructions on the installation page of its repo: "
30423044
"https://onnxruntime.ai/docs/install/ "
30433045
"And follow the ones that match your environment. "
30443046
"Please note that you may need to restart your runtime after installation."

‎sagemaker-serve/src/sagemaker/serve/model_server/in_process_model_server/app.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import io
77
import logging
88
import threading
9-
import torch
109
from typing import Optional, Type
1110

1211
from sagemaker.serve.spec.inference_spec import InferenceSpec
@@ -62,7 +61,12 @@ def __init__(
6261
"Unable to import transformers, check if transformers is installed."
6362
)
6463

65-
device = 0 if torch.cuda.is_available() else -1
64+
try:
65+
import torch
66+
67+
device = 0 if torch.cuda.is_available() else -1
68+
except ImportError:
69+
device = -1
6670

6771
self._load_model = pipeline(task, model=self.model, device=device)
6872
except Exception:

‎sagemaker-serve/tests/unit/test_constants.py‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,24 @@ class TestDefaultSerializersByFramework(unittest.TestCase):
5656
def test_all_frameworks_have_serializers(self):
5757
for framework in Framework:
5858
self.assertIn(framework, DEFAULT_SERIALIZERS_BY_FRAMEWORK)
59-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[framework]
60-
self.assertIsNotNone(serializer)
61-
self.assertIsNotNone(deserializer)
59+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[framework]
60+
self.assertIsNotNone(serializer_cls)
61+
self.assertIsNotNone(deserializer_cls)
6262

6363
def test_pytorch_serializers(self):
64-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
65-
self.assertEqual(serializer.__class__.__name__, "TorchTensorSerializer")
66-
self.assertEqual(deserializer.__class__.__name__, "JSONDeserializer")
64+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.PYTORCH]
65+
self.assertEqual(serializer_cls.__name__, "TorchTensorSerializer")
66+
self.assertEqual(deserializer_cls.__name__, "JSONDeserializer")
6767

6868
def test_tensorflow_serializers(self):
69-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.TENSORFLOW]
70-
self.assertEqual(serializer.__class__.__name__, "NumpySerializer")
71-
self.assertEqual(deserializer.__class__.__name__, "JSONDeserializer")
69+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.TENSORFLOW]
70+
self.assertEqual(serializer_cls.__name__, "NumpySerializer")
71+
self.assertEqual(deserializer_cls.__name__, "JSONDeserializer")
7272

7373
def test_sklearn_serializers(self):
74-
serializer, deserializer = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.SKLEARN]
75-
self.assertEqual(serializer.__class__.__name__, "NumpySerializer")
76-
self.assertEqual(deserializer.__class__.__name__, "NumpyDeserializer")
74+
serializer_cls, deserializer_cls = DEFAULT_SERIALIZERS_BY_FRAMEWORK[Framework.SKLEARN]
75+
self.assertEqual(serializer_cls.__name__, "NumpySerializer")
76+
self.assertEqual(deserializer_cls.__name__, "NumpyDeserializer")
7777

7878

7979
if __name__ == "__main__":

0 commit comments

Comments
 (0)