modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/cls.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import dataclasses
|
|
3
3
|
import inspect
|
|
4
|
-
import os
|
|
5
4
|
import typing
|
|
6
5
|
from collections.abc import Collection
|
|
7
|
-
from
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from typing import Any, Callable, Optional, Sequence, TypeVar, Union
|
|
8
8
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
10
|
from grpclib import GRPCError, Status
|
|
@@ -12,7 +12,8 @@ from grpclib import GRPCError, Status
|
|
|
12
12
|
from modal_proto import api_pb2
|
|
13
13
|
|
|
14
14
|
from ._functions import _Function, _parse_retries
|
|
15
|
-
from .
|
|
15
|
+
from ._load_context import LoadContext
|
|
16
|
+
from ._object import _Object, live_method
|
|
16
17
|
from ._partial_function import (
|
|
17
18
|
_find_callables_for_obj,
|
|
18
19
|
_find_partial_methods_for_user_cls,
|
|
@@ -25,14 +26,18 @@ from ._serialization import check_valid_cls_constructor_arg
|
|
|
25
26
|
from ._traceback import print_server_warnings
|
|
26
27
|
from ._type_manager import parameter_serde_registry
|
|
27
28
|
from ._utils.async_utils import synchronize_api, synchronizer
|
|
28
|
-
from ._utils.deprecation import
|
|
29
|
-
|
|
29
|
+
from ._utils.deprecation import (
|
|
30
|
+
deprecation_warning,
|
|
31
|
+
warn_if_passing_namespace,
|
|
32
|
+
warn_on_renamed_autoscaler_settings,
|
|
33
|
+
)
|
|
30
34
|
from ._utils.mount_utils import validate_volumes
|
|
31
35
|
from .client import _Client
|
|
32
|
-
from .
|
|
36
|
+
from .cloud_bucket_mount import _CloudBucketMount
|
|
33
37
|
from .exception import ExecutionError, InvalidError, NotFoundError
|
|
34
38
|
from .gpu import GPU_T
|
|
35
39
|
from .retries import Retries
|
|
40
|
+
from .scheduler_placement import SchedulerPlacement
|
|
36
41
|
from .secret import _Secret
|
|
37
42
|
from .volume import _Volume
|
|
38
43
|
|
|
@@ -54,7 +59,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
|
54
59
|
return inspect.signature(user_cls)
|
|
55
60
|
else:
|
|
56
61
|
constructor_parameters = []
|
|
57
|
-
for name, annotation_value in
|
|
62
|
+
for name, annotation_value in typing.get_type_hints(user_cls).items():
|
|
58
63
|
if hasattr(user_cls, name):
|
|
59
64
|
parameter_spec = getattr(user_cls, name)
|
|
60
65
|
if is_parameter(parameter_spec):
|
|
@@ -75,7 +80,8 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
|
75
80
|
|
|
76
81
|
@dataclasses.dataclass()
|
|
77
82
|
class _ServiceOptions:
|
|
78
|
-
|
|
83
|
+
# Note that default values should always be "untruthy" so we can detect when they are not set
|
|
84
|
+
secrets: Collection[_Secret] = ()
|
|
79
85
|
validated_volumes: typing.Sequence[tuple[str, _Volume]] = ()
|
|
80
86
|
resources: Optional[api_pb2.Resources] = None
|
|
81
87
|
retry_policy: Optional[api_pb2.FunctionRetryPolicy] = None
|
|
@@ -87,6 +93,29 @@ class _ServiceOptions:
|
|
|
87
93
|
target_concurrent_inputs: Optional[int] = None
|
|
88
94
|
batch_max_size: Optional[int] = None
|
|
89
95
|
batch_wait_ms: Optional[int] = None
|
|
96
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
97
|
+
cloud: Optional[str] = None
|
|
98
|
+
cloud_bucket_mounts: typing.Sequence[tuple[str, _CloudBucketMount]] = ()
|
|
99
|
+
|
|
100
|
+
def merge_options(self, new_options: "_ServiceOptions") -> "_ServiceOptions":
|
|
101
|
+
"""Implement protobuf-like MergeFrom semantics for this dataclass.
|
|
102
|
+
|
|
103
|
+
This mostly exists to support "stacking" of `.with_options()` calls.
|
|
104
|
+
"""
|
|
105
|
+
# Don't use dataclasses.asdict() because it does a deepcopy(), which chokes on a hydrated object
|
|
106
|
+
new_options_dict = {k.name: getattr(new_options, k.name) for k in dataclasses.fields(new_options)}
|
|
107
|
+
|
|
108
|
+
# Resources needs special merge handling because individual fields are parameters in the public API
|
|
109
|
+
merged_resources = api_pb2.Resources()
|
|
110
|
+
if self.resources:
|
|
111
|
+
merged_resources.MergeFrom(self.resources)
|
|
112
|
+
if new_resources := new_options_dict.pop("resources"):
|
|
113
|
+
merged_resources.MergeFrom(new_resources)
|
|
114
|
+
self.resources = merged_resources
|
|
115
|
+
|
|
116
|
+
for key, value in new_options_dict.items():
|
|
117
|
+
if value: # Only overwrite data when the value was set in the new options
|
|
118
|
+
setattr(self, key, value)
|
|
90
119
|
|
|
91
120
|
|
|
92
121
|
def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name: str):
|
|
@@ -106,7 +135,7 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
106
135
|
method_metadata = cls._method_metadata[method_name]
|
|
107
136
|
new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
|
|
108
137
|
|
|
109
|
-
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
|
138
|
+
async def _load(fun: "_Function", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
110
139
|
# there is currently no actual loading logic executed to create each method on
|
|
111
140
|
# the *parametrized* instance of a class - it uses the parameter-bound service-function
|
|
112
141
|
# for the instance. This load method just makes sure to set all attributes after the
|
|
@@ -125,11 +154,14 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
125
154
|
|
|
126
155
|
rep = f"Method({cls._name}.{method_name})"
|
|
127
156
|
|
|
157
|
+
# Bound methods should *reference* their parent Cls's LoadContext
|
|
158
|
+
# so that it can be modified in place on the parent and be reflected in the method
|
|
128
159
|
fun = _Function._from_loader(
|
|
129
160
|
_load,
|
|
130
161
|
rep,
|
|
131
162
|
deps=_deps,
|
|
132
163
|
hydrate_lazily=True,
|
|
164
|
+
load_context_overrides=cls._load_context_overrides,
|
|
133
165
|
)
|
|
134
166
|
if service_function.is_hydrated:
|
|
135
167
|
# Eager hydration (skip load) if the instance service function is already loaded
|
|
@@ -393,14 +425,13 @@ class _Obj:
|
|
|
393
425
|
|
|
394
426
|
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
|
395
427
|
# has not yet been loaded. So use a special loader that loads it lazily:
|
|
396
|
-
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
|
397
|
-
await resolver.load(self._cls) # load class so we get info about methods
|
|
428
|
+
async def method_loader(fun, resolver: Resolver, load_context: LoadContext, existing_object_id):
|
|
398
429
|
method_function = _get_maybe_method()
|
|
399
430
|
if method_function is None:
|
|
400
431
|
raise NotFoundError(
|
|
401
432
|
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
|
402
433
|
)
|
|
403
|
-
await resolver.load(method_function) # get the appropriate method handle (lazy)
|
|
434
|
+
await resolver.load(method_function, load_context) # get the appropriate method handle (lazy)
|
|
404
435
|
fun._hydrate_from_other(method_function)
|
|
405
436
|
|
|
406
437
|
# The reason we don't *always* use this lazy loader is because it precludes attribute access
|
|
@@ -408,8 +439,9 @@ class _Obj:
|
|
|
408
439
|
return _Function._from_loader(
|
|
409
440
|
method_loader,
|
|
410
441
|
rep=f"Method({self._cls._name}.{k})",
|
|
411
|
-
deps=lambda: [],
|
|
442
|
+
deps=lambda: [self._cls],
|
|
412
443
|
hydrate_lazily=True,
|
|
444
|
+
load_context_overrides=self._cls._load_context_overrides,
|
|
413
445
|
)
|
|
414
446
|
|
|
415
447
|
|
|
@@ -418,11 +450,11 @@ Obj = synchronize_api(_Obj)
|
|
|
418
450
|
|
|
419
451
|
class _Cls(_Object, type_prefix="cs"):
|
|
420
452
|
"""
|
|
421
|
-
Cls adds method pooling and [lifecycle hook](/docs/guide/lifecycle-functions) behavior
|
|
422
|
-
to [modal.Function](/docs/reference/modal.Function).
|
|
453
|
+
Cls adds method pooling and [lifecycle hook](https://modal.com/docs/guide/lifecycle-functions) behavior
|
|
454
|
+
to [modal.Function](https://modal.com/docs/reference/modal.Function).
|
|
423
455
|
|
|
424
456
|
Generally, you will not construct a Cls directly.
|
|
425
|
-
Instead, use the [`@app.cls()`](/docs/reference/modal.App#cls) decorator on the App object.
|
|
457
|
+
Instead, use the [`@app.cls()`](https://modal.com/docs/reference/modal.App#cls) decorator on the App object.
|
|
426
458
|
"""
|
|
427
459
|
|
|
428
460
|
_class_service_function: Optional[_Function] # The _Function (read "service") serving *all* methods of the class
|
|
@@ -456,6 +488,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
456
488
|
self._callables = other._callables
|
|
457
489
|
self._name = other._name
|
|
458
490
|
self._method_metadata = other._method_metadata
|
|
491
|
+
self._load_context_overrides = other._load_context_overrides
|
|
459
492
|
|
|
460
493
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
|
461
494
|
if not self._user_cls:
|
|
@@ -482,6 +515,11 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
482
515
|
# returns method names for a *local* class only for now (used by cli)
|
|
483
516
|
return self._method_partials.keys()
|
|
484
517
|
|
|
518
|
+
@live_method
|
|
519
|
+
async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
|
|
520
|
+
"""URL of the flash service for the class."""
|
|
521
|
+
return await self._get_class_service_function()._experimental_get_flash_urls()
|
|
522
|
+
|
|
485
523
|
def _hydrate_metadata(self, metadata: Message):
|
|
486
524
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
|
487
525
|
class_service_function = self._get_class_service_function()
|
|
@@ -543,22 +581,15 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
543
581
|
# validate signature
|
|
544
582
|
_Cls.validate_construction_mechanism(user_cls)
|
|
545
583
|
|
|
546
|
-
method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
|
547
|
-
user_cls, _PartialFunctionFlags.interface_flags()
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
for method_name, partial_function in method_partials.items():
|
|
551
|
-
if partial_function.params.webhook_config is not None:
|
|
552
|
-
full_name = f"{user_cls.__name__}.{method_name}"
|
|
553
|
-
app._web_endpoints.append(full_name)
|
|
554
|
-
partial_function.registered = True
|
|
555
|
-
|
|
556
584
|
# Disable the warning that lifecycle methods are not wrapped
|
|
557
|
-
|
|
585
|
+
lifecycle_method_partials = _find_partial_methods_for_user_cls(
|
|
558
586
|
user_cls, ~_PartialFunctionFlags.interface_flags()
|
|
559
|
-
)
|
|
587
|
+
)
|
|
588
|
+
for partial_function in lifecycle_method_partials.values():
|
|
560
589
|
partial_function.registered = True
|
|
561
590
|
|
|
591
|
+
method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
|
|
592
|
+
|
|
562
593
|
# Get all callables
|
|
563
594
|
callables: dict[str, Callable] = {
|
|
564
595
|
k: pf.raw_f
|
|
@@ -569,15 +600,18 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
569
600
|
def _deps() -> list[_Function]:
|
|
570
601
|
return [class_service_function]
|
|
571
602
|
|
|
572
|
-
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
|
603
|
+
async def _load(self: "_Cls", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
573
604
|
req = api_pb2.ClassCreateRequest(
|
|
574
|
-
app_id=
|
|
605
|
+
app_id=load_context.app_id, existing_class_id=existing_object_id, only_class_function=True
|
|
575
606
|
)
|
|
576
|
-
resp = await
|
|
577
|
-
self._hydrate(resp.class_id,
|
|
607
|
+
resp = await load_context.client.stub.ClassCreate(req)
|
|
608
|
+
self._hydrate(resp.class_id, load_context.client, resp.handle_metadata)
|
|
578
609
|
|
|
579
610
|
rep = f"Cls({user_cls.__name__})"
|
|
580
|
-
|
|
611
|
+
# Pass a *reference* to the App's LoadContext - this is important since the App is
|
|
612
|
+
# the only way to infer a LoadContext for an `@app.cls`, and the App doesn't
|
|
613
|
+
# get its client until *after* the Cls is created.
|
|
614
|
+
cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps, load_context_overrides=app._root_load_context)
|
|
581
615
|
cls._app = app
|
|
582
616
|
cls._user_cls = user_cls
|
|
583
617
|
cls._class_service_function = class_service_function
|
|
@@ -592,55 +626,67 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
592
626
|
app_name: str,
|
|
593
627
|
name: str,
|
|
594
628
|
*,
|
|
595
|
-
namespace=
|
|
629
|
+
namespace: Any = None, # mdmd:line-hidden
|
|
596
630
|
environment_name: Optional[str] = None,
|
|
631
|
+
client: Optional["_Client"] = None,
|
|
597
632
|
) -> "_Cls":
|
|
598
633
|
"""Reference a Cls from a deployed App by its name.
|
|
599
634
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
635
|
+
This is a lazy method that defers hydrating the local
|
|
636
|
+
object with metadata from Modal servers until the first
|
|
637
|
+
time it is actually used.
|
|
603
638
|
|
|
604
639
|
```python
|
|
605
640
|
Model = modal.Cls.from_name("other-app", "Model")
|
|
606
641
|
```
|
|
607
642
|
"""
|
|
608
|
-
|
|
643
|
+
warn_if_passing_namespace(namespace, "modal.Cls.from_name")
|
|
609
644
|
|
|
610
|
-
async def _load_remote(
|
|
645
|
+
async def _load_remote(
|
|
646
|
+
self: _Cls, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
647
|
+
):
|
|
611
648
|
request = api_pb2.ClassGetRequest(
|
|
612
649
|
app_name=app_name,
|
|
613
650
|
object_tag=name,
|
|
614
|
-
|
|
615
|
-
environment_name=_environment_name,
|
|
651
|
+
environment_name=load_context.environment_name,
|
|
616
652
|
only_class_function=True,
|
|
617
653
|
)
|
|
618
654
|
try:
|
|
619
|
-
response = await
|
|
655
|
+
response = await load_context.client.stub.ClassGet(request)
|
|
656
|
+
except NotFoundError as exc:
|
|
657
|
+
env_context = (
|
|
658
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
659
|
+
)
|
|
660
|
+
raise NotFoundError(
|
|
661
|
+
f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
662
|
+
) from None
|
|
620
663
|
except GRPCError as exc:
|
|
621
|
-
if exc.status == Status.
|
|
622
|
-
|
|
623
|
-
raise NotFoundError(
|
|
624
|
-
f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc.message}."
|
|
625
|
-
)
|
|
626
|
-
elif exc.status == Status.FAILED_PRECONDITION:
|
|
627
|
-
raise InvalidError(exc.message)
|
|
664
|
+
if exc.status == Status.FAILED_PRECONDITION:
|
|
665
|
+
raise InvalidError(exc.message) from None
|
|
628
666
|
else:
|
|
629
667
|
raise
|
|
630
668
|
|
|
631
669
|
print_server_warnings(response.server_warnings)
|
|
632
|
-
await resolver.load(self._class_service_function)
|
|
633
|
-
self._hydrate(response.class_id,
|
|
670
|
+
await resolver.load(self._class_service_function, load_context)
|
|
671
|
+
self._hydrate(response.class_id, load_context.client, response.handle_metadata)
|
|
634
672
|
|
|
635
|
-
|
|
636
|
-
|
|
673
|
+
environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
|
|
674
|
+
rep = f"Cls.from_name({app_name!r}, {name!r}{environment_rep})"
|
|
675
|
+
|
|
676
|
+
load_context_overrides = LoadContext(client=client, environment_name=environment_name)
|
|
677
|
+
cls = cls._from_loader(
|
|
678
|
+
_load_remote,
|
|
679
|
+
rep,
|
|
680
|
+
is_another_app=True,
|
|
681
|
+
hydrate_lazily=True,
|
|
682
|
+
load_context_overrides=load_context_overrides,
|
|
683
|
+
)
|
|
637
684
|
|
|
638
685
|
class_service_name = f"{name}.*" # special name of the base service function for the class
|
|
639
686
|
cls._class_service_function = _Function._from_name(
|
|
640
687
|
app_name,
|
|
641
688
|
class_service_name,
|
|
642
|
-
|
|
643
|
-
environment_name=_environment_name,
|
|
689
|
+
load_context_overrides=load_context_overrides,
|
|
644
690
|
)
|
|
645
691
|
cls._name = name
|
|
646
692
|
return cls
|
|
@@ -652,27 +698,47 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
652
698
|
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
|
653
699
|
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
654
700
|
gpu: GPU_T = None,
|
|
655
|
-
|
|
656
|
-
|
|
701
|
+
env: Optional[dict[str, Optional[str]]] = None,
|
|
702
|
+
secrets: Optional[Collection[_Secret]] = None,
|
|
703
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
|
657
704
|
retries: Optional[Union[int, Retries]] = None,
|
|
658
705
|
max_containers: Optional[int] = None, # Limit on the number of containers that can be concurrently running.
|
|
659
706
|
buffer_containers: Optional[int] = None, # Additional containers to scale up while Function is active.
|
|
660
707
|
scaledown_window: Optional[int] = None, # Max amount of time a container can remain idle before scaling down.
|
|
661
708
|
timeout: Optional[int] = None,
|
|
709
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
710
|
+
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
662
711
|
# The following parameters are deprecated
|
|
663
712
|
concurrency_limit: Optional[int] = None, # Now called `max_containers`
|
|
664
713
|
container_idle_timeout: Optional[int] = None, # Now called `scaledown_window`
|
|
665
714
|
allow_concurrent_inputs: Optional[int] = None, # See `.with_concurrency`
|
|
666
715
|
) -> "_Cls":
|
|
667
|
-
"""
|
|
716
|
+
"""Override the static Function configuration at runtime.
|
|
717
|
+
|
|
718
|
+
This method will return a new instance of the cls that will autoscale independently of the
|
|
719
|
+
original instance. Note that options cannot be "unset" with this method (i.e., if a GPU
|
|
720
|
+
is configured in the `@app.cls()` decorator, passing `gpu=None` here will not create a
|
|
721
|
+
CPU-only instance).
|
|
668
722
|
|
|
669
723
|
**Usage:**
|
|
670
724
|
|
|
725
|
+
You can use this method after looking up the Cls from a deployed App or if you have a
|
|
726
|
+
direct reference to a Cls from another Function or local entrypoint on its App:
|
|
727
|
+
|
|
671
728
|
```python notest
|
|
672
729
|
Model = modal.Cls.from_name("my_app", "Model")
|
|
673
730
|
ModelUsingGPU = Model.with_options(gpu="A100")
|
|
674
|
-
ModelUsingGPU().generate.remote(
|
|
731
|
+
ModelUsingGPU().generate.remote(input_prompt) # Run with an A100 GPU
|
|
675
732
|
```
|
|
733
|
+
|
|
734
|
+
The method can be called multiple times to "stack" updates:
|
|
735
|
+
|
|
736
|
+
```python notest
|
|
737
|
+
Model.with_options(gpu="A100").with_options(scaledown_window=300) # Use an A100 with slow scaledown
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Note that container arguments (i.e. `volumes` and `secrets`) passed in subsequent calls
|
|
741
|
+
will not be merged.
|
|
676
742
|
"""
|
|
677
743
|
retry_policy = _parse_retries(retries, f"Class {self.__name__}" if self._user_cls else "")
|
|
678
744
|
if gpu or cpu or memory:
|
|
@@ -680,6 +746,8 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
680
746
|
else:
|
|
681
747
|
resources = None
|
|
682
748
|
|
|
749
|
+
scheduler_placement = SchedulerPlacement(region=region).proto if region else None
|
|
750
|
+
|
|
683
751
|
if allow_concurrent_inputs is not None:
|
|
684
752
|
deprecation_warning(
|
|
685
753
|
(2025, 5, 9),
|
|
@@ -687,7 +755,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
687
755
|
" please use the `.with_concurrency` method instead.",
|
|
688
756
|
)
|
|
689
757
|
|
|
690
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
758
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
691
759
|
# this is a bit confusing, the cls will always have the same metadata
|
|
692
760
|
# since it has the same *class* service function (i.e. "template")
|
|
693
761
|
# But the (instance) service function for each Obj will be different
|
|
@@ -696,30 +764,51 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
696
764
|
if not self.is_hydrated:
|
|
697
765
|
# this should only happen for Cls.from_name instances
|
|
698
766
|
# other classes should already be hydrated!
|
|
699
|
-
await resolver.load(self)
|
|
767
|
+
await resolver.load(self, load_context)
|
|
700
768
|
|
|
701
769
|
new_cls._initialize_from_other(self)
|
|
702
770
|
|
|
703
771
|
def _deps():
|
|
704
772
|
return []
|
|
705
773
|
|
|
706
|
-
cls = _Cls._from_loader(
|
|
774
|
+
cls = _Cls._from_loader(
|
|
775
|
+
_load_from_base,
|
|
776
|
+
rep=f"{self._name}.with_options(...)",
|
|
777
|
+
is_another_app=True,
|
|
778
|
+
deps=_deps,
|
|
779
|
+
load_context_overrides=self._load_context_overrides,
|
|
780
|
+
hydrate_lazily=True,
|
|
781
|
+
)
|
|
707
782
|
cls._initialize_from_other(self)
|
|
708
|
-
|
|
709
|
-
|
|
783
|
+
|
|
784
|
+
# Validate volumes
|
|
785
|
+
validated_volumes = validate_volumes(volumes)
|
|
786
|
+
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
787
|
+
validated_volumes_no_cloud_buckets = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
|
|
788
|
+
|
|
789
|
+
secrets = secrets or []
|
|
790
|
+
if env:
|
|
791
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
792
|
+
|
|
793
|
+
new_options = _ServiceOptions(
|
|
710
794
|
secrets=secrets,
|
|
795
|
+
validated_volumes=validated_volumes_no_cloud_buckets,
|
|
796
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
711
797
|
resources=resources,
|
|
712
798
|
retry_policy=retry_policy,
|
|
713
799
|
max_containers=max_containers,
|
|
714
800
|
buffer_containers=buffer_containers,
|
|
715
801
|
scaledown_window=scaledown_window,
|
|
716
802
|
timeout_secs=timeout,
|
|
717
|
-
|
|
803
|
+
scheduler_placement=scheduler_placement,
|
|
804
|
+
cloud=cloud,
|
|
718
805
|
# Note: set both for backwards / forwards compatibility
|
|
719
806
|
# But going forward `.with_concurrency` is the preferred method with distinct parameterization
|
|
720
807
|
max_concurrent_inputs=allow_concurrent_inputs,
|
|
721
808
|
target_concurrent_inputs=allow_concurrent_inputs,
|
|
722
809
|
)
|
|
810
|
+
|
|
811
|
+
cls._options.merge_options(new_options)
|
|
723
812
|
return cls
|
|
724
813
|
|
|
725
814
|
def with_concurrency(self: "_Cls", *, max_inputs: int, target_inputs: Optional[int] = None) -> "_Cls":
|
|
@@ -734,21 +823,26 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
734
823
|
```
|
|
735
824
|
"""
|
|
736
825
|
|
|
737
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
826
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
738
827
|
if not self.is_hydrated:
|
|
739
|
-
await resolver.load(self)
|
|
828
|
+
await resolver.load(self, load_context)
|
|
740
829
|
new_cls._initialize_from_other(self)
|
|
741
830
|
|
|
742
831
|
def _deps():
|
|
743
832
|
return []
|
|
744
833
|
|
|
745
834
|
cls = _Cls._from_loader(
|
|
746
|
-
_load_from_base,
|
|
835
|
+
_load_from_base,
|
|
836
|
+
rep=f"{self._name}.with_concurrency(...)",
|
|
837
|
+
is_another_app=True,
|
|
838
|
+
deps=_deps,
|
|
839
|
+
load_context_overrides=self._load_context_overrides,
|
|
840
|
+
hydrate_lazily=True,
|
|
747
841
|
)
|
|
748
842
|
cls._initialize_from_other(self)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
)
|
|
843
|
+
|
|
844
|
+
concurrency_options = _ServiceOptions(max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs)
|
|
845
|
+
cls._options.merge_options(concurrency_options)
|
|
752
846
|
return cls
|
|
753
847
|
|
|
754
848
|
def with_batching(self: "_Cls", *, max_batch_size: int, wait_ms: int) -> "_Cls":
|
|
@@ -763,60 +857,27 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
763
857
|
```
|
|
764
858
|
"""
|
|
765
859
|
|
|
766
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
860
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
767
861
|
if not self.is_hydrated:
|
|
768
|
-
await resolver.load(self)
|
|
862
|
+
await resolver.load(self, load_context)
|
|
769
863
|
new_cls._initialize_from_other(self)
|
|
770
864
|
|
|
771
865
|
def _deps():
|
|
772
866
|
return []
|
|
773
867
|
|
|
774
868
|
cls = _Cls._from_loader(
|
|
775
|
-
_load_from_base,
|
|
869
|
+
_load_from_base,
|
|
870
|
+
rep=f"{self._name}.with_concurrency(...)",
|
|
871
|
+
is_another_app=True,
|
|
872
|
+
deps=_deps,
|
|
873
|
+
load_context_overrides=self._load_context_overrides,
|
|
874
|
+
hydrate_lazily=True,
|
|
776
875
|
)
|
|
777
876
|
cls._initialize_from_other(self)
|
|
778
|
-
cls._options = dataclasses.replace(cls._options, batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
|
|
779
|
-
return cls
|
|
780
|
-
|
|
781
|
-
@staticmethod
|
|
782
|
-
async def lookup(
|
|
783
|
-
app_name: str,
|
|
784
|
-
name: str,
|
|
785
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
786
|
-
client: Optional[_Client] = None,
|
|
787
|
-
environment_name: Optional[str] = None,
|
|
788
|
-
) -> "_Cls":
|
|
789
|
-
"""mdmd:hidden
|
|
790
|
-
Lookup a Cls from a deployed App by its name.
|
|
791
|
-
|
|
792
|
-
DEPRECATED: This method is deprecated in favor of `modal.Cls.from_name`.
|
|
793
877
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
```python notest
|
|
798
|
-
Model = modal.Cls.from_name("other-app", "Model")
|
|
799
|
-
model = Model()
|
|
800
|
-
model.inference(...)
|
|
801
|
-
```
|
|
802
|
-
"""
|
|
803
|
-
deprecation_warning(
|
|
804
|
-
(2025, 1, 27),
|
|
805
|
-
"`modal.Cls.lookup` is deprecated and will be removed in a future release."
|
|
806
|
-
" It can be replaced with `modal.Cls.from_name`."
|
|
807
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
808
|
-
)
|
|
809
|
-
obj = _Cls.from_name(
|
|
810
|
-
app_name,
|
|
811
|
-
name,
|
|
812
|
-
namespace=namespace,
|
|
813
|
-
environment_name=environment_name,
|
|
814
|
-
)
|
|
815
|
-
if client is None:
|
|
816
|
-
client = await _Client.from_env()
|
|
817
|
-
resolver = Resolver(client=client)
|
|
818
|
-
await resolver.load(obj)
|
|
819
|
-
return obj
|
|
878
|
+
batching_options = _ServiceOptions(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
|
|
879
|
+
cls._options.merge_options(batching_options)
|
|
880
|
+
return cls
|
|
820
881
|
|
|
821
882
|
@synchronizer.no_input_translation
|
|
822
883
|
def __call__(self, *args, **kwargs) -> _Obj:
|
|
@@ -830,18 +891,31 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
830
891
|
)
|
|
831
892
|
|
|
832
893
|
def __getattr__(self, k):
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
# if
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
894
|
+
if self._user_cls is not None:
|
|
895
|
+
# local class, we can check if there are static attributes and let the user access them
|
|
896
|
+
# except if they are PartialFunction (i.e. methods)
|
|
897
|
+
v = getattr(self._user_cls, k)
|
|
898
|
+
if not isinstance(v, modal.partial_function.PartialFunction):
|
|
899
|
+
return v
|
|
900
|
+
|
|
901
|
+
# We create a synthetic dummy Function that is guaranteed to raise an AttributeError when
|
|
902
|
+
# a user tries to use any of its "live methods" - this lets us raise exceptions for users
|
|
903
|
+
# only if they try to access methods on a Cls as if they were methods on the instance.
|
|
904
|
+
async def error_loader(
|
|
905
|
+
fun: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
906
|
+
):
|
|
907
|
+
raise AttributeError(
|
|
908
|
+
"You can't access methods on a Cls directly - Did you forget to instantiate the class first?\n"
|
|
909
|
+
"e.g. instead of MyClass.method.remote(), do MyClass().method.remote()"
|
|
841
910
|
)
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
911
|
+
|
|
912
|
+
return _Function._from_loader(
|
|
913
|
+
error_loader,
|
|
914
|
+
rep=f"UnboundMethod({self._name}.{k})",
|
|
915
|
+
deps=lambda: [],
|
|
916
|
+
hydrate_lazily=True,
|
|
917
|
+
load_context_overrides=self._load_context_overrides,
|
|
918
|
+
)
|
|
845
919
|
|
|
846
920
|
def _is_local(self) -> bool:
|
|
847
921
|
return self._user_cls is not None
|