modal 0.66.17__py3-none-any.whl → 0.66.44__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/_container_entrypoint.py +5 -342
- modal/_runtime/container_io_manager.py +6 -14
- modal/_runtime/user_code_imports.py +361 -0
- modal/_utils/function_utils.py +28 -8
- modal/_utils/grpc_testing.py +33 -26
- modal/app.py +13 -46
- modal/cli/import_refs.py +4 -38
- modal/client.pyi +2 -2
- modal/cls.py +26 -19
- modal/cls.pyi +4 -4
- modal/dict.py +0 -6
- modal/dict.pyi +0 -4
- modal/experimental.py +0 -3
- modal/functions.py +42 -38
- modal/functions.pyi +9 -13
- modal/gpu.py +8 -6
- modal/image.py +141 -7
- modal/image.pyi +34 -4
- modal/io_streams.py +40 -33
- modal/io_streams.pyi +13 -13
- modal/mount.py +5 -2
- modal/network_file_system.py +0 -28
- modal/network_file_system.pyi +0 -14
- modal/partial_function.py +12 -2
- modal/queue.py +0 -6
- modal/queue.pyi +0 -4
- modal/sandbox.py +1 -1
- modal/volume.py +0 -22
- modal/volume.pyi +0 -9
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/METADATA +1 -2
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/RECORD +43 -42
- modal_proto/api.proto +3 -20
- modal_proto/api_grpc.py +0 -16
- modal_proto/api_pb2.py +389 -413
- modal_proto/api_pb2.pyi +12 -58
- modal_proto/api_pb2_grpc.py +0 -33
- modal_proto/api_pb2_grpc.pyi +0 -10
- modal_proto/modal_api_grpc.py +0 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/LICENSE +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/WHEEL +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/entry_points.txt +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/top_level.txt +0 -0
modal/cli/import_refs.py
CHANGED
@@ -19,7 +19,7 @@ from rich.console import Console
|
|
19
19
|
from rich.markdown import Markdown
|
20
20
|
|
21
21
|
from modal.app import App, LocalEntrypoint
|
22
|
-
from modal.exception import InvalidError, _CliUserExecutionError
|
22
|
+
from modal.exception import InvalidError, _CliUserExecutionError
|
23
23
|
from modal.functions import Function
|
24
24
|
|
25
25
|
|
@@ -79,7 +79,7 @@ def import_file_or_module(file_or_module: str):
|
|
79
79
|
return module
|
80
80
|
|
81
81
|
|
82
|
-
def get_by_object_path(obj: Any, obj_path:
|
82
|
+
def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
|
83
83
|
# Try to evaluate a `.`-delimited object path in a Modal context
|
84
84
|
# With the caveat that some object names can actually have `.` in their name (lifecycled methods' tags)
|
85
85
|
|
@@ -107,35 +107,6 @@ def get_by_object_path(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
|
107
107
|
return obj
|
108
108
|
|
109
109
|
|
110
|
-
def get_by_object_path_try_possible_app_names(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
111
|
-
"""This just exists as a dumb workaround to support both "stub" and "app" """
|
112
|
-
|
113
|
-
if obj_path:
|
114
|
-
return get_by_object_path(obj, obj_path)
|
115
|
-
else:
|
116
|
-
app = get_by_object_path(obj, DEFAULT_APP_NAME)
|
117
|
-
stub = get_by_object_path(obj, "stub")
|
118
|
-
if isinstance(app, App):
|
119
|
-
return app
|
120
|
-
elif app is not None and isinstance(stub, App):
|
121
|
-
deprecation_warning(
|
122
|
-
(2024, 4, 20),
|
123
|
-
"The symbol `app` is present at the module level but it's not a Modal app."
|
124
|
-
" We will use `stub` instead, but this will not work in future Modal versions."
|
125
|
-
" Suggestion: change the name of `app` to something else.",
|
126
|
-
)
|
127
|
-
return stub
|
128
|
-
elif isinstance(stub, App):
|
129
|
-
deprecation_warning(
|
130
|
-
(2024, 5, 1),
|
131
|
-
"The symbol `app` is not present but `stub` is. This will not work in future"
|
132
|
-
" Modal versions. Suggestion: change the name of `stub` to `app`.",
|
133
|
-
)
|
134
|
-
return stub
|
135
|
-
else:
|
136
|
-
return None
|
137
|
-
|
138
|
-
|
139
110
|
def _infer_function_or_help(
|
140
111
|
app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
|
141
112
|
) -> Union[Function, LocalEntrypoint]:
|
@@ -210,7 +181,7 @@ def import_app(app_ref: str) -> App:
|
|
210
181
|
import_ref = parse_import_ref(app_ref)
|
211
182
|
|
212
183
|
module = import_file_or_module(import_ref.file_or_module)
|
213
|
-
app =
|
184
|
+
app = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
214
185
|
|
215
186
|
if app is None:
|
216
187
|
_show_no_auto_detectable_app(import_ref)
|
@@ -258,7 +229,7 @@ def import_function(
|
|
258
229
|
import_ref = parse_import_ref(func_ref)
|
259
230
|
|
260
231
|
module = import_file_or_module(import_ref.file_or_module)
|
261
|
-
app_or_function =
|
232
|
+
app_or_function = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
262
233
|
|
263
234
|
if app_or_function is None:
|
264
235
|
_show_function_ref_help(import_ref, base_cmd)
|
@@ -279,8 +250,3 @@ def import_function(
|
|
279
250
|
return app_or_function
|
280
251
|
else:
|
281
252
|
raise click.UsageError(f"{app_or_function} is not a Modal entity (should be an App or Function)")
|
282
|
-
|
283
|
-
|
284
|
-
# For backwards compatibility - delete soon
|
285
|
-
# We use it in our internal intergration tests
|
286
|
-
import_stub = import_app
|
modal/client.pyi
CHANGED
@@ -31,7 +31,7 @@ class _Client:
|
|
31
31
|
server_url: str,
|
32
32
|
client_type: int,
|
33
33
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
34
|
-
version: str = "0.66.
|
34
|
+
version: str = "0.66.44",
|
35
35
|
): ...
|
36
36
|
def is_closed(self) -> bool: ...
|
37
37
|
@property
|
@@ -90,7 +90,7 @@ class Client:
|
|
90
90
|
server_url: str,
|
91
91
|
client_type: int,
|
92
92
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
93
|
-
version: str = "0.66.
|
93
|
+
version: str = "0.66.44",
|
94
94
|
): ...
|
95
95
|
def is_closed(self) -> bool: ...
|
96
96
|
@property
|
modal/cls.py
CHANGED
@@ -113,7 +113,7 @@ class _Obj:
|
|
113
113
|
method = self._instance_service_function._bind_instance_method(class_bound_method)
|
114
114
|
self._method_functions[method_name] = method
|
115
115
|
else:
|
116
|
-
# <v0.63 classes - bind each individual method to the new parameters
|
116
|
+
# looked up <v0.63 classes - bind each individual method to the new parameters
|
117
117
|
self._instance_service_function = None
|
118
118
|
for method_name, class_bound_method in classbound_methods.items():
|
119
119
|
method = class_bound_method._bind_parameters(self, from_other_workspace, options, args, kwargs)
|
@@ -125,12 +125,14 @@ class _Obj:
|
|
125
125
|
self._user_cls = user_cls
|
126
126
|
self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
|
127
127
|
|
128
|
-
def
|
128
|
+
def _new_user_cls_instance(self):
|
129
129
|
args, kwargs = self._construction_args
|
130
130
|
if not _use_annotation_parameters(self._user_cls):
|
131
131
|
# TODO(elias): deprecate this code path eventually
|
132
132
|
user_cls_instance = self._user_cls(*args, **kwargs)
|
133
133
|
else:
|
134
|
+
# ignore constructor (assumes there is no custom constructor,
|
135
|
+
# which is guaranteed by _use_annotation_parameters)
|
134
136
|
# set the attributes on the class corresponding to annotations
|
135
137
|
# with = parameter() specifications
|
136
138
|
sig = _get_class_constructor_signature(self._user_cls)
|
@@ -139,6 +141,7 @@ class _Obj:
|
|
139
141
|
user_cls_instance = self._user_cls.__new__(self._user_cls) # new instance without running __init__
|
140
142
|
user_cls_instance.__dict__.update(bound_vars.arguments)
|
141
143
|
|
144
|
+
# TODO: always use Obj instances instead of making modifications to user cls
|
142
145
|
user_cls_instance._modal_functions = self._method_functions # Needed for PartialFunction.__get__
|
143
146
|
return user_cls_instance
|
144
147
|
|
@@ -163,10 +166,12 @@ class _Obj:
|
|
163
166
|
)
|
164
167
|
await self._instance_service_function.keep_warm(warm_pool_size)
|
165
168
|
|
166
|
-
def
|
167
|
-
"""
|
169
|
+
def _cached_user_cls_instance(self):
|
170
|
+
"""Get or construct the local object
|
171
|
+
|
172
|
+
Used for .local() calls and getting attributes of classes"""
|
168
173
|
if not self._user_cls_instance:
|
169
|
-
self._user_cls_instance = self.
|
174
|
+
self._user_cls_instance = self._new_user_cls_instance() # Instantiate object
|
170
175
|
|
171
176
|
return self._user_cls_instance
|
172
177
|
|
@@ -196,7 +201,7 @@ class _Obj:
|
|
196
201
|
@synchronizer.nowrap
|
197
202
|
async def aenter(self):
|
198
203
|
if not self.entered:
|
199
|
-
user_cls_instance = self.
|
204
|
+
user_cls_instance = self._cached_user_cls_instance()
|
200
205
|
if hasattr(user_cls_instance, "__aenter__"):
|
201
206
|
await user_cls_instance.__aenter__()
|
202
207
|
elif hasattr(user_cls_instance, "__enter__"):
|
@@ -205,20 +210,22 @@ class _Obj:
|
|
205
210
|
|
206
211
|
def __getattr__(self, k):
|
207
212
|
if k in self._method_functions:
|
208
|
-
#
|
209
|
-
#
|
210
|
-
#
|
213
|
+
# If we know the user is accessing a *method* and not another attribute,
|
214
|
+
# we don't have to create an instance of the user class yet.
|
215
|
+
# This is because it might just be a call to `.remote()` on it which
|
216
|
+
# doesn't require a local instance.
|
217
|
+
# As long as we have the service function or params, we can do remote calls
|
218
|
+
# without calling the constructor of the class in the calling context.
|
211
219
|
return self._method_functions[k]
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
raise AttributeError(k)
|
220
|
+
|
221
|
+
# if it's *not* a method, it *might* be an attribute of the class,
|
222
|
+
# so we construct it and proxy the attribute
|
223
|
+
# TODO: To get lazy loading (from_name) of classes to work, we need to avoid
|
224
|
+
# this path, otherwise local initialization will happen regardless if user
|
225
|
+
# only runs .remote(), since we don't know methods for the class until we
|
226
|
+
# load it
|
227
|
+
user_cls_instance = self._cached_user_cls_instance()
|
228
|
+
return getattr(user_cls_instance, k)
|
222
229
|
|
223
230
|
|
224
231
|
Obj = synchronize_api(_Obj)
|
modal/cls.pyi
CHANGED
@@ -37,9 +37,9 @@ class _Obj:
|
|
37
37
|
args,
|
38
38
|
kwargs,
|
39
39
|
): ...
|
40
|
-
def
|
40
|
+
def _new_user_cls_instance(self): ...
|
41
41
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
42
|
-
def
|
42
|
+
def _cached_user_cls_instance(self): ...
|
43
43
|
def enter(self): ...
|
44
44
|
@property
|
45
45
|
def entered(self): ...
|
@@ -66,7 +66,7 @@ class Obj:
|
|
66
66
|
kwargs,
|
67
67
|
): ...
|
68
68
|
def _uses_common_service_function(self): ...
|
69
|
-
def
|
69
|
+
def _new_user_cls_instance(self): ...
|
70
70
|
|
71
71
|
class __keep_warm_spec(typing_extensions.Protocol):
|
72
72
|
def __call__(self, warm_pool_size: int) -> None: ...
|
@@ -74,7 +74,7 @@ class Obj:
|
|
74
74
|
|
75
75
|
keep_warm: __keep_warm_spec
|
76
76
|
|
77
|
-
def
|
77
|
+
def _cached_user_cls_instance(self): ...
|
78
78
|
def enter(self): ...
|
79
79
|
@property
|
80
80
|
def entered(self): ...
|
modal/dict.py
CHANGED
@@ -143,12 +143,6 @@ class _Dict(_Object, type_prefix="di"):
|
|
143
143
|
|
144
144
|
return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
|
145
145
|
|
146
|
-
@staticmethod
|
147
|
-
def persisted(label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None):
|
148
|
-
"""mdmd:hidden"""
|
149
|
-
message = "`Dict.persisted` is deprecated. Please use `Dict.from_name(name, create_if_missing=True)` instead."
|
150
|
-
deprecation_error((2024, 3, 1), message)
|
151
|
-
|
152
146
|
@staticmethod
|
153
147
|
async def lookup(
|
154
148
|
label: str,
|
modal/dict.pyi
CHANGED
@@ -27,8 +27,6 @@ class _Dict(modal.object._Object):
|
|
27
27
|
create_if_missing: bool = False,
|
28
28
|
) -> _Dict: ...
|
29
29
|
@staticmethod
|
30
|
-
def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
|
31
|
-
@staticmethod
|
32
30
|
async def lookup(
|
33
31
|
label: str,
|
34
32
|
data: typing.Optional[dict] = None,
|
@@ -79,8 +77,6 @@ class Dict(modal.object.Object):
|
|
79
77
|
environment_name: typing.Optional[str] = None,
|
80
78
|
create_if_missing: bool = False,
|
81
79
|
) -> Dict: ...
|
82
|
-
@staticmethod
|
83
|
-
def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
|
84
80
|
|
85
81
|
class __lookup_spec(typing_extensions.Protocol):
|
86
82
|
def __call__(
|
modal/experimental.py
CHANGED
@@ -17,7 +17,6 @@ from .partial_function import _PartialFunction, _PartialFunctionFlags
|
|
17
17
|
def stop_fetching_inputs():
|
18
18
|
"""Don't fetch any more inputs from the server, after the current one.
|
19
19
|
The container will exit gracefully after the current input is processed."""
|
20
|
-
|
21
20
|
_ContainerIOManager.stop_fetching_inputs()
|
22
21
|
|
23
22
|
|
@@ -25,7 +24,6 @@ def get_local_input_concurrency():
|
|
25
24
|
"""Get the container's local input concurrency.
|
26
25
|
If recently reduced to particular value, it can return a larger number than
|
27
26
|
set due to in-progress inputs."""
|
28
|
-
|
29
27
|
return _ContainerIOManager.get_input_concurrency()
|
30
28
|
|
31
29
|
|
@@ -33,7 +31,6 @@ def set_local_input_concurrency(concurrency: int):
|
|
33
31
|
"""Set the container's local input concurrency. Dynamic concurrency will be disabled.
|
34
32
|
When setting to a smaller value, this method will not interrupt in-progress inputs.
|
35
33
|
"""
|
36
|
-
|
37
34
|
_ContainerIOManager.set_input_concurrency(concurrency)
|
38
35
|
|
39
36
|
|
modal/functions.py
CHANGED
@@ -299,13 +299,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
299
299
|
"""Functions are the basic units of serverless execution on Modal.
|
300
300
|
|
301
301
|
Generally, you will not construct a `Function` directly. Instead, use the
|
302
|
-
|
303
|
-
for your application.
|
302
|
+
`App.function()` decorator to register your Python functions with your App.
|
304
303
|
"""
|
305
304
|
|
306
305
|
# TODO: more type annotations
|
307
306
|
_info: Optional[FunctionInfo]
|
308
|
-
|
307
|
+
_serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
|
309
308
|
_app: Optional["modal.app._App"] = None
|
310
309
|
_obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
|
311
310
|
_web_url: Optional[str]
|
@@ -315,7 +314,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
315
314
|
_tag: str
|
316
315
|
_raw_f: Callable[..., Any]
|
317
316
|
_build_args: dict
|
318
|
-
|
317
|
+
|
319
318
|
_is_generator: Optional[bool] = None
|
320
319
|
_cluster_size: Optional[int] = None
|
321
320
|
|
@@ -324,10 +323,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
324
323
|
_use_function_id: str # The function to invoke
|
325
324
|
_use_method_name: str = ""
|
326
325
|
|
327
|
-
# TODO (elias): remove _parent. In case of instance functions, and methods bound on those,
|
328
|
-
# this references the parent class-function and is used to infer the client for lazy-loaded methods
|
329
|
-
_parent: Optional["_Function"] = None
|
330
|
-
|
331
326
|
_class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
|
332
327
|
_method_handle_metadata: Optional[Dict[str, "api_pb2.FunctionHandleMetadata"]] = None
|
333
328
|
|
@@ -512,7 +507,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
512
507
|
fun._info = class_bound_method._info
|
513
508
|
fun._obj = instance_service_function._obj
|
514
509
|
fun._is_method = True
|
515
|
-
fun._parent = instance_service_function._parent
|
516
510
|
fun._app = class_bound_method._app
|
517
511
|
fun._spec = class_bound_method._spec
|
518
512
|
return fun
|
@@ -580,6 +574,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
580
574
|
|
581
575
|
if is_local():
|
582
576
|
entrypoint_mounts = info.get_entrypoint_mount()
|
577
|
+
|
583
578
|
all_mounts = [
|
584
579
|
_get_client_mount(),
|
585
580
|
*explicit_mounts,
|
@@ -612,6 +607,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
612
607
|
if proxy:
|
613
608
|
# HACK: remove this once we stop using ssh tunnels for this.
|
614
609
|
if image:
|
610
|
+
# TODO(elias): this will cause an error if users use prior `.add_local_*` commands without copy=True
|
615
611
|
image = image.apt_install("autossh")
|
616
612
|
|
617
613
|
function_spec = _FunctionSpec(
|
@@ -828,7 +824,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
828
824
|
)
|
829
825
|
for path, volume in validated_volumes
|
830
826
|
]
|
831
|
-
loaded_mount_ids = {m.object_id for m in all_mounts}
|
827
|
+
loaded_mount_ids = {m.object_id for m in all_mounts} | {m.object_id for m in image._mount_layers}
|
832
828
|
|
833
829
|
# Get object dependencies
|
834
830
|
object_dependencies = []
|
@@ -970,9 +966,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
970
966
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
971
967
|
raise
|
972
968
|
function_creation_status.set_response(response)
|
973
|
-
|
974
|
-
|
975
|
-
obj.
|
969
|
+
serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
|
970
|
+
serve_mounts |= image._serve_mounts
|
971
|
+
obj._serve_mounts = frozenset(serve_mounts)
|
976
972
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
977
973
|
|
978
974
|
rep = f"Function({tag})"
|
@@ -1018,27 +1014,37 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1018
1014
|
Binds a class-function to a specific instance of (init params, options) or a new workspace
|
1019
1015
|
"""
|
1020
1016
|
|
1021
|
-
|
1022
|
-
|
1017
|
+
# In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
|
1018
|
+
can_use_parent = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
|
1019
|
+
parent = self
|
1020
|
+
|
1021
|
+
async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
|
1022
|
+
if parent is None:
|
1023
1023
|
raise ExecutionError("Can't find the parent class' service function")
|
1024
1024
|
try:
|
1025
|
-
identity = f"{
|
1025
|
+
identity = f"{parent.info.function_name} class service function"
|
1026
1026
|
except Exception:
|
1027
1027
|
# Can't always look up the function name that way, so fall back to generic message
|
1028
1028
|
identity = "class service function for a parameterized class"
|
1029
|
-
if not
|
1030
|
-
if
|
1031
|
-
reason = ", because the App it is defined on is not running
|
1029
|
+
if not parent.is_hydrated:
|
1030
|
+
if parent.app._running_app is None:
|
1031
|
+
reason = ", because the App it is defined on is not running"
|
1032
1032
|
else:
|
1033
1033
|
reason = ""
|
1034
1034
|
raise ExecutionError(
|
1035
1035
|
f"The {identity} has not been hydrated with the metadata it needs to run on Modal{reason}."
|
1036
1036
|
)
|
1037
|
-
|
1037
|
+
|
1038
|
+
assert parent._client.stub
|
1039
|
+
|
1040
|
+
if can_use_parent:
|
1041
|
+
# We can end up here if parent wasn't hydrated when class was instantiated, but has been since.
|
1042
|
+
param_bound_func._hydrate_from_other(parent)
|
1043
|
+
return
|
1044
|
+
|
1038
1045
|
if (
|
1039
|
-
|
1040
|
-
and
|
1041
|
-
== api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
|
1046
|
+
parent._class_parameter_info
|
1047
|
+
and parent._class_parameter_info.format == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
|
1042
1048
|
):
|
1043
1049
|
if args:
|
1044
1050
|
# TODO(elias) - We could potentially support positional args as well, if we want to?
|
@@ -1046,34 +1052,30 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1046
1052
|
"Can't use positional arguments with modal.parameter-based synthetic constructors.\n"
|
1047
1053
|
"Use (<parameter_name>=value) keyword arguments when constructing classes instead."
|
1048
1054
|
)
|
1049
|
-
serialized_params = serialize_proto_params(kwargs,
|
1055
|
+
serialized_params = serialize_proto_params(kwargs, parent._class_parameter_info.schema)
|
1050
1056
|
else:
|
1051
1057
|
serialized_params = serialize((args, kwargs))
|
1052
1058
|
environment_name = _get_environment_name(None, resolver)
|
1053
|
-
assert
|
1059
|
+
assert parent is not None
|
1054
1060
|
req = api_pb2.FunctionBindParamsRequest(
|
1055
|
-
function_id=
|
1061
|
+
function_id=parent._object_id,
|
1056
1062
|
serialized_params=serialized_params,
|
1057
1063
|
function_options=options,
|
1058
1064
|
environment_name=environment_name
|
1059
1065
|
or "", # TODO: investigate shouldn't environment name always be specified here?
|
1060
1066
|
)
|
1061
1067
|
|
1062
|
-
response = await retry_transient_errors(
|
1063
|
-
|
1068
|
+
response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
|
1069
|
+
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
1064
1070
|
|
1065
1071
|
fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True)
|
1066
1072
|
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
# Edge case that lets us hydrate all objects right away
|
1071
|
-
# if the instance didn't use explicit constructor arguments
|
1072
|
-
fun._hydrate_from_other(self)
|
1073
|
+
if can_use_parent and parent.is_hydrated:
|
1074
|
+
# skip the resolver altogether:
|
1075
|
+
fun._hydrate_from_other(parent)
|
1073
1076
|
|
1074
1077
|
fun._info = self._info
|
1075
1078
|
fun._obj = obj
|
1076
|
-
fun._parent = self
|
1077
1079
|
return fun
|
1078
1080
|
|
1079
1081
|
@live_method
|
@@ -1223,7 +1225,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1223
1225
|
self._function_name = None
|
1224
1226
|
self._info = None
|
1225
1227
|
self._use_function_id = ""
|
1226
|
-
self.
|
1228
|
+
self._serve_mounts = frozenset()
|
1227
1229
|
|
1228
1230
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
1229
1231
|
# Overridden concrete implementation of base class method
|
@@ -1264,8 +1266,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1264
1266
|
+ f"or call it locally: {self._function_name}.local()"
|
1265
1267
|
)
|
1266
1268
|
|
1269
|
+
# TODO (live_method on properties is not great, since it could be blocking the event loop from async contexts)
|
1267
1270
|
@property
|
1268
|
-
|
1271
|
+
@live_method
|
1272
|
+
async def web_url(self) -> str:
|
1269
1273
|
"""URL of a Function running as a web endpoint."""
|
1270
1274
|
if not self._web_url:
|
1271
1275
|
raise ValueError(
|
@@ -1438,7 +1442,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1438
1442
|
return fun(*args, **kwargs)
|
1439
1443
|
else:
|
1440
1444
|
# This is a method on a class, so bind the self to the function
|
1441
|
-
user_cls_instance = obj.
|
1445
|
+
user_cls_instance = obj._cached_user_cls_instance()
|
1442
1446
|
|
1443
1447
|
fun = info.raw_f.__get__(user_cls_instance)
|
1444
1448
|
|
modal/functions.pyi
CHANGED
@@ -110,7 +110,7 @@ OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
|
|
110
110
|
|
111
111
|
class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object._Object):
|
112
112
|
_info: typing.Optional[modal._utils.function_utils.FunctionInfo]
|
113
|
-
|
113
|
+
_serve_mounts: typing.FrozenSet[modal.mount._Mount]
|
114
114
|
_app: typing.Optional[modal.app._App]
|
115
115
|
_obj: typing.Optional[modal.cls._Obj]
|
116
116
|
_web_url: typing.Optional[str]
|
@@ -120,12 +120,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
120
120
|
_tag: str
|
121
121
|
_raw_f: typing.Callable[..., typing.Any]
|
122
122
|
_build_args: dict
|
123
|
-
_can_use_base_function: bool
|
124
123
|
_is_generator: typing.Optional[bool]
|
125
124
|
_cluster_size: typing.Optional[int]
|
126
125
|
_use_function_id: str
|
127
126
|
_use_method_name: str
|
128
|
-
_parent: typing.Optional[_Function]
|
129
127
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
130
128
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
131
129
|
|
@@ -218,7 +216,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
218
216
|
def _get_metadata(self): ...
|
219
217
|
def _check_no_web_url(self, fn_name: str): ...
|
220
218
|
@property
|
221
|
-
def web_url(self) -> str: ...
|
219
|
+
async def web_url(self) -> str: ...
|
222
220
|
@property
|
223
221
|
def is_generator(self) -> bool: ...
|
224
222
|
@property
|
@@ -286,7 +284,7 @@ P_INNER = typing_extensions.ParamSpec("P_INNER")
|
|
286
284
|
|
287
285
|
class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.Object):
|
288
286
|
_info: typing.Optional[modal._utils.function_utils.FunctionInfo]
|
289
|
-
|
287
|
+
_serve_mounts: typing.FrozenSet[modal.mount.Mount]
|
290
288
|
_app: typing.Optional[modal.app.App]
|
291
289
|
_obj: typing.Optional[modal.cls.Obj]
|
292
290
|
_web_url: typing.Optional[str]
|
@@ -296,12 +294,10 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
296
294
|
_tag: str
|
297
295
|
_raw_f: typing.Callable[..., typing.Any]
|
298
296
|
_build_args: dict
|
299
|
-
_can_use_base_function: bool
|
300
297
|
_is_generator: typing.Optional[bool]
|
301
298
|
_cluster_size: typing.Optional[int]
|
302
299
|
_use_function_id: str
|
303
300
|
_use_method_name: str
|
304
|
-
_parent: typing.Optional[Function]
|
305
301
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
306
302
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
307
303
|
|
@@ -450,11 +446,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
450
446
|
|
451
447
|
_call_generator_nowait: ___call_generator_nowait_spec
|
452
448
|
|
453
|
-
class __remote_spec(typing_extensions.Protocol[
|
449
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
454
450
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
455
451
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
456
452
|
|
457
|
-
remote: __remote_spec[
|
453
|
+
remote: __remote_spec[ReturnType, P]
|
458
454
|
|
459
455
|
class __remote_gen_spec(typing_extensions.Protocol):
|
460
456
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -466,17 +462,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
466
462
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
467
463
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
468
464
|
|
469
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
465
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
470
466
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
471
467
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
472
468
|
|
473
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
469
|
+
_experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
|
474
470
|
|
475
|
-
class __spawn_spec(typing_extensions.Protocol[
|
471
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
476
472
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
477
473
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
478
474
|
|
479
|
-
spawn: __spawn_spec[
|
475
|
+
spawn: __spawn_spec[ReturnType, P]
|
480
476
|
|
481
477
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
482
478
|
|
modal/gpu.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import Optional, Union
|
3
|
+
from typing import Callable, Optional, Union
|
4
4
|
|
5
5
|
from modal_proto import api_pb2
|
6
6
|
|
@@ -147,25 +147,27 @@ class Any(_GPUConfig):
|
|
147
147
|
return f"GPU(Any, count={self.count})"
|
148
148
|
|
149
149
|
|
150
|
-
STRING_TO_GPU_CONFIG = {
|
150
|
+
STRING_TO_GPU_CONFIG: dict[str, Callable] = {
|
151
151
|
"t4": T4,
|
152
152
|
"l4": L4,
|
153
153
|
"a100": A100,
|
154
|
+
"a100-80gb": lambda: A100(size="80GB"),
|
154
155
|
"h100": H100,
|
155
156
|
"a10g": A10G,
|
156
157
|
"any": Any,
|
157
158
|
}
|
158
|
-
display_string_to_config = "\n".join(
|
159
|
-
f'- "{key}" → `{cls()}`' for key, cls in STRING_TO_GPU_CONFIG.items() if key != "inf2"
|
160
|
-
)
|
159
|
+
display_string_to_config = "\n".join(f'- "{key}" → `{c()}`' for key, c in STRING_TO_GPU_CONFIG.items() if key != "inf2")
|
161
160
|
__doc__ = f"""
|
162
161
|
**GPU configuration shortcodes**
|
163
162
|
|
164
163
|
The following are the valid `str` values for the `gpu` parameter of
|
165
|
-
[`@app.function`](/docs/reference/modal.
|
164
|
+
[`@app.function`](/docs/reference/modal.App#function).
|
166
165
|
|
167
166
|
{display_string_to_config}
|
168
167
|
|
168
|
+
The shortcodes also support specifying count by suffixing `:N` to acquire `N` GPUs.
|
169
|
+
For example, `a10g:4` will provision 4 A10G GPUs.
|
170
|
+
|
169
171
|
Other configurations can be created using the constructors documented below.
|
170
172
|
"""
|
171
173
|
|