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.
Files changed (43) hide show
  1. modal/_container_entrypoint.py +5 -342
  2. modal/_runtime/container_io_manager.py +6 -14
  3. modal/_runtime/user_code_imports.py +361 -0
  4. modal/_utils/function_utils.py +28 -8
  5. modal/_utils/grpc_testing.py +33 -26
  6. modal/app.py +13 -46
  7. modal/cli/import_refs.py +4 -38
  8. modal/client.pyi +2 -2
  9. modal/cls.py +26 -19
  10. modal/cls.pyi +4 -4
  11. modal/dict.py +0 -6
  12. modal/dict.pyi +0 -4
  13. modal/experimental.py +0 -3
  14. modal/functions.py +42 -38
  15. modal/functions.pyi +9 -13
  16. modal/gpu.py +8 -6
  17. modal/image.py +141 -7
  18. modal/image.pyi +34 -4
  19. modal/io_streams.py +40 -33
  20. modal/io_streams.pyi +13 -13
  21. modal/mount.py +5 -2
  22. modal/network_file_system.py +0 -28
  23. modal/network_file_system.pyi +0 -14
  24. modal/partial_function.py +12 -2
  25. modal/queue.py +0 -6
  26. modal/queue.pyi +0 -4
  27. modal/sandbox.py +1 -1
  28. modal/volume.py +0 -22
  29. modal/volume.pyi +0 -9
  30. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/METADATA +1 -2
  31. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/RECORD +43 -42
  32. modal_proto/api.proto +3 -20
  33. modal_proto/api_grpc.py +0 -16
  34. modal_proto/api_pb2.py +389 -413
  35. modal_proto/api_pb2.pyi +12 -58
  36. modal_proto/api_pb2_grpc.py +0 -33
  37. modal_proto/api_pb2_grpc.pyi +0 -10
  38. modal_proto/modal_api_grpc.py +0 -1
  39. modal_version/_version_generated.py +1 -1
  40. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/LICENSE +0 -0
  41. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/WHEEL +0 -0
  42. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/entry_points.txt +0 -0
  43. {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, deprecation_warning
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: Optional[str]) -> Optional[Any]:
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 = get_by_object_path_try_possible_app_names(module, import_ref.object_path)
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 = get_by_object_path_try_possible_app_names(module, import_ref.object_path)
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.17",
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.17",
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 _user_cls_instance_constr(self):
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 _get_user_cls_instance(self):
167
- """Construct local object lazily. Used for .local() calls."""
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._user_cls_instance_constr() # Instantiate object
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._get_user_cls_instance()
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
- # if we know the user is accessing a method, we don't have to create an instance
209
- # yet, since the user might just call `.remote()` on it which doesn't require
210
- # a local instance (in case __init__ does stuff that can't locally)
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
- elif self._user_cls_instance_constr:
213
- # if it's *not* a method
214
- # TODO: To get lazy loading (from_name) of classes to work, we need to avoid
215
- # this path, otherwise local initialization will happen regardless if user
216
- # only runs .remote(), since we don't know methods for the class until we
217
- # load it
218
- user_cls_instance = self._get_user_cls_instance()
219
- return getattr(user_cls_instance, k)
220
- else:
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 _user_cls_instance_constr(self): ...
40
+ def _new_user_cls_instance(self): ...
41
41
  async def keep_warm(self, warm_pool_size: int) -> None: ...
42
- def _get_user_cls_instance(self): ...
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 _user_cls_instance_constr(self): ...
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 _get_user_cls_instance(self): ...
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
- `@app.function()` decorator on the `App` object (formerly called "Stub")
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
- _used_local_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
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
- _can_use_base_function: bool = False # whether we need to call FunctionBindParams
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
- local_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
974
- local_mounts |= image._used_local_mounts
975
- obj._used_local_mounts = frozenset(local_mounts)
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
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1022
- if self._parent is None:
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"{self._parent.info.function_name} class service function"
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 self._parent.is_hydrated:
1030
- if self._parent.app._running_app is None:
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
- assert self._parent._client.stub
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
- self._parent._class_parameter_info
1040
- and self._parent._class_parameter_info.format
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, self._parent._class_parameter_info.schema)
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 self._parent is not None
1059
+ assert parent is not None
1054
1060
  req = api_pb2.FunctionBindParamsRequest(
1055
- function_id=self._parent._object_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(self._parent._client.stub.FunctionBindParams, req)
1063
- self._hydrate(response.bound_function_id, self._parent._client, response.handle_metadata)
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
- # In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
1068
- fun._can_use_base_function = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
1069
- if fun._can_use_base_function and self.is_hydrated:
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._used_local_mounts = frozenset()
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
- def web_url(self) -> str:
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._get_user_cls_instance()
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
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
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
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
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[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
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[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
469
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
474
470
 
475
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
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.Stub#function).
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