modal 1.1.1.dev41__py3-none-any.whl → 1.1.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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__main__.py +1 -2
- modal/_container_entrypoint.py +18 -7
- modal/_functions.py +135 -13
- modal/_object.py +13 -2
- modal/_partial_function.py +8 -8
- modal/_runtime/asgi.py +3 -2
- modal/_runtime/container_io_manager.py +20 -14
- modal/_runtime/container_io_manager.pyi +38 -13
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +158 -54
- modal/_utils/blob_utils.py +83 -24
- modal/_utils/function_utils.py +4 -3
- modal/_utils/time_utils.py +28 -4
- modal/app.py +8 -4
- modal/app.pyi +8 -8
- modal/cli/dict.py +14 -11
- modal/cli/entry_point.py +9 -3
- modal/cli/launch.py +102 -4
- modal/cli/profile.py +1 -0
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/queues.py +49 -19
- modal/cli/secret.py +45 -18
- modal/cli/volume.py +14 -16
- modal/client.pyi +2 -10
- modal/cls.py +12 -2
- modal/cls.pyi +9 -1
- modal/config.py +7 -7
- modal/dict.py +206 -12
- modal/dict.pyi +358 -4
- modal/experimental/__init__.py +130 -0
- modal/file_io.py +1 -1
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +111 -11
- modal/image.py +9 -3
- modal/image.pyi +7 -7
- modal/mount.py +20 -13
- modal/mount.pyi +16 -3
- modal/network_file_system.py +8 -2
- modal/object.pyi +3 -0
- modal/parallel_map.py +346 -101
- modal/parallel_map.pyi +108 -0
- modal/proxy.py +2 -1
- modal/queue.py +199 -9
- modal/queue.pyi +357 -3
- modal/sandbox.py +6 -5
- modal/sandbox.pyi +17 -14
- modal/secret.py +196 -3
- modal/secret.pyi +372 -0
- modal/volume.py +239 -23
- modal/volume.pyi +405 -10
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/METADATA +2 -2
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/RECORD +68 -66
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +37 -10
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +627 -597
- modal_proto/api_pb2.pyi +107 -19
- modal_proto/api_pb2_grpc.py +67 -2
- modal_proto/api_pb2_grpc.pyi +24 -8
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/__init__.py +1 -1
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/WHEEL +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/entry_points.txt +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/top_level.txt +0 -0
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
|
|
@@ -212,6 +213,135 @@ async def raw_registry_image(
|
|
|
212
213
|
)
|
|
213
214
|
|
|
214
215
|
|
|
216
|
+
def _install_cuda_command() -> str:
|
|
217
|
+
"""Command to install CUDA Toolkit (nvcc) inside a container."""
|
|
218
|
+
arch = "x86_64" # instruction set architecture for the CPU, all Modal machines are x86_64
|
|
219
|
+
distro = "debian12" # the distribution and version number of our OS (GNU/Linux)
|
|
220
|
+
filename = "cuda-keyring_1.1-1_all.deb" # NVIDIA signing key file
|
|
221
|
+
cuda_keyring_url = f"https://developer.download.nvidia.com/compute/cuda/repos/{distro}/{arch}/{filename}"
|
|
222
|
+
|
|
223
|
+
major, minor = 12, 8
|
|
224
|
+
max_cuda_version = f"{major}-{minor}"
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
f"wget {cuda_keyring_url} && "
|
|
228
|
+
+ f"dpkg -i {filename} && "
|
|
229
|
+
+ f"rm -f {filename} && "
|
|
230
|
+
+ f"apt-get update && apt-get install -y cuda-nvcc-{max_cuda_version}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@synchronizer.create_blocking
|
|
235
|
+
async def notebook_base_image(*, python_version: Optional[str] = None, force_build: bool = False) -> _Image:
|
|
236
|
+
"""Default image used for Modal notebook kernels, with common libraries.
|
|
237
|
+
|
|
238
|
+
This can be used to bootstrap development workflows quickly. We don't
|
|
239
|
+
recommend using this image for production Modal Functions though, as it may
|
|
240
|
+
change at any time in the future.
|
|
241
|
+
"""
|
|
242
|
+
# Include several common packages, as well as kernelshim dependencies (except 'modal').
|
|
243
|
+
# These packages aren't pinned, so they may change over time with builds.
|
|
244
|
+
#
|
|
245
|
+
# We plan to use `--exclude-newer` in the future, with date-specific image builds.
|
|
246
|
+
base_image = _Image.debian_slim(python_version=python_version)
|
|
247
|
+
|
|
248
|
+
environment_packages: list[str] = [
|
|
249
|
+
"accelerate",
|
|
250
|
+
"aiohttp",
|
|
251
|
+
"altair",
|
|
252
|
+
"anthropic",
|
|
253
|
+
"asyncpg",
|
|
254
|
+
"beautifulsoup4",
|
|
255
|
+
"bokeh",
|
|
256
|
+
"boto3[crt]",
|
|
257
|
+
"click",
|
|
258
|
+
"diffusers[torch,flax]",
|
|
259
|
+
"dm-sonnet",
|
|
260
|
+
"flax",
|
|
261
|
+
"ftfy",
|
|
262
|
+
"h5py",
|
|
263
|
+
"urllib3",
|
|
264
|
+
"httpx",
|
|
265
|
+
"huggingface-hub",
|
|
266
|
+
"ipywidgets",
|
|
267
|
+
"jax[cuda12]",
|
|
268
|
+
"keras",
|
|
269
|
+
"matplotlib",
|
|
270
|
+
"nbformat",
|
|
271
|
+
"numba",
|
|
272
|
+
"numpy",
|
|
273
|
+
"openai",
|
|
274
|
+
"optax",
|
|
275
|
+
"pandas",
|
|
276
|
+
"plotly[express]",
|
|
277
|
+
"polars",
|
|
278
|
+
"psycopg2",
|
|
279
|
+
"requests",
|
|
280
|
+
"safetensors",
|
|
281
|
+
"scikit-image",
|
|
282
|
+
"scikit-learn",
|
|
283
|
+
"scipy",
|
|
284
|
+
"seaborn",
|
|
285
|
+
"sentencepiece",
|
|
286
|
+
"sqlalchemy",
|
|
287
|
+
"statsmodels",
|
|
288
|
+
"sympy",
|
|
289
|
+
"tabulate",
|
|
290
|
+
"tensorboard",
|
|
291
|
+
"toml",
|
|
292
|
+
"transformers",
|
|
293
|
+
"triton",
|
|
294
|
+
"typer",
|
|
295
|
+
"vega-datasets",
|
|
296
|
+
"watchfiles",
|
|
297
|
+
"websockets",
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
# Kernelshim dependencies. (see NOTEBOOK_KERNELSHIM_DEPENDENCIES)
|
|
301
|
+
kernelshim_packages: list[str] = [
|
|
302
|
+
"authlib>=1.3",
|
|
303
|
+
"basedpyright>=1.28",
|
|
304
|
+
"fastapi>=0.100",
|
|
305
|
+
"ipykernel>=6",
|
|
306
|
+
"pydantic>=2",
|
|
307
|
+
"pyzmq>=26",
|
|
308
|
+
"ruff>=0.11",
|
|
309
|
+
"uvicorn>=0.32",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
commands: list[str] = [
|
|
313
|
+
"apt-get update",
|
|
314
|
+
"apt-get install -y libpq-dev pkg-config cmake git curl wget unzip zip libsqlite3-dev openssh-server vim",
|
|
315
|
+
_install_cuda_command(),
|
|
316
|
+
# Install uv since it's faster than pip for installing packages.
|
|
317
|
+
"pip install uv",
|
|
318
|
+
# https://github.com/astral-sh/uv/issues/11480
|
|
319
|
+
"pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126",
|
|
320
|
+
f"uv pip install --system {shlex.join(sorted(environment_packages))}",
|
|
321
|
+
f"uv pip install --system {shlex.join(sorted(kernelshim_packages))}",
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
# TODO: Also install the CUDA Toolkit, so `nvcc` is available.
|
|
325
|
+
# https://github.com/charlesfrye/cuda-modal/blob/7fef8db12402986cf42d9c8cca8c63d1da6d7700/cuda/use_cuda.py#L158-L188
|
|
326
|
+
|
|
327
|
+
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
328
|
+
return DockerfileSpec(
|
|
329
|
+
commands=[
|
|
330
|
+
"FROM base",
|
|
331
|
+
*(f"RUN {cmd}" for cmd in commands),
|
|
332
|
+
"ENV PATH=/usr/local/cuda/bin:$PATH",
|
|
333
|
+
],
|
|
334
|
+
context_files={},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return _Image._from_args(
|
|
338
|
+
base_images={"base": base_image},
|
|
339
|
+
dockerfile_function=build_dockerfile,
|
|
340
|
+
force_build=force_build,
|
|
341
|
+
_namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
215
345
|
@synchronizer.create_blocking
|
|
216
346
|
async def update_autoscaler(
|
|
217
347
|
obj: Union[_Function, _Obj],
|
modal/file_io.py
CHANGED
modal/file_io.pyi
CHANGED
|
@@ -62,7 +62,7 @@ class _FileIO(typing.Generic[T]):
|
|
|
62
62
|
|
|
63
63
|
**Usage**
|
|
64
64
|
|
|
65
|
-
```python
|
|
65
|
+
```python notest
|
|
66
66
|
import modal
|
|
67
67
|
|
|
68
68
|
app = modal.App.lookup("my-app", create_if_missing=True)
|
|
@@ -232,7 +232,7 @@ class FileIO(typing.Generic[T]):
|
|
|
232
232
|
|
|
233
233
|
**Usage**
|
|
234
234
|
|
|
235
|
-
```python
|
|
235
|
+
```python notest
|
|
236
236
|
import modal
|
|
237
237
|
|
|
238
238
|
app = modal.App.lookup("my-app", create_if_missing=True)
|
modal/file_pattern_matcher.py
CHANGED
|
@@ -11,6 +11,7 @@ then asking it whether file paths match any of its patterns.
|
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
from abc import abstractmethod
|
|
14
|
+
from functools import cached_property
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Callable, Optional, Sequence, Union
|
|
16
17
|
|
|
@@ -99,11 +100,11 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
99
100
|
```
|
|
100
101
|
"""
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
_file_path: Optional[Union[str, Path]]
|
|
104
|
+
_pattern_strings: Optional[Sequence[str]]
|
|
104
105
|
|
|
105
|
-
def
|
|
106
|
-
|
|
106
|
+
def _parse_patterns(self, patterns: Sequence[str]) -> list[Pattern]:
|
|
107
|
+
parsed_patterns = []
|
|
107
108
|
for pattern in list(patterns):
|
|
108
109
|
pattern = pattern.strip().strip(os.path.sep)
|
|
109
110
|
if not pattern:
|
|
@@ -118,7 +119,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
118
119
|
# In Python, we can proceed without explicit syntax checking
|
|
119
120
|
new_pattern.cleaned_pattern = pattern
|
|
120
121
|
new_pattern.dirs = pattern.split(os.path.sep)
|
|
121
|
-
|
|
122
|
+
parsed_patterns.append(new_pattern)
|
|
123
|
+
return parsed_patterns
|
|
122
124
|
|
|
123
125
|
def __init__(self, *pattern: str) -> None:
|
|
124
126
|
"""Initialize a new FilePatternMatcher instance.
|
|
@@ -129,7 +131,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
129
131
|
Raises:
|
|
130
132
|
ValueError: If an illegal exclusion pattern is provided.
|
|
131
133
|
"""
|
|
132
|
-
self.
|
|
134
|
+
self._pattern_strings = pattern
|
|
135
|
+
self._file_path = None
|
|
133
136
|
|
|
134
137
|
@classmethod
|
|
135
138
|
def from_file(cls, file_path: Union[str, Path]) -> "FilePatternMatcher":
|
|
@@ -148,14 +151,10 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
148
151
|
```
|
|
149
152
|
|
|
150
153
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
uninitialized._delayed_init = None
|
|
156
|
-
|
|
157
|
-
uninitialized._delayed_init = _delayed_init
|
|
158
|
-
return uninitialized
|
|
154
|
+
instance = cls.__new__(cls)
|
|
155
|
+
instance._file_path = file_path
|
|
156
|
+
instance._pattern_strings = None
|
|
157
|
+
return instance
|
|
159
158
|
|
|
160
159
|
def _matches(self, file_path: str) -> bool:
|
|
161
160
|
"""Check if the file path or any of its parent directories match the patterns.
|
|
@@ -194,6 +193,18 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
194
193
|
|
|
195
194
|
return matched
|
|
196
195
|
|
|
196
|
+
@cached_property
|
|
197
|
+
def patterns(self) -> list[Pattern]:
|
|
198
|
+
"""Get the patterns, loading from file if necessary."""
|
|
199
|
+
if self._file_path is not None:
|
|
200
|
+
# Lazy load from file
|
|
201
|
+
pattern_strings = Path(self._file_path).read_text("utf8").splitlines()
|
|
202
|
+
else:
|
|
203
|
+
# Use patterns provided in __init__
|
|
204
|
+
pattern_strings = list(self._pattern_strings)
|
|
205
|
+
|
|
206
|
+
return self._parse_patterns(pattern_strings)
|
|
207
|
+
|
|
197
208
|
def can_prune_directories(self) -> bool:
|
|
198
209
|
"""
|
|
199
210
|
Returns True if this pattern matcher allows safe early directory pruning.
|
|
@@ -205,8 +216,6 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
205
216
|
return not any(pattern.exclusion for pattern in self.patterns)
|
|
206
217
|
|
|
207
218
|
def __call__(self, file_path: Path) -> bool:
|
|
208
|
-
if self._delayed_init:
|
|
209
|
-
self._delayed_init()
|
|
210
219
|
return self._matches(str(file_path))
|
|
211
220
|
|
|
212
221
|
|
modal/functions.pyi
CHANGED
|
@@ -84,7 +84,7 @@ class Function(
|
|
|
84
84
|
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
85
85
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
86
86
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
|
87
|
-
timeout:
|
|
87
|
+
timeout: int = 300,
|
|
88
88
|
min_containers: typing.Optional[int] = None,
|
|
89
89
|
max_containers: typing.Optional[int] = None,
|
|
90
90
|
buffer_containers: typing.Optional[int] = None,
|
|
@@ -405,6 +405,12 @@ class Function(
|
|
|
405
405
|
|
|
406
406
|
_map: ___map_spec[typing_extensions.Self]
|
|
407
407
|
|
|
408
|
+
class ___spawn_map_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
|
|
409
|
+
def __call__(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
|
|
410
|
+
async def aio(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
|
|
411
|
+
|
|
412
|
+
_spawn_map: ___spawn_map_spec[modal._functions.ReturnType, typing_extensions.Self]
|
|
413
|
+
|
|
408
414
|
class ___call_function_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
|
|
409
415
|
def __call__(self, /, args, kwargs) -> ReturnType_INNER: ...
|
|
410
416
|
async def aio(self, /, args, kwargs) -> ReturnType_INNER: ...
|
|
@@ -427,7 +433,7 @@ class Function(
|
|
|
427
433
|
|
|
428
434
|
_call_generator: ___call_generator_spec[typing_extensions.Self]
|
|
429
435
|
|
|
430
|
-
class __remote_spec(typing_extensions.Protocol[
|
|
436
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
431
437
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
|
|
432
438
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
433
439
|
...
|
|
@@ -436,7 +442,7 @@ class Function(
|
|
|
436
442
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
437
443
|
...
|
|
438
444
|
|
|
439
|
-
remote: __remote_spec[modal._functions.
|
|
445
|
+
remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
|
|
440
446
|
|
|
441
447
|
class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
442
448
|
def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
|
|
@@ -463,7 +469,7 @@ class Function(
|
|
|
463
469
|
"""
|
|
464
470
|
...
|
|
465
471
|
|
|
466
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
|
472
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
467
473
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
468
474
|
"""[Experimental] Calls the function with the given arguments, without waiting for the results.
|
|
469
475
|
|
|
@@ -487,7 +493,7 @@ class Function(
|
|
|
487
493
|
...
|
|
488
494
|
|
|
489
495
|
_experimental_spawn: ___experimental_spawn_spec[
|
|
490
|
-
modal._functions.
|
|
496
|
+
modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
|
|
491
497
|
]
|
|
492
498
|
|
|
493
499
|
class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
|
|
@@ -496,7 +502,7 @@ class Function(
|
|
|
496
502
|
|
|
497
503
|
_spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
|
|
498
504
|
|
|
499
|
-
class __spawn_spec(typing_extensions.Protocol[
|
|
505
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
500
506
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
501
507
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
502
508
|
|
|
@@ -517,7 +523,7 @@ class Function(
|
|
|
517
523
|
"""
|
|
518
524
|
...
|
|
519
525
|
|
|
520
|
-
spawn: __spawn_spec[modal._functions.
|
|
526
|
+
spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
|
|
521
527
|
|
|
522
528
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
|
|
523
529
|
"""Return the inner Python object wrapped by this Modal Function."""
|
|
@@ -693,6 +699,34 @@ class Function(
|
|
|
693
699
|
|
|
694
700
|
spawn_map: __spawn_map_spec[typing_extensions.Self]
|
|
695
701
|
|
|
702
|
+
class __experimental_spawn_map_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
703
|
+
def __call__(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall:
|
|
704
|
+
"""mdmd:hidden
|
|
705
|
+
Spawn parallel execution over a set of inputs, returning as soon as the inputs are created.
|
|
706
|
+
|
|
707
|
+
Unlike `modal.Function.map`, this method does not block on completion of the remote execution but
|
|
708
|
+
returns a `modal.FunctionCall` object that can be used to poll status and retrieve results later.
|
|
709
|
+
|
|
710
|
+
Takes one iterator argument per argument in the function being mapped over.
|
|
711
|
+
|
|
712
|
+
Example:
|
|
713
|
+
```python
|
|
714
|
+
@app.function()
|
|
715
|
+
def my_func(a, b):
|
|
716
|
+
return a ** b
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@app.local_entrypoint()
|
|
720
|
+
def main():
|
|
721
|
+
fc = my_func.spawn_map([1, 2], [3, 4])
|
|
722
|
+
```
|
|
723
|
+
"""
|
|
724
|
+
...
|
|
725
|
+
|
|
726
|
+
async def aio(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall: ...
|
|
727
|
+
|
|
728
|
+
experimental_spawn_map: __experimental_spawn_map_spec[typing_extensions.Self]
|
|
729
|
+
|
|
696
730
|
class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Object):
|
|
697
731
|
"""A reference to an executed function call.
|
|
698
732
|
|
|
@@ -705,16 +739,31 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
705
739
|
"""
|
|
706
740
|
|
|
707
741
|
_is_generator: bool
|
|
742
|
+
_num_inputs: typing.Optional[int]
|
|
708
743
|
|
|
709
744
|
def __init__(self, *args, **kwargs):
|
|
710
745
|
"""mdmd:hidden"""
|
|
711
746
|
...
|
|
712
747
|
|
|
713
748
|
def _invocation(self): ...
|
|
749
|
+
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
750
|
+
|
|
751
|
+
class __num_inputs_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
752
|
+
def __call__(self, /) -> int:
|
|
753
|
+
"""Get the number of inputs in the function call."""
|
|
754
|
+
...
|
|
755
|
+
|
|
756
|
+
async def aio(self, /) -> int:
|
|
757
|
+
"""Get the number of inputs in the function call."""
|
|
758
|
+
...
|
|
759
|
+
|
|
760
|
+
num_inputs: __num_inputs_spec[typing_extensions.Self]
|
|
714
761
|
|
|
715
762
|
class __get_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
|
|
716
|
-
def __call__(self, /, timeout: typing.Optional[float] = None) -> ReturnType_INNER:
|
|
717
|
-
"""Get the result of the function call.
|
|
763
|
+
def __call__(self, /, timeout: typing.Optional[float] = None, *, index: int = 0) -> ReturnType_INNER:
|
|
764
|
+
"""Get the result of the index-th input of the function call.
|
|
765
|
+
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
766
|
+
A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
|
|
718
767
|
|
|
719
768
|
This function waits indefinitely by default. It takes an optional
|
|
720
769
|
`timeout` argument that specifies the maximum number of seconds to wait,
|
|
@@ -724,8 +773,10 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
724
773
|
"""
|
|
725
774
|
...
|
|
726
775
|
|
|
727
|
-
async def aio(self, /, timeout: typing.Optional[float] = None) -> ReturnType_INNER:
|
|
728
|
-
"""Get the result of the function call.
|
|
776
|
+
async def aio(self, /, timeout: typing.Optional[float] = None, *, index: int = 0) -> ReturnType_INNER:
|
|
777
|
+
"""Get the result of the index-th input of the function call.
|
|
778
|
+
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
779
|
+
A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
|
|
729
780
|
|
|
730
781
|
This function waits indefinitely by default. It takes an optional
|
|
731
782
|
`timeout` argument that specifies the maximum number of seconds to wait,
|
|
@@ -737,6 +788,55 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
737
788
|
|
|
738
789
|
get: __get_spec[modal._functions.ReturnType, typing_extensions.Self]
|
|
739
790
|
|
|
791
|
+
class __iter_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
|
|
792
|
+
def __call__(self, /, *, start: int = 0, end: typing.Optional[int] = None) -> typing.Iterator[ReturnType_INNER]:
|
|
793
|
+
"""Iterate in-order over the results of the function call.
|
|
794
|
+
|
|
795
|
+
Optionally, specify a range [start, end) to iterate over.
|
|
796
|
+
|
|
797
|
+
Example:
|
|
798
|
+
```python
|
|
799
|
+
@app.function()
|
|
800
|
+
def my_func(a):
|
|
801
|
+
return a ** 2
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@app.local_entrypoint()
|
|
805
|
+
def main():
|
|
806
|
+
fc = my_func.spawn_map([1, 2, 3, 4])
|
|
807
|
+
assert list(fc.iter()) == [1, 4, 9, 16]
|
|
808
|
+
assert list(fc.iter(start=1, end=3)) == [4, 9]
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
If `end` is not provided, it will iterate over all results.
|
|
812
|
+
"""
|
|
813
|
+
...
|
|
814
|
+
|
|
815
|
+
def aio(self, /, *, start: int = 0, end: typing.Optional[int] = None) -> typing.AsyncIterator[ReturnType_INNER]:
|
|
816
|
+
"""Iterate in-order over the results of the function call.
|
|
817
|
+
|
|
818
|
+
Optionally, specify a range [start, end) to iterate over.
|
|
819
|
+
|
|
820
|
+
Example:
|
|
821
|
+
```python
|
|
822
|
+
@app.function()
|
|
823
|
+
def my_func(a):
|
|
824
|
+
return a ** 2
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@app.local_entrypoint()
|
|
828
|
+
def main():
|
|
829
|
+
fc = my_func.spawn_map([1, 2, 3, 4])
|
|
830
|
+
assert list(fc.iter()) == [1, 4, 9, 16]
|
|
831
|
+
assert list(fc.iter(start=1, end=3)) == [4, 9]
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
If `end` is not provided, it will iterate over all results.
|
|
835
|
+
"""
|
|
836
|
+
...
|
|
837
|
+
|
|
838
|
+
iter: __iter_spec[modal._functions.ReturnType, typing_extensions.Self]
|
|
839
|
+
|
|
740
840
|
class __get_call_graph_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
741
841
|
def __call__(self, /) -> list[modal.call_graph.InputInfo]:
|
|
742
842
|
"""Returns a structure representing the call graph from a given root
|
modal/image.py
CHANGED
|
@@ -1393,7 +1393,13 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1393
1393
|
# a requirement in `uv.lock`
|
|
1394
1394
|
return
|
|
1395
1395
|
|
|
1396
|
-
|
|
1396
|
+
try:
|
|
1397
|
+
dependencies = pyproject_toml_content["project"]["dependencies"]
|
|
1398
|
+
except KeyError as e:
|
|
1399
|
+
raise InvalidError(
|
|
1400
|
+
f"Invalid pyproject.toml file: missing key {e} in {pyproject_toml}. "
|
|
1401
|
+
"See https://packaging.python.org/en/latest/guides/writing-pyproject-toml for guidelines."
|
|
1402
|
+
)
|
|
1397
1403
|
|
|
1398
1404
|
for group in groups:
|
|
1399
1405
|
if (
|
|
@@ -1459,7 +1465,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1459
1465
|
commands.append(f"COPY /.uv.lock {UV_ROOT}/uv.lock")
|
|
1460
1466
|
|
|
1461
1467
|
if frozen:
|
|
1462
|
-
# Do not update `uv.lock` when we have one when `frozen=True`. This
|
|
1468
|
+
# Do not update `uv.lock` when we have one when `frozen=True`. This is the default because this
|
|
1463
1469
|
# ensures that the runtime environment matches the local `uv.lock`.
|
|
1464
1470
|
#
|
|
1465
1471
|
# If `frozen=False`, then `uv sync` will update the the dependencies in the `uv.lock` file
|
|
@@ -2108,7 +2114,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2108
2114
|
gpu: Union[GPU_T, list[GPU_T]] = None, # Requested GPU or or list of acceptable GPUs( e.g. ["A10", "A100"])
|
|
2109
2115
|
cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
|
|
2110
2116
|
memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
|
|
2111
|
-
timeout:
|
|
2117
|
+
timeout: int = 60 * 60, # Maximum execution time of the function in seconds.
|
|
2112
2118
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
2113
2119
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
2114
2120
|
force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
|
modal/image.pyi
CHANGED
|
@@ -91,7 +91,7 @@ def _create_context_mount_function(
|
|
|
91
91
|
dockerfile_cmds: list[str] = [],
|
|
92
92
|
dockerfile_path: typing.Optional[pathlib.Path] = None,
|
|
93
93
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
|
94
|
-
context_dir: typing.Union[pathlib.Path,
|
|
94
|
+
context_dir: typing.Union[str, pathlib.Path, None] = None,
|
|
95
95
|
): ...
|
|
96
96
|
|
|
97
97
|
class _ImageRegistryConfig:
|
|
@@ -537,7 +537,7 @@ class _Image(modal._object._Object):
|
|
|
537
537
|
secrets: collections.abc.Sequence[modal.secret._Secret] = [],
|
|
538
538
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
539
539
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
|
540
|
-
context_dir: typing.Union[pathlib.Path,
|
|
540
|
+
context_dir: typing.Union[str, pathlib.Path, None] = None,
|
|
541
541
|
force_build: bool = False,
|
|
542
542
|
ignore: typing.Union[
|
|
543
543
|
collections.abc.Sequence[str], collections.abc.Callable[[pathlib.Path], bool]
|
|
@@ -748,7 +748,7 @@ class _Image(modal._object._Object):
|
|
|
748
748
|
*,
|
|
749
749
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
|
750
750
|
force_build: bool = False,
|
|
751
|
-
context_dir: typing.Union[pathlib.Path,
|
|
751
|
+
context_dir: typing.Union[str, pathlib.Path, None] = None,
|
|
752
752
|
secrets: collections.abc.Sequence[modal.secret._Secret] = [],
|
|
753
753
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
754
754
|
add_python: typing.Optional[str] = None,
|
|
@@ -845,7 +845,7 @@ class _Image(modal._object._Object):
|
|
|
845
845
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig, list[typing.Union[None, str, modal.gpu._GPUConfig]]] = None,
|
|
846
846
|
cpu: typing.Optional[float] = None,
|
|
847
847
|
memory: typing.Optional[int] = None,
|
|
848
|
-
timeout:
|
|
848
|
+
timeout: int = 3600,
|
|
849
849
|
cloud: typing.Optional[str] = None,
|
|
850
850
|
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
851
851
|
force_build: bool = False,
|
|
@@ -1381,7 +1381,7 @@ class Image(modal.object.Object):
|
|
|
1381
1381
|
secrets: collections.abc.Sequence[modal.secret.Secret] = [],
|
|
1382
1382
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
1383
1383
|
context_mount: typing.Optional[modal.mount.Mount] = None,
|
|
1384
|
-
context_dir: typing.Union[pathlib.Path,
|
|
1384
|
+
context_dir: typing.Union[str, pathlib.Path, None] = None,
|
|
1385
1385
|
force_build: bool = False,
|
|
1386
1386
|
ignore: typing.Union[
|
|
1387
1387
|
collections.abc.Sequence[str], collections.abc.Callable[[pathlib.Path], bool]
|
|
@@ -1592,7 +1592,7 @@ class Image(modal.object.Object):
|
|
|
1592
1592
|
*,
|
|
1593
1593
|
context_mount: typing.Optional[modal.mount.Mount] = None,
|
|
1594
1594
|
force_build: bool = False,
|
|
1595
|
-
context_dir: typing.Union[pathlib.Path,
|
|
1595
|
+
context_dir: typing.Union[str, pathlib.Path, None] = None,
|
|
1596
1596
|
secrets: collections.abc.Sequence[modal.secret.Secret] = [],
|
|
1597
1597
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
1598
1598
|
add_python: typing.Optional[str] = None,
|
|
@@ -1689,7 +1689,7 @@ class Image(modal.object.Object):
|
|
|
1689
1689
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig, list[typing.Union[None, str, modal.gpu._GPUConfig]]] = None,
|
|
1690
1690
|
cpu: typing.Optional[float] = None,
|
|
1691
1691
|
memory: typing.Optional[int] = None,
|
|
1692
|
-
timeout:
|
|
1692
|
+
timeout: int = 3600,
|
|
1693
1693
|
cloud: typing.Optional[str] = None,
|
|
1694
1694
|
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
1695
1695
|
force_build: bool = False,
|
modal/mount.py
CHANGED
|
@@ -13,6 +13,7 @@ from pathlib import Path, PurePosixPath
|
|
|
13
13
|
from typing import Callable, Optional, Sequence, Union
|
|
14
14
|
|
|
15
15
|
from google.protobuf.message import Message
|
|
16
|
+
from grpclib import GRPCError
|
|
16
17
|
|
|
17
18
|
import modal.exception
|
|
18
19
|
import modal.file_pattern_matcher
|
|
@@ -929,19 +930,23 @@ async def _create_single_client_dependency_mount(
|
|
|
929
930
|
remote_path=REMOTE_SITECUSTOMIZE_PATH,
|
|
930
931
|
)
|
|
931
932
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
933
|
+
try:
|
|
934
|
+
await python_mount._deploy.aio(
|
|
935
|
+
mount_name,
|
|
936
|
+
api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
937
|
+
environment_name=profile_environment,
|
|
938
|
+
allow_overwrite=allow_overwrite,
|
|
939
|
+
client=client,
|
|
940
|
+
)
|
|
941
|
+
print(f"✅ Deployed mount {mount_name} to global namespace.")
|
|
942
|
+
except GRPCError as e:
|
|
943
|
+
print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
|
|
940
944
|
|
|
941
945
|
|
|
942
946
|
async def _create_client_dependency_mounts(
|
|
943
947
|
client=None,
|
|
944
948
|
python_versions: list[str] = list(PYTHON_STANDALONE_VERSIONS),
|
|
949
|
+
builder_versions: list[str] = ["2025.06"], # Reenable "PREVIEW" during testing
|
|
945
950
|
check_if_exists=True,
|
|
946
951
|
):
|
|
947
952
|
arch = "x86_64"
|
|
@@ -950,8 +955,8 @@ async def _create_client_dependency_mounts(
|
|
|
950
955
|
("musllinux_1_2", f"{arch}-unknown-linux-musl"), # musl >= 1.2
|
|
951
956
|
]
|
|
952
957
|
coros = []
|
|
953
|
-
for
|
|
954
|
-
for
|
|
958
|
+
for python_version in python_versions:
|
|
959
|
+
for builder_version in builder_versions:
|
|
955
960
|
for platform, uv_python_platform in platform_tags:
|
|
956
961
|
coros.append(
|
|
957
962
|
_create_single_client_dependency_mount(
|
|
@@ -961,9 +966,11 @@ async def _create_client_dependency_mounts(
|
|
|
961
966
|
arch,
|
|
962
967
|
platform,
|
|
963
968
|
uv_python_platform,
|
|
964
|
-
#
|
|
965
|
-
#
|
|
966
|
-
#
|
|
969
|
+
# This check_if_exists / allow_overwrite parameterization is very awkward
|
|
970
|
+
# Also it doesn't provide a hook for overwriting a non-preview version, which
|
|
971
|
+
# in theory we may need to do at some point (hopefully not, but...)
|
|
972
|
+
check_if_exists=check_if_exists and builder_version != "PREVIEW",
|
|
973
|
+
allow_overwrite=builder_version == "PREVIEW",
|
|
967
974
|
)
|
|
968
975
|
)
|
|
969
976
|
await TaskContext.gather(*coros)
|
modal/mount.pyi
CHANGED
|
@@ -570,15 +570,28 @@ async def _create_single_client_dependency_mount(
|
|
|
570
570
|
allow_overwrite: bool = False,
|
|
571
571
|
): ...
|
|
572
572
|
async def _create_client_dependency_mounts(
|
|
573
|
-
client=None,
|
|
573
|
+
client=None,
|
|
574
|
+
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
|
575
|
+
builder_versions: list[str] = ["2025.06"],
|
|
576
|
+
check_if_exists=True,
|
|
574
577
|
): ...
|
|
575
578
|
|
|
576
579
|
class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
|
577
580
|
def __call__(
|
|
578
|
-
self,
|
|
581
|
+
self,
|
|
582
|
+
/,
|
|
583
|
+
client=None,
|
|
584
|
+
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
|
585
|
+
builder_versions: list[str] = ["2025.06"],
|
|
586
|
+
check_if_exists=True,
|
|
579
587
|
): ...
|
|
580
588
|
async def aio(
|
|
581
|
-
self,
|
|
589
|
+
self,
|
|
590
|
+
/,
|
|
591
|
+
client=None,
|
|
592
|
+
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
|
593
|
+
builder_versions: list[str] = ["2025.06"],
|
|
594
|
+
check_if_exists=True,
|
|
582
595
|
): ...
|
|
583
596
|
|
|
584
597
|
create_client_dependency_mounts: __create_client_dependency_mounts_spec
|