modal 0.68.42__py3-none-any.whl → 0.70.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/_container_entrypoint.py +1 -1
- modal/_runtime/asgi.py +11 -4
- modal/_runtime/container_io_manager.py +12 -19
- modal/_utils/docker_utils.py +64 -0
- modal/_utils/function_utils.py +24 -13
- modal/cli/launch.py +2 -0
- modal/cli/programs/vscode.py +27 -2
- modal/client.pyi +2 -2
- modal/file_io.py +16 -10
- modal/file_pattern_matcher.py +11 -1
- modal/functions.py +1 -1
- modal/functions.pyi +6 -6
- modal/gpu.py +22 -0
- modal/image.py +95 -39
- modal/image.pyi +11 -2
- modal/mount.py +10 -9
- modal/mount.pyi +4 -4
- modal/partial_function.py +4 -4
- modal/runner.py +9 -5
- modal/running_app.py +27 -1
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/METADATA +2 -2
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/RECORD +35 -34
- modal_proto/api.proto +9 -0
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +785 -765
- modal_proto/api_pb2.pyi +30 -0
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/LICENSE +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/WHEEL +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/entry_points.txt +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -464,7 +464,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
464
464
|
batch_wait_ms = function_def.batch_linger_ms or 0
|
465
465
|
|
466
466
|
# Get ids and metadata for objects (primarily functions and classes) on the app
|
467
|
-
container_app: RunningApp = container_io_manager.get_app_objects()
|
467
|
+
container_app: RunningApp = container_io_manager.get_app_objects(container_args.app_layout)
|
468
468
|
|
469
469
|
# Initialize objects on the app.
|
470
470
|
# This is basically only functions and classes - anything else is deprecated and will be unsupported soon
|
modal/_runtime/asgi.py
CHANGED
@@ -26,6 +26,7 @@ class LifespanManager:
|
|
26
26
|
shutdown: asyncio.Future
|
27
27
|
queue: asyncio.Queue
|
28
28
|
has_run_init: bool = False
|
29
|
+
lifespan_supported: bool = False
|
29
30
|
|
30
31
|
def __init__(self, asgi_app, state):
|
31
32
|
self.asgi_app = asgi_app
|
@@ -46,6 +47,7 @@ class LifespanManager:
|
|
46
47
|
await self.ensure_init()
|
47
48
|
|
48
49
|
async def receive():
|
50
|
+
self.lifespan_supported = True
|
49
51
|
return await self.queue.get()
|
50
52
|
|
51
53
|
async def send(message):
|
@@ -63,16 +65,21 @@ class LifespanManager:
|
|
63
65
|
try:
|
64
66
|
await self.asgi_app({"type": "lifespan", "state": self.state}, receive, send)
|
65
67
|
except Exception as e:
|
68
|
+
if not self.lifespan_supported:
|
69
|
+
logger.info(f"ASGI lifespan task exited before receiving any messages with exception:\n{e}")
|
70
|
+
self.startup.set_result(None)
|
71
|
+
self.shutdown.set_result(None)
|
72
|
+
return
|
73
|
+
|
66
74
|
logger.error(f"Error in ASGI lifespan task: {e}")
|
67
75
|
if not self.startup.done():
|
68
76
|
self.startup.set_exception(ExecutionError("ASGI lifespan task exited startup"))
|
69
77
|
if not self.shutdown.done():
|
70
78
|
self.shutdown.set_exception(ExecutionError("ASGI lifespan task exited shutdown"))
|
71
79
|
else:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
self.shutdown.set_result("ASGI Lifespan protocol is probably not supported by this library")
|
80
|
+
logger.info("ASGI Lifespan protocol is probably not supported by this library")
|
81
|
+
self.startup.set_result(None)
|
82
|
+
self.shutdown.set_result(None)
|
76
83
|
|
77
84
|
async def lifespan_startup(self):
|
78
85
|
await self.ensure_init()
|
@@ -21,7 +21,6 @@ from typing import (
|
|
21
21
|
)
|
22
22
|
|
23
23
|
from google.protobuf.empty_pb2 import Empty
|
24
|
-
from google.protobuf.message import Message
|
25
24
|
from grpclib import Status
|
26
25
|
from synchronicity.async_wrap import asynccontextmanager
|
27
26
|
|
@@ -31,12 +30,12 @@ from modal._traceback import extract_traceback, print_exception
|
|
31
30
|
from modal._utils.async_utils import TaskContext, asyncify, synchronize_api, synchronizer
|
32
31
|
from modal._utils.blob_utils import MAX_OBJECT_SIZE_BYTES, blob_download, blob_upload
|
33
32
|
from modal._utils.function_utils import _stream_function_call_data
|
34
|
-
from modal._utils.grpc_utils import
|
33
|
+
from modal._utils.grpc_utils import retry_transient_errors
|
35
34
|
from modal._utils.package_utils import parse_major_minor_version
|
36
35
|
from modal.client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
37
36
|
from modal.config import config, logger
|
38
37
|
from modal.exception import ClientClosed, InputCancellation, InvalidError, SerializationError
|
39
|
-
from modal.running_app import RunningApp
|
38
|
+
from modal.running_app import RunningApp, running_app_from_layout
|
40
39
|
from modal_proto import api_pb2
|
41
40
|
|
42
41
|
if TYPE_CHECKING:
|
@@ -450,25 +449,19 @@ class _ContainerIOManager:
|
|
450
449
|
|
451
450
|
await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
|
452
451
|
|
453
|
-
async def get_app_objects(self) -> RunningApp:
|
454
|
-
|
455
|
-
|
456
|
-
|
452
|
+
async def get_app_objects(self, app_layout: api_pb2.AppLayout) -> RunningApp:
|
453
|
+
if len(app_layout.objects) == 0:
|
454
|
+
# TODO(erikbern): this should never happen! let's keep it in here for a short second
|
455
|
+
# until we've sanity checked that this is, in fact, dead code.
|
456
|
+
req = api_pb2.AppGetLayoutRequest(app_id=self.app_id)
|
457
|
+
resp = await retry_transient_errors(self._client.stub.AppGetLayout, req)
|
458
|
+
app_layout = resp.app_layout
|
457
459
|
|
458
|
-
|
459
|
-
object_handle_metadata = {}
|
460
|
-
for item in resp.items:
|
461
|
-
handle_metadata: Optional[Message] = get_proto_oneof(item.object, "handle_metadata_oneof")
|
462
|
-
object_handle_metadata[item.object.object_id] = handle_metadata
|
463
|
-
if item.tag:
|
464
|
-
tag_to_object_id[item.tag] = item.object.object_id
|
465
|
-
|
466
|
-
return RunningApp(
|
460
|
+
return running_app_from_layout(
|
467
461
|
self.app_id,
|
462
|
+
app_layout,
|
463
|
+
self._client,
|
468
464
|
environment_name=self._environment_name,
|
469
|
-
tag_to_object_id=tag_to_object_id,
|
470
|
-
object_handle_metadata=object_handle_metadata,
|
471
|
-
client=self._client,
|
472
465
|
)
|
473
466
|
|
474
467
|
async def get_serialized_function(self) -> tuple[Optional[Any], Optional[Callable[..., Any]]]:
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
import re
|
3
|
+
import shlex
|
4
|
+
from typing import Sequence
|
5
|
+
|
6
|
+
from ..exception import InvalidError
|
7
|
+
|
8
|
+
|
9
|
+
def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
|
10
|
+
"""
|
11
|
+
Extract all COPY command sources from a Dockerfile.
|
12
|
+
Combines multiline COPY commands into a single line.
|
13
|
+
"""
|
14
|
+
copy_source_patterns: set[str] = set()
|
15
|
+
current_command = ""
|
16
|
+
copy_pattern = re.compile(r"^\s*COPY\s+(.+)$", re.IGNORECASE)
|
17
|
+
|
18
|
+
# First pass: handle line continuations and collect full commands
|
19
|
+
for line in dockerfile_lines:
|
20
|
+
line = line.strip()
|
21
|
+
if not line or line.startswith("#"):
|
22
|
+
# ignore comments and empty lines
|
23
|
+
continue
|
24
|
+
|
25
|
+
if current_command:
|
26
|
+
# Continue previous line
|
27
|
+
current_command += " " + line.rstrip("\\").strip()
|
28
|
+
else:
|
29
|
+
# Start new command
|
30
|
+
current_command = line.rstrip("\\").strip()
|
31
|
+
|
32
|
+
if not line.endswith("\\"):
|
33
|
+
# Command is complete
|
34
|
+
|
35
|
+
match = copy_pattern.match(current_command)
|
36
|
+
if match:
|
37
|
+
args = match.group(1)
|
38
|
+
parts = shlex.split(args)
|
39
|
+
|
40
|
+
# COPY --from=... commands reference external sources and do not need a context mount.
|
41
|
+
# https://docs.docker.com/reference/dockerfile/#copy---from
|
42
|
+
if parts[0].startswith("--from="):
|
43
|
+
current_command = ""
|
44
|
+
continue
|
45
|
+
|
46
|
+
if len(parts) >= 2:
|
47
|
+
# Last part is destination, everything else is a mount source
|
48
|
+
sources = parts[:-1]
|
49
|
+
|
50
|
+
for source in sources:
|
51
|
+
special_pattern = re.compile(r"^\s*--|\$\s*")
|
52
|
+
if special_pattern.match(source):
|
53
|
+
raise InvalidError(
|
54
|
+
f"COPY command: {source} using special flags/arguments/variables are not supported"
|
55
|
+
)
|
56
|
+
|
57
|
+
if source == ".":
|
58
|
+
copy_source_patterns.add("./**")
|
59
|
+
else:
|
60
|
+
copy_source_patterns.add(source)
|
61
|
+
|
62
|
+
current_command = ""
|
63
|
+
|
64
|
+
return list(copy_source_patterns)
|
modal/_utils/function_utils.py
CHANGED
@@ -99,16 +99,18 @@ def get_function_type(is_generator: Optional[bool]) -> "api_pb2.Function.Functio
|
|
99
99
|
|
100
100
|
|
101
101
|
class FunctionInfo:
|
102
|
-
"""
|
102
|
+
"""Utility that determines serialization/deserialization mechanisms for functions
|
103
103
|
|
104
|
-
|
105
|
-
|
104
|
+
* Stored as file vs serialized
|
105
|
+
* If serialized: how to serialize the function
|
106
|
+
* If file: which module/function name should be used to retrieve
|
107
|
+
|
108
|
+
Used for populating the definition of a remote function
|
106
109
|
"""
|
107
110
|
|
108
111
|
raw_f: Optional[Callable[..., Any]] # if None - this is a "class service function"
|
109
112
|
function_name: str
|
110
113
|
user_cls: Optional[type[Any]]
|
111
|
-
definition_type: "modal_proto.api_pb2.Function.DefinitionType.ValueType"
|
112
114
|
module_name: Optional[str]
|
113
115
|
|
114
116
|
_type: FunctionInfoType
|
@@ -116,6 +118,12 @@ class FunctionInfo:
|
|
116
118
|
_base_dir: str
|
117
119
|
_remote_dir: Optional[PurePosixPath] = None
|
118
120
|
|
121
|
+
def get_definition_type(self) -> "modal_proto.api_pb2.Function.DefinitionType.ValueType":
|
122
|
+
if self.is_serialized():
|
123
|
+
return modal_proto.api_pb2.Function.DEFINITION_TYPE_SERIALIZED
|
124
|
+
else:
|
125
|
+
return modal_proto.api_pb2.Function.DEFINITION_TYPE_FILE
|
126
|
+
|
119
127
|
def is_service_class(self):
|
120
128
|
if self.raw_f is None:
|
121
129
|
assert self.user_cls
|
@@ -172,7 +180,7 @@ class FunctionInfo:
|
|
172
180
|
self._base_dir = base_dirs[0]
|
173
181
|
self.module_name = module.__spec__.name
|
174
182
|
self._remote_dir = ROOT_DIR / PurePosixPath(module.__package__.split(".")[0])
|
175
|
-
self.
|
183
|
+
self._is_serialized = False
|
176
184
|
self._type = FunctionInfoType.PACKAGE
|
177
185
|
elif hasattr(module, "__file__") and not serialized:
|
178
186
|
# This generally covers the case where it's invoked with
|
@@ -182,18 +190,18 @@ class FunctionInfo:
|
|
182
190
|
self._file = os.path.abspath(inspect.getfile(module))
|
183
191
|
self.module_name = inspect.getmodulename(self._file)
|
184
192
|
self._base_dir = os.path.dirname(self._file)
|
185
|
-
self.
|
193
|
+
self._is_serialized = False
|
186
194
|
self._type = FunctionInfoType.FILE
|
187
195
|
else:
|
188
196
|
self.module_name = None
|
189
197
|
self._base_dir = os.path.abspath("") # get current dir
|
190
|
-
self.
|
191
|
-
if serialized:
|
198
|
+
self._is_serialized = True # either explicitly, or by being in a notebook
|
199
|
+
if serialized: # if explicit
|
192
200
|
self._type = FunctionInfoType.SERIALIZED
|
193
201
|
else:
|
194
202
|
self._type = FunctionInfoType.NOTEBOOK
|
195
203
|
|
196
|
-
if self.
|
204
|
+
if not self.is_serialized():
|
197
205
|
# Sanity check that this function is defined in global scope
|
198
206
|
# Unfortunately, there's no "clean" way to do this in Python
|
199
207
|
qualname = f.__qualname__ if f else user_cls.__qualname__
|
@@ -203,7 +211,7 @@ class FunctionInfo:
|
|
203
211
|
)
|
204
212
|
|
205
213
|
def is_serialized(self) -> bool:
|
206
|
-
return self.
|
214
|
+
return self._is_serialized
|
207
215
|
|
208
216
|
def serialized_function(self) -> bytes:
|
209
217
|
# Note: this should only be called from .load() and not at function decoration time
|
@@ -312,7 +320,7 @@ class FunctionInfo:
|
|
312
320
|
if self._type == FunctionInfoType.PACKAGE:
|
313
321
|
if config.get("automount"):
|
314
322
|
return [_Mount.from_local_python_packages(self.module_name)]
|
315
|
-
elif self.
|
323
|
+
elif not self.is_serialized():
|
316
324
|
# mount only relevant file and __init__.py:s
|
317
325
|
return [
|
318
326
|
_Mount.from_local_dir(
|
@@ -322,7 +330,7 @@ class FunctionInfo:
|
|
322
330
|
condition=entrypoint_only_package_mount_condition(self._file),
|
323
331
|
)
|
324
332
|
]
|
325
|
-
elif self.
|
333
|
+
elif not self.is_serialized():
|
326
334
|
remote_path = ROOT_DIR / Path(self._file).name
|
327
335
|
if not _is_modal_path(remote_path):
|
328
336
|
return [
|
@@ -570,12 +578,15 @@ class FunctionCreationStatus:
|
|
570
578
|
|
571
579
|
elif self.response.function.web_url:
|
572
580
|
url_info = self.response.function.web_url_info
|
581
|
+
requires_proxy_auth = self.response.function.webhook_config.requires_proxy_auth
|
582
|
+
proxy_auth_suffix = " 🔑" if requires_proxy_auth else ""
|
573
583
|
# Ensure terms used here match terms used in modal.com/docs/guide/webhook-urls doc.
|
574
584
|
suffix = _get_suffix_from_web_url_info(url_info)
|
575
585
|
# TODO: this is only printed when we're showing progress. Maybe move this somewhere else.
|
576
586
|
web_url = self.response.handle_metadata.web_url
|
577
587
|
self.status_row.finish(
|
578
|
-
f"Created web function {self.tag} => [magenta underline]{web_url}[/magenta underline]
|
588
|
+
f"Created web function {self.tag} => [magenta underline]{web_url}[/magenta underline]"
|
589
|
+
f"{proxy_auth_suffix}{suffix}"
|
579
590
|
)
|
580
591
|
|
581
592
|
# Print custom domain in terminal
|
modal/cli/launch.py
CHANGED
@@ -77,6 +77,7 @@ def vscode(
|
|
77
77
|
cpu: int = 8,
|
78
78
|
memory: int = 32768,
|
79
79
|
gpu: Optional[str] = None,
|
80
|
+
image: str = "debian:12",
|
80
81
|
timeout: int = 3600,
|
81
82
|
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
82
83
|
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
@@ -86,6 +87,7 @@ def vscode(
|
|
86
87
|
"cpu": cpu,
|
87
88
|
"memory": memory,
|
88
89
|
"gpu": gpu,
|
90
|
+
"image": image,
|
89
91
|
"timeout": timeout,
|
90
92
|
"mount": mount,
|
91
93
|
"volume": volume,
|
modal/cli/programs/vscode.py
CHANGED
@@ -15,9 +15,34 @@ from modal import App, Image, Mount, Queue, Secret, Volume, forward
|
|
15
15
|
# Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
|
16
16
|
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
17
17
|
|
18
|
+
CODE_SERVER_INSTALLER = "https://code-server.dev/install.sh"
|
19
|
+
CODE_SERVER_ENTRYPOINT = (
|
20
|
+
"https://raw.githubusercontent.com/coder/code-server/refs/tags/v4.96.1/ci/release-image/entrypoint.sh"
|
21
|
+
)
|
22
|
+
FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz"
|
23
|
+
|
18
24
|
|
19
25
|
app = App()
|
20
|
-
app.image =
|
26
|
+
app.image = (
|
27
|
+
Image.from_registry(args.get("image"), add_python="3.11")
|
28
|
+
.apt_install("curl", "dumb-init", "git", "git-lfs")
|
29
|
+
.run_commands(
|
30
|
+
f"curl -fsSL {CODE_SERVER_INSTALLER} | sh",
|
31
|
+
f"curl -fsSL {CODE_SERVER_ENTRYPOINT} > /code-server.sh",
|
32
|
+
"chmod u+x /code-server.sh",
|
33
|
+
)
|
34
|
+
.run_commands(
|
35
|
+
'ARCH="$(dpkg --print-architecture)"'
|
36
|
+
f' && curl -fsSL "{FIXUD_INSTALLER}" | tar -C /usr/local/bin -xzf - '
|
37
|
+
" && chown root:root /usr/local/bin/fixuid"
|
38
|
+
" && chmod 4755 /usr/local/bin/fixuid"
|
39
|
+
" && mkdir -p /etc/fixuid"
|
40
|
+
' && echo "user: root" >> /etc/fixuid/config.yml'
|
41
|
+
' && echo "group: root" >> /etc/fixuid/config.yml'
|
42
|
+
)
|
43
|
+
.run_commands("mkdir /home/coder")
|
44
|
+
.env({"ENTRYPOINTD": ""})
|
45
|
+
)
|
21
46
|
|
22
47
|
|
23
48
|
mount = (
|
@@ -71,7 +96,7 @@ def run_vscode(q: Queue):
|
|
71
96
|
url = tunnel.url
|
72
97
|
threading.Thread(target=wait_for_port, args=((url, token), q)).start()
|
73
98
|
subprocess.run(
|
74
|
-
["/
|
99
|
+
["/code-server.sh", "--bind-addr", "0.0.0.0:8080", "."],
|
75
100
|
env={**os.environ, "SHELL": "/bin/bash", "PASSWORD": token},
|
76
101
|
)
|
77
102
|
q.put("done")
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.70.2"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.70.2"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/file_io.py
CHANGED
@@ -18,7 +18,8 @@ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
18
18
|
from .client import _Client
|
19
19
|
from .exception import FilesystemExecutionError, InvalidError
|
20
20
|
|
21
|
-
|
21
|
+
WRITE_CHUNK_SIZE = 16 * 1024 * 1024 # 16 MiB
|
22
|
+
WRITE_FILE_SIZE_LIMIT = 1024 * 1024 * 1024 # 1 GiB
|
22
23
|
READ_FILE_SIZE_LIMIT = 100 * 1024 * 1024 # 100 MiB
|
23
24
|
|
24
25
|
ERROR_MAPPING = {
|
@@ -77,7 +78,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
|
|
77
78
|
if start is not None and end is not None:
|
78
79
|
if start >= end:
|
79
80
|
raise InvalidError("start must be less than end")
|
80
|
-
if len(data) >
|
81
|
+
if len(data) > WRITE_CHUNK_SIZE:
|
81
82
|
raise InvalidError("Write request payload exceeds 16 MiB limit")
|
82
83
|
resp = await file._make_request(
|
83
84
|
api_pb2.ContainerFilesystemExecRequest(
|
@@ -288,15 +289,20 @@ class _FileIO(Generic[T]):
|
|
288
289
|
self._validate_type(data)
|
289
290
|
if isinstance(data, str):
|
290
291
|
data = data.encode("utf-8")
|
291
|
-
if len(data) >
|
292
|
-
raise ValueError("Write request payload exceeds
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
292
|
+
if len(data) > WRITE_FILE_SIZE_LIMIT:
|
293
|
+
raise ValueError("Write request payload exceeds 1 GiB limit")
|
294
|
+
for i in range(0, len(data), WRITE_CHUNK_SIZE):
|
295
|
+
chunk = data[i : i + WRITE_CHUNK_SIZE]
|
296
|
+
resp = await self._make_request(
|
297
|
+
api_pb2.ContainerFilesystemExecRequest(
|
298
|
+
file_write_request=api_pb2.ContainerFileWriteRequest(
|
299
|
+
file_descriptor=self._file_descriptor,
|
300
|
+
data=chunk,
|
301
|
+
),
|
302
|
+
task_id=self._task_id,
|
303
|
+
)
|
297
304
|
)
|
298
|
-
|
299
|
-
await self._wait(resp.exec_id)
|
305
|
+
await self._wait(resp.exec_id)
|
300
306
|
|
301
307
|
async def flush(self) -> None:
|
302
308
|
"""Flush the buffer to disk."""
|
modal/file_pattern_matcher.py
CHANGED
@@ -12,7 +12,7 @@ then asking it whether file paths match any of its patterns.
|
|
12
12
|
import os
|
13
13
|
from abc import abstractmethod
|
14
14
|
from pathlib import Path
|
15
|
-
from typing import Callable, Optional
|
15
|
+
from typing import Callable, Optional, Sequence, Union
|
16
16
|
|
17
17
|
from ._utils.pattern_utils import Pattern
|
18
18
|
|
@@ -152,3 +152,13 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
152
152
|
# with_repr allows us to use this matcher as a default value in a function signature
|
153
153
|
# and get a nice repr in the docs and auto-generated type stubs:
|
154
154
|
NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py")).with_repr(f"{__name__}.NON_PYTHON_FILES")
|
155
|
+
_NOTHING = (~FilePatternMatcher()).with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
|
156
|
+
|
157
|
+
|
158
|
+
def _ignore_fn(ignore: Union[Sequence[str], Callable[[Path], bool]]) -> Callable[[Path], bool]:
|
159
|
+
# if a callable is passed, return it
|
160
|
+
# otherwise, treat input as a sequence of patterns and return a callable pattern matcher for those
|
161
|
+
if callable(ignore):
|
162
|
+
return ignore
|
163
|
+
|
164
|
+
return FilePatternMatcher(*ignore)
|
modal/functions.py
CHANGED
@@ -753,7 +753,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
753
753
|
mount_ids=loaded_mount_ids,
|
754
754
|
secret_ids=[secret.object_id for secret in secrets],
|
755
755
|
image_id=(image.object_id if image else ""),
|
756
|
-
definition_type=info.
|
756
|
+
definition_type=info.get_definition_type(),
|
757
757
|
function_serialized=function_serialized or b"",
|
758
758
|
class_serialized=class_serialized or b"",
|
759
759
|
function_type=function_type,
|
modal/functions.pyi
CHANGED
@@ -462,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
462
462
|
|
463
463
|
_call_generator_nowait: ___call_generator_nowait_spec
|
464
464
|
|
465
|
-
class __remote_spec(typing_extensions.Protocol[
|
465
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
466
466
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
467
467
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
468
468
|
|
469
|
-
remote: __remote_spec[
|
469
|
+
remote: __remote_spec[ReturnType, P]
|
470
470
|
|
471
471
|
class __remote_gen_spec(typing_extensions.Protocol):
|
472
472
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -479,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
479
479
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
480
480
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
481
481
|
|
482
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
482
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
483
483
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
484
484
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
485
485
|
|
486
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
486
|
+
_experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
|
487
487
|
|
488
|
-
class __spawn_spec(typing_extensions.Protocol[
|
488
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
489
489
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
490
490
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
491
491
|
|
492
|
-
spawn: __spawn_spec[
|
492
|
+
spawn: __spawn_spec[ReturnType, P]
|
493
493
|
|
494
494
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
495
495
|
|
modal/gpu.py
CHANGED
@@ -137,6 +137,27 @@ class H100(_GPUConfig):
|
|
137
137
|
return f"GPU(H100, count={self.count})"
|
138
138
|
|
139
139
|
|
140
|
+
class L40S(_GPUConfig):
|
141
|
+
"""
|
142
|
+
[NVIDIA L40S](https://www.nvidia.com/en-us/data-center/l40s/) GPU class.
|
143
|
+
|
144
|
+
The L40S is a data center GPU for the Ada Lovelace architecture. It has 48 GB of on-chip
|
145
|
+
GDDR6 RAM and enhanced support for FP8 precision.
|
146
|
+
"""
|
147
|
+
|
148
|
+
def __init__(
|
149
|
+
self,
|
150
|
+
*,
|
151
|
+
# Number of GPUs per container. Defaults to 1.
|
152
|
+
# Useful if you have very large models that don't fit on a single GPU.
|
153
|
+
count: int = 1,
|
154
|
+
):
|
155
|
+
super().__init__(api_pb2.GPU_TYPE_L40S, count)
|
156
|
+
|
157
|
+
def __repr__(self):
|
158
|
+
return f"GPU(L40S, count={self.count})"
|
159
|
+
|
160
|
+
|
140
161
|
class Any(_GPUConfig):
|
141
162
|
"""Selects any one of the GPU classes available within Modal, according to availability."""
|
142
163
|
|
@@ -154,6 +175,7 @@ STRING_TO_GPU_CONFIG: dict[str, Callable] = {
|
|
154
175
|
"a100-80gb": lambda: A100(size="80GB"),
|
155
176
|
"h100": H100,
|
156
177
|
"a10g": A10G,
|
178
|
+
"l40s": L40S,
|
157
179
|
"any": Any,
|
158
180
|
}
|
159
181
|
display_string_to_config = "\n".join(f'- "{key}" → `{c()}`' for key, c in STRING_TO_GPU_CONFIG.items() if key != "inf2")
|