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/environments.py
CHANGED
|
@@ -8,11 +8,10 @@ from google.protobuf.wrappers_pb2 import StringValue
|
|
|
8
8
|
|
|
9
9
|
from modal_proto import api_pb2
|
|
10
10
|
|
|
11
|
+
from ._load_context import LoadContext
|
|
11
12
|
from ._object import _Object
|
|
12
13
|
from ._resolver import Resolver
|
|
13
14
|
from ._utils.async_utils import synchronize_api, synchronizer
|
|
14
|
-
from ._utils.deprecation import deprecation_warning
|
|
15
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
16
15
|
from ._utils.name_utils import check_object_name
|
|
17
16
|
from .client import _Client
|
|
18
17
|
from .config import config, logger
|
|
@@ -54,6 +53,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
54
53
|
name: str,
|
|
55
54
|
*,
|
|
56
55
|
create_if_missing: bool = False,
|
|
56
|
+
client: Optional[_Client] = None,
|
|
57
57
|
):
|
|
58
58
|
if name:
|
|
59
59
|
# Allow null names for the case where we want to look up the "default" environment,
|
|
@@ -63,7 +63,9 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
63
63
|
# environments as part of public API when we make this class more useful.
|
|
64
64
|
check_object_name(name, "Environment")
|
|
65
65
|
|
|
66
|
-
async def _load(
|
|
66
|
+
async def _load(
|
|
67
|
+
self: _Environment, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
68
|
+
):
|
|
67
69
|
request = api_pb2.EnvironmentGetOrCreateRequest(
|
|
68
70
|
deployment_name=name,
|
|
69
71
|
object_creation_type=(
|
|
@@ -72,31 +74,17 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
72
74
|
else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
|
|
73
75
|
),
|
|
74
76
|
)
|
|
75
|
-
response = await
|
|
77
|
+
response = await load_context.client.stub.EnvironmentGetOrCreate(request)
|
|
76
78
|
logger.debug(f"Created environment with id {response.environment_id}")
|
|
77
|
-
self._hydrate(response.environment_id,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
client: Optional[_Client] = None,
|
|
86
|
-
create_if_missing: bool = False,
|
|
87
|
-
):
|
|
88
|
-
deprecation_warning(
|
|
89
|
-
(2025, 1, 27),
|
|
90
|
-
"`modal.Environment.lookup` is deprecated and will be removed in a future release."
|
|
91
|
-
" It can be replaced with `modal.Environment.from_name`."
|
|
92
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
79
|
+
self._hydrate(response.environment_id, load_context.client, response.metadata)
|
|
80
|
+
|
|
81
|
+
return _Environment._from_loader(
|
|
82
|
+
_load,
|
|
83
|
+
f"Environment.from_name({name!r})",
|
|
84
|
+
is_another_app=True,
|
|
85
|
+
hydrate_lazily=True,
|
|
86
|
+
load_context_overrides=LoadContext(client=client),
|
|
93
87
|
)
|
|
94
|
-
obj = _Environment.from_name(name, create_if_missing=create_if_missing)
|
|
95
|
-
if client is None:
|
|
96
|
-
client = await _Client.from_env()
|
|
97
|
-
resolver = Resolver(client=client)
|
|
98
|
-
await resolver.load(obj)
|
|
99
|
-
return obj
|
|
100
88
|
|
|
101
89
|
|
|
102
90
|
Environment = synchronize_api(_Environment)
|
|
@@ -109,7 +97,7 @@ ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
|
109
97
|
async def _get_environment_cached(name: str, client: _Client) -> _Environment:
|
|
110
98
|
if name in ENVIRONMENT_CACHE:
|
|
111
99
|
return ENVIRONMENT_CACHE[name]
|
|
112
|
-
environment = await _Environment.from_name(name).hydrate(
|
|
100
|
+
environment = await _Environment.from_name(name, client=client).hydrate()
|
|
113
101
|
ENVIRONMENT_CACHE[name] = environment
|
|
114
102
|
return environment
|
|
115
103
|
|
modal/environments.pyi
CHANGED
|
@@ -7,45 +7,60 @@ import typing
|
|
|
7
7
|
import typing_extensions
|
|
8
8
|
|
|
9
9
|
class EnvironmentSettings:
|
|
10
|
+
"""EnvironmentSettings(image_builder_version: str, webhook_suffix: str)"""
|
|
11
|
+
|
|
10
12
|
image_builder_version: str
|
|
11
13
|
webhook_suffix: str
|
|
12
14
|
|
|
13
|
-
def __init__(self, image_builder_version: str, webhook_suffix: str) -> None:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def
|
|
18
|
-
|
|
15
|
+
def __init__(self, image_builder_version: str, webhook_suffix: str) -> None:
|
|
16
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
"""Return repr(self)."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def __eq__(self, other):
|
|
24
|
+
"""Return self==value."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def __setattr__(self, name, value):
|
|
28
|
+
"""Implement setattr(self, name, value)."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def __delattr__(self, name):
|
|
32
|
+
"""Implement delattr(self, name)."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def __hash__(self):
|
|
36
|
+
"""Return hash(self)."""
|
|
37
|
+
...
|
|
19
38
|
|
|
20
39
|
class _Environment(modal._object._Object):
|
|
21
40
|
_settings: EnvironmentSettings
|
|
22
41
|
|
|
23
|
-
def __init__(self):
|
|
42
|
+
def __init__(self):
|
|
43
|
+
"""mdmd:hidden"""
|
|
44
|
+
...
|
|
45
|
+
|
|
24
46
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
|
25
47
|
@staticmethod
|
|
26
|
-
def from_name(
|
|
27
|
-
|
|
28
|
-
async def lookup(
|
|
29
|
-
name: str, client: typing.Optional[modal.client._Client] = None, create_if_missing: bool = False
|
|
48
|
+
def from_name(
|
|
49
|
+
name: str, *, create_if_missing: bool = False, client: typing.Optional[modal.client._Client] = None
|
|
30
50
|
): ...
|
|
31
51
|
|
|
32
52
|
class Environment(modal.object.Object):
|
|
33
53
|
_settings: EnvironmentSettings
|
|
34
54
|
|
|
35
|
-
def __init__(self):
|
|
55
|
+
def __init__(self):
|
|
56
|
+
"""mdmd:hidden"""
|
|
57
|
+
...
|
|
58
|
+
|
|
36
59
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
|
37
60
|
@staticmethod
|
|
38
|
-
def from_name(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def __call__(
|
|
42
|
-
self, /, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
|
|
43
|
-
): ...
|
|
44
|
-
async def aio(
|
|
45
|
-
self, /, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
|
|
46
|
-
): ...
|
|
47
|
-
|
|
48
|
-
lookup: __lookup_spec
|
|
61
|
+
def from_name(
|
|
62
|
+
name: str, *, create_if_missing: bool = False, client: typing.Optional[modal.client.Client] = None
|
|
63
|
+
): ...
|
|
49
64
|
|
|
50
65
|
async def _get_environment_cached(name: str, client: modal.client._Client) -> _Environment: ...
|
|
51
66
|
|
|
@@ -93,6 +108,13 @@ class __list_environments_spec(typing_extensions.Protocol):
|
|
|
93
108
|
|
|
94
109
|
list_environments: __list_environments_spec
|
|
95
110
|
|
|
96
|
-
def ensure_env(environment_name: typing.Optional[str] = None) -> str:
|
|
111
|
+
def ensure_env(environment_name: typing.Optional[str] = None) -> str:
|
|
112
|
+
"""Override config environment with environment from environment_name
|
|
113
|
+
|
|
114
|
+
This is necessary since a cli command that runs Modal code, without explicit
|
|
115
|
+
environment specification wouldn't pick up the environment specified in a
|
|
116
|
+
command line flag otherwise, e.g. when doing `modal run --env=foo`
|
|
117
|
+
"""
|
|
118
|
+
...
|
|
97
119
|
|
|
98
120
|
ENVIRONMENT_CACHE: dict[str, _Environment]
|
modal/exception.py
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
import random
|
|
3
3
|
import signal
|
|
4
4
|
|
|
5
|
+
import synchronicity.exceptions
|
|
6
|
+
|
|
7
|
+
UserCodeException = synchronicity.exceptions.UserCodeException # Deprecated type used for return_exception wrapping
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
class Error(Exception):
|
|
7
11
|
"""
|
|
8
|
-
Base class for all Modal errors. See [`modal.exception`](/docs/reference/modal.exception)
|
|
9
|
-
error classes.
|
|
12
|
+
Base class for all Modal errors. See [`modal.exception`](https://modal.com/docs/reference/modal.exception)
|
|
13
|
+
for the specialized error classes.
|
|
10
14
|
|
|
11
15
|
**Usage**
|
|
12
16
|
|
|
@@ -22,6 +26,10 @@ class Error(Exception):
|
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
|
|
29
|
+
class AlreadyExistsError(Error):
|
|
30
|
+
"""Raised when a resource creation conflicts with an existing resource."""
|
|
31
|
+
|
|
32
|
+
|
|
25
33
|
class RemoteError(Error):
|
|
26
34
|
"""Raised when an error occurs on the Modal server."""
|
|
27
35
|
|
|
@@ -34,6 +42,10 @@ class SandboxTimeoutError(TimeoutError):
|
|
|
34
42
|
"""Raised when a Sandbox exceeds its execution duration limit and times out."""
|
|
35
43
|
|
|
36
44
|
|
|
45
|
+
class ExecTimeoutError(TimeoutError):
|
|
46
|
+
"""Raised when a container process exceeds its execution duration limit and times out."""
|
|
47
|
+
|
|
48
|
+
|
|
37
49
|
class SandboxTerminatedError(Error):
|
|
38
50
|
"""Raised when a Sandbox is terminated for an internal reason."""
|
|
39
51
|
|
modal/experimental/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
import os
|
|
3
|
+
import shlex
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Literal, Optional, Union
|
|
@@ -12,13 +13,13 @@ from .._object import _get_environment_name
|
|
|
12
13
|
from .._partial_function import _clustered
|
|
13
14
|
from .._runtime.container_io_manager import _ContainerIOManager
|
|
14
15
|
from .._utils.async_utils import synchronize_api, synchronizer
|
|
15
|
-
from ..
|
|
16
|
-
from .._utils.grpc_utils import retry_transient_errors
|
|
16
|
+
from ..app import _App
|
|
17
17
|
from ..client import _Client
|
|
18
|
-
from ..cls import
|
|
18
|
+
from ..cls import _Cls
|
|
19
19
|
from ..exception import InvalidError
|
|
20
20
|
from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
|
|
21
21
|
from ..secret import _Secret
|
|
22
|
+
from .flash import flash_forward, flash_get_containers, flash_prometheus_autoscaler # noqa: F401
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def stop_fetching_inputs():
|
|
@@ -84,6 +85,51 @@ async def list_deployed_apps(environment_name: str = "", client: Optional[_Clien
|
|
|
84
85
|
return app_infos
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
@synchronizer.create_blocking
|
|
89
|
+
async def get_app_objects(
|
|
90
|
+
app_name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None
|
|
91
|
+
) -> dict[str, Union[_Function, _Cls]]:
|
|
92
|
+
"""Experimental interface for retrieving a dictionary of the Functions / Clses in an App.
|
|
93
|
+
|
|
94
|
+
The return value is a dictionary mapping names to unhydrated Function or Cls objects.
|
|
95
|
+
|
|
96
|
+
We plan to support this functionality through a stable API in the future. It's likely that
|
|
97
|
+
the stable API will look different (it will probably be a method on the App object itself).
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
# This is implemented through a somewhat odd mixture of internal RPCs and public APIs.
|
|
101
|
+
# While AppGetLayout provides the object ID and metadata for each object in the App, it's
|
|
102
|
+
# currently somewhere between very awkward and impossible to hydrate a modal.Cls with just
|
|
103
|
+
# that information, since the "class service function" needs to be loaded first
|
|
104
|
+
# (and it's not always possible to do that without knowledge of the parameterization).
|
|
105
|
+
# So instead we just use AppGetLayout to retrieve the names of the Functions / Clsices on
|
|
106
|
+
# the App and then use the public .from_name constructors to return unhydrated handles.
|
|
107
|
+
|
|
108
|
+
# Additionally, since we need to know the environment name to use `.from_name`, and the App's
|
|
109
|
+
# environment name isn't stored anywhere on the App (and cannot be retrieved via an RPC), the
|
|
110
|
+
# experimental function is parameterized by an App name while the stable API would instead
|
|
111
|
+
# be a method on the App itself.
|
|
112
|
+
|
|
113
|
+
if client is None:
|
|
114
|
+
client = await _Client.from_env()
|
|
115
|
+
|
|
116
|
+
app = await _App.lookup(app_name, environment_name=environment_name, client=client)
|
|
117
|
+
req = api_pb2.AppGetLayoutRequest(app_id=app.app_id)
|
|
118
|
+
app_layout_resp = await client.stub.AppGetLayout(req)
|
|
119
|
+
|
|
120
|
+
app_objects: dict[str, Union[_Function, _Cls]] = {}
|
|
121
|
+
|
|
122
|
+
for cls_name in app_layout_resp.app_layout.class_ids:
|
|
123
|
+
app_objects[cls_name] = _Cls.from_name(app_name, cls_name, environment_name=environment_name)
|
|
124
|
+
|
|
125
|
+
for func_name in app_layout_resp.app_layout.function_ids:
|
|
126
|
+
if func_name.endswith(".*"):
|
|
127
|
+
continue # TODO explain
|
|
128
|
+
app_objects[func_name] = _Function.from_name(app_name, func_name, environment_name=environment_name)
|
|
129
|
+
|
|
130
|
+
return app_objects
|
|
131
|
+
|
|
132
|
+
|
|
87
133
|
@synchronizer.create_blocking
|
|
88
134
|
async def raw_dockerfile_image(
|
|
89
135
|
path: Union[str, Path],
|
|
@@ -94,8 +140,9 @@ async def raw_dockerfile_image(
|
|
|
94
140
|
|
|
95
141
|
Unlike for `modal.Image.from_dockerfile`, the provided recipe will not be embellished with
|
|
96
142
|
steps to install dependencies for the Modal client package. As a consequence, the resulting
|
|
97
|
-
Image cannot be used with a modal Function unless those dependencies are
|
|
98
|
-
|
|
143
|
+
Image cannot be used with a modal Function unless those dependencies are already included
|
|
144
|
+
as part of the base Dockerfile recipe or are added in a subsequent layer. The Image _can_ be
|
|
145
|
+
directly used with a modal Sandbox, which does not need the Modal client.
|
|
99
146
|
|
|
100
147
|
We expect to support this experimental function until the `2025.04` Modal Image Builder is
|
|
101
148
|
stable, at which point Modal Image recipes will no longer install the client dependencies
|
|
@@ -127,8 +174,9 @@ async def raw_registry_image(
|
|
|
127
174
|
|
|
128
175
|
Unlike for `modal.Image.from_registry`, the provided recipe will not be embellished with
|
|
129
176
|
steps to install dependencies for the Modal client package. As a consequence, the resulting
|
|
130
|
-
Image cannot be used with a modal Function unless those dependencies are
|
|
131
|
-
|
|
177
|
+
Image cannot be used with a modal Function unless those dependencies are already included
|
|
178
|
+
as part of the registry Image or are added in a subsequent layer. The Image _can_ be
|
|
179
|
+
directly used with a modal Sandbox, which does not need the Modal client.
|
|
132
180
|
|
|
133
181
|
We expect to support this experimental function until the `2025.04` Modal Image Builder is
|
|
134
182
|
stable, at which point Modal Image recipes will no longer install the client dependencies
|
|
@@ -163,47 +211,153 @@ async def raw_registry_image(
|
|
|
163
211
|
)
|
|
164
212
|
|
|
165
213
|
|
|
214
|
+
def _install_cuda_command() -> str:
|
|
215
|
+
"""Command to install CUDA Toolkit (nvcc) inside a container."""
|
|
216
|
+
arch = "x86_64" # instruction set architecture for the CPU, all Modal machines are x86_64
|
|
217
|
+
distro = "debian12" # the distribution and version number of our OS (GNU/Linux)
|
|
218
|
+
filename = "cuda-keyring_1.1-1_all.deb" # NVIDIA signing key file
|
|
219
|
+
cuda_keyring_url = f"https://developer.download.nvidia.com/compute/cuda/repos/{distro}/{arch}/{filename}"
|
|
220
|
+
|
|
221
|
+
major, minor = 12, 8
|
|
222
|
+
max_cuda_version = f"{major}-{minor}"
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
f"wget {cuda_keyring_url} && "
|
|
226
|
+
+ f"dpkg -i {filename} && "
|
|
227
|
+
+ f"rm -f {filename} && "
|
|
228
|
+
+ f"apt-get update && apt-get install -y cuda-nvcc-{max_cuda_version}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
166
232
|
@synchronizer.create_blocking
|
|
167
|
-
async def
|
|
168
|
-
|
|
233
|
+
async def notebook_base_image(*, python_version: Optional[str] = None, force_build: bool = False) -> _Image:
|
|
234
|
+
"""Default image used for Modal notebook kernels, with common libraries.
|
|
235
|
+
|
|
236
|
+
This can be used to bootstrap development workflows quickly. We don't
|
|
237
|
+
recommend using this image for production Modal Functions though, as it may
|
|
238
|
+
change at any time in the future.
|
|
239
|
+
"""
|
|
240
|
+
# Include several common packages, as well as kernelshim dependencies (except 'modal').
|
|
241
|
+
# These packages aren't pinned, so they may change over time with builds.
|
|
242
|
+
#
|
|
243
|
+
# We plan to use `--exclude-newer` in the future, with date-specific image builds.
|
|
244
|
+
base_image = _Image.debian_slim(python_version=python_version)
|
|
245
|
+
|
|
246
|
+
environment_packages: list[str] = [
|
|
247
|
+
"accelerate",
|
|
248
|
+
"aiohttp",
|
|
249
|
+
"altair",
|
|
250
|
+
"anthropic",
|
|
251
|
+
"asyncpg",
|
|
252
|
+
"beautifulsoup4",
|
|
253
|
+
"bokeh",
|
|
254
|
+
"boto3[crt]",
|
|
255
|
+
"click",
|
|
256
|
+
"diffusers[torch,flax]",
|
|
257
|
+
"dm-sonnet",
|
|
258
|
+
"flax",
|
|
259
|
+
"ftfy",
|
|
260
|
+
"h5py",
|
|
261
|
+
"urllib3",
|
|
262
|
+
"httpx",
|
|
263
|
+
"huggingface-hub",
|
|
264
|
+
"ipywidgets",
|
|
265
|
+
"jax[cuda12]",
|
|
266
|
+
"keras",
|
|
267
|
+
"matplotlib",
|
|
268
|
+
"nbformat",
|
|
269
|
+
"numba",
|
|
270
|
+
"numpy",
|
|
271
|
+
"openai",
|
|
272
|
+
"optax",
|
|
273
|
+
"pandas",
|
|
274
|
+
"plotly[express]",
|
|
275
|
+
"polars",
|
|
276
|
+
"psycopg2",
|
|
277
|
+
"requests",
|
|
278
|
+
"safetensors",
|
|
279
|
+
"scikit-image",
|
|
280
|
+
"scikit-learn",
|
|
281
|
+
"scipy",
|
|
282
|
+
"seaborn",
|
|
283
|
+
"sentencepiece",
|
|
284
|
+
"sqlalchemy",
|
|
285
|
+
"statsmodels",
|
|
286
|
+
"sympy",
|
|
287
|
+
"tabulate",
|
|
288
|
+
"tensorboard",
|
|
289
|
+
"toml",
|
|
290
|
+
"transformers",
|
|
291
|
+
"triton",
|
|
292
|
+
"typer",
|
|
293
|
+
"vega-datasets",
|
|
294
|
+
"watchfiles",
|
|
295
|
+
"websockets",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
# Kernelshim dependencies. (see NOTEBOOK_KERNELSHIM_DEPENDENCIES)
|
|
299
|
+
kernelshim_packages: list[str] = [
|
|
300
|
+
"authlib>=1.3",
|
|
301
|
+
"basedpyright>=1.28",
|
|
302
|
+
"fastapi>=0.100",
|
|
303
|
+
"ipykernel>=6",
|
|
304
|
+
"pydantic>=2",
|
|
305
|
+
"pyzmq>=26",
|
|
306
|
+
"ruff>=0.11",
|
|
307
|
+
"uvicorn>=0.32",
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
commands: list[str] = [
|
|
311
|
+
"apt-get update",
|
|
312
|
+
"apt-get install -y "
|
|
313
|
+
+ "libpq-dev pkg-config cmake git curl wget unzip zip libsqlite3-dev openssh-server vim ffmpeg",
|
|
314
|
+
_install_cuda_command(),
|
|
315
|
+
# Install uv since it's faster than pip for installing packages.
|
|
316
|
+
"pip install uv",
|
|
317
|
+
# https://github.com/astral-sh/uv/issues/11480
|
|
318
|
+
"pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu129",
|
|
319
|
+
f"uv pip install --system {shlex.join(sorted(environment_packages))}",
|
|
320
|
+
f"uv pip install --system {shlex.join(sorted(kernelshim_packages))}",
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
324
|
+
return DockerfileSpec(
|
|
325
|
+
commands=[
|
|
326
|
+
"FROM base",
|
|
327
|
+
*(f"RUN {cmd}" for cmd in commands),
|
|
328
|
+
"ENV PATH=/usr/local/cuda/bin:$PATH",
|
|
329
|
+
],
|
|
330
|
+
context_files={},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return _Image._from_args(
|
|
334
|
+
base_images={"base": base_image},
|
|
335
|
+
dockerfile_function=build_dockerfile,
|
|
336
|
+
force_build=force_build,
|
|
337
|
+
_namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@synchronizer.create_blocking
|
|
342
|
+
async def image_delete(
|
|
343
|
+
image_id: str,
|
|
169
344
|
*,
|
|
170
|
-
min_containers: Optional[int] = None,
|
|
171
|
-
max_containers: Optional[int] = None,
|
|
172
|
-
buffer_containers: Optional[int] = None,
|
|
173
|
-
scaledown_window: Optional[int] = None,
|
|
174
345
|
client: Optional[_Client] = None,
|
|
175
346
|
) -> None:
|
|
176
|
-
"""
|
|
347
|
+
"""Delete an Image by its ID.
|
|
348
|
+
|
|
349
|
+
Deletion is irreversible and will prevent Apps from using the Image.
|
|
177
350
|
|
|
178
351
|
This is an experimental interface for a feature that we will be adding to
|
|
179
|
-
|
|
180
|
-
may look different (i.e., it may be a standalone function or a method).
|
|
352
|
+
the main Image class. The stable form of this interface may look different.
|
|
181
353
|
|
|
354
|
+
Note: When building an Image, each chained method call will create an
|
|
355
|
+
intermediate Image layer, each with its own ID. Deleting an Image will not
|
|
356
|
+
delete any of its intermediate layers, only the image identified by the
|
|
357
|
+
provided ID.
|
|
182
358
|
"""
|
|
183
|
-
deprecation_warning(
|
|
184
|
-
(2025, 5, 5),
|
|
185
|
-
"The modal.experimental.update_autoscaler(...) function is now deprecated in favor of"
|
|
186
|
-
" a stable `.update_autoscaler(...) method on the corresponding object.",
|
|
187
|
-
show_source=True,
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
settings = api_pb2.AutoscalerSettings(
|
|
191
|
-
min_containers=min_containers,
|
|
192
|
-
max_containers=max_containers,
|
|
193
|
-
buffer_containers=buffer_containers,
|
|
194
|
-
scaledown_window=scaledown_window,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
359
|
if client is None:
|
|
198
360
|
client = await _Client.from_env()
|
|
199
361
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
else:
|
|
203
|
-
assert obj._cls._class_service_function is not None
|
|
204
|
-
await obj._cls._class_service_function.hydrate(client=client)
|
|
205
|
-
f = obj._cached_service_function()
|
|
206
|
-
await f.hydrate(client=client)
|
|
207
|
-
|
|
208
|
-
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=f.object_id, settings=settings)
|
|
209
|
-
await retry_transient_errors(client.stub.FunctionUpdateSchedulingParams, request)
|
|
362
|
+
req = api_pb2.ImageDeleteRequest(image_id=image_id)
|
|
363
|
+
await client.stub.ImageDelete(req)
|