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/_partial_function.py
CHANGED
|
@@ -19,7 +19,7 @@ from ._functions import _Function
|
|
|
19
19
|
from ._utils.async_utils import synchronizer
|
|
20
20
|
from ._utils.deprecation import deprecation_warning
|
|
21
21
|
from ._utils.function_utils import callable_has_non_self_params
|
|
22
|
-
from .config import logger
|
|
22
|
+
from .config import config, logger
|
|
23
23
|
from .exception import InvalidError
|
|
24
24
|
|
|
25
25
|
MAX_MAX_BATCH_SIZE = 1000
|
|
@@ -31,7 +31,6 @@ if typing.TYPE_CHECKING:
|
|
|
31
31
|
|
|
32
32
|
class _PartialFunctionFlags(enum.IntFlag):
|
|
33
33
|
# Lifecycle method flags
|
|
34
|
-
BUILD = 1 # Deprecated, will be removed
|
|
35
34
|
ENTER_PRE_SNAPSHOT = 2
|
|
36
35
|
ENTER_POST_SNAPSHOT = 4
|
|
37
36
|
EXIT = 8
|
|
@@ -55,8 +54,7 @@ class _PartialFunctionFlags(enum.IntFlag):
|
|
|
55
54
|
@staticmethod
|
|
56
55
|
def lifecycle_flags() -> int:
|
|
57
56
|
return (
|
|
58
|
-
_PartialFunctionFlags.
|
|
59
|
-
| _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
|
57
|
+
_PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
|
60
58
|
| _PartialFunctionFlags.ENTER_POST_SNAPSHOT
|
|
61
59
|
| _PartialFunctionFlags.EXIT
|
|
62
60
|
)
|
|
@@ -95,6 +93,26 @@ NullaryFuncOrMethod = Union[Callable[[], Any], Callable[[Any], Any]]
|
|
|
95
93
|
NullaryMethod = Callable[[Any], Any]
|
|
96
94
|
|
|
97
95
|
|
|
96
|
+
def verify_concurrent_params(params: _PartialFunctionParams, is_flash: bool = False) -> None:
|
|
97
|
+
def _verify_concurrent_params_with_flash_settings(params: _PartialFunctionParams) -> None:
|
|
98
|
+
if params.max_concurrent_inputs is not None:
|
|
99
|
+
raise TypeError(
|
|
100
|
+
"@modal.concurrent(max_inputs=...) is not yet supported for Flash functions. "
|
|
101
|
+
"Use `@modal.concurrent(target_inputs=...)` instead."
|
|
102
|
+
)
|
|
103
|
+
if params.target_concurrent_inputs is None:
|
|
104
|
+
raise TypeError("`@modal.concurrent()` missing required argument: `target_inputs`.")
|
|
105
|
+
|
|
106
|
+
def _verify_concurrent_params(params: _PartialFunctionParams) -> None:
|
|
107
|
+
if params.max_concurrent_inputs is None:
|
|
108
|
+
raise TypeError("`@modal.concurrent()` missing required argument: `max_inputs`.")
|
|
109
|
+
|
|
110
|
+
if is_flash:
|
|
111
|
+
_verify_concurrent_params_with_flash_settings(params)
|
|
112
|
+
else:
|
|
113
|
+
_verify_concurrent_params(params)
|
|
114
|
+
|
|
115
|
+
|
|
98
116
|
class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
99
117
|
"""Object produced by a decorator in the `modal` namespace
|
|
100
118
|
|
|
@@ -284,7 +302,7 @@ class _MethodDecoratorType:
|
|
|
284
302
|
|
|
285
303
|
# TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
|
|
286
304
|
def _method(
|
|
287
|
-
_warn_parentheses_missing=None,
|
|
305
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
288
306
|
*,
|
|
289
307
|
# Set this to True if it's a non-generator function returning
|
|
290
308
|
# a [sync/async] generator object
|
|
@@ -339,7 +357,7 @@ def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) -> lis
|
|
|
339
357
|
|
|
340
358
|
|
|
341
359
|
def _fastapi_endpoint(
|
|
342
|
-
_warn_parentheses_missing=None,
|
|
360
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
343
361
|
*,
|
|
344
362
|
method: str = "GET", # REST method for the created endpoint.
|
|
345
363
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
@@ -380,6 +398,7 @@ def _fastapi_endpoint(
|
|
|
380
398
|
method=method,
|
|
381
399
|
web_endpoint_docs=docs,
|
|
382
400
|
requested_suffix=label or "",
|
|
401
|
+
ephemeral_suffix=config.get("dev_suffix"),
|
|
383
402
|
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
384
403
|
custom_domains=_parse_custom_domains(custom_domains),
|
|
385
404
|
requires_proxy_auth=requires_proxy_auth,
|
|
@@ -402,7 +421,7 @@ def _fastapi_endpoint(
|
|
|
402
421
|
|
|
403
422
|
|
|
404
423
|
def _web_endpoint(
|
|
405
|
-
_warn_parentheses_missing=None,
|
|
424
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
406
425
|
*,
|
|
407
426
|
method: str = "GET", # REST method for the created endpoint.
|
|
408
427
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
@@ -448,6 +467,7 @@ def _web_endpoint(
|
|
|
448
467
|
method=method,
|
|
449
468
|
web_endpoint_docs=docs,
|
|
450
469
|
requested_suffix=label or "",
|
|
470
|
+
ephemeral_suffix=config.get("dev_suffix"),
|
|
451
471
|
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
452
472
|
custom_domains=_parse_custom_domains(custom_domains),
|
|
453
473
|
requires_proxy_auth=requires_proxy_auth,
|
|
@@ -470,7 +490,7 @@ def _web_endpoint(
|
|
|
470
490
|
|
|
471
491
|
|
|
472
492
|
def _asgi_app(
|
|
473
|
-
_warn_parentheses_missing=None,
|
|
493
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
474
494
|
*,
|
|
475
495
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
476
496
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
|
@@ -507,6 +527,7 @@ def _asgi_app(
|
|
|
507
527
|
webhook_config = api_pb2.WebhookConfig(
|
|
508
528
|
type=api_pb2.WEBHOOK_TYPE_ASGI_APP,
|
|
509
529
|
requested_suffix=label or "",
|
|
530
|
+
ephemeral_suffix=config.get("dev_suffix"),
|
|
510
531
|
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
511
532
|
custom_domains=_parse_custom_domains(custom_domains),
|
|
512
533
|
requires_proxy_auth=requires_proxy_auth,
|
|
@@ -527,7 +548,7 @@ def _asgi_app(
|
|
|
527
548
|
|
|
528
549
|
|
|
529
550
|
def _wsgi_app(
|
|
530
|
-
_warn_parentheses_missing=None,
|
|
551
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
531
552
|
*,
|
|
532
553
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
533
554
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
|
@@ -538,7 +559,7 @@ def _wsgi_app(
|
|
|
538
559
|
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
|
539
560
|
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
|
540
561
|
which is compatible with ASGI and supports additional functionality such as web sockets.
|
|
541
|
-
Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
|
|
562
|
+
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/reference/modal.asgi_app).
|
|
542
563
|
|
|
543
564
|
**Usage:**
|
|
544
565
|
|
|
@@ -564,6 +585,7 @@ def _wsgi_app(
|
|
|
564
585
|
webhook_config = api_pb2.WebhookConfig(
|
|
565
586
|
type=api_pb2.WEBHOOK_TYPE_WSGI_APP,
|
|
566
587
|
requested_suffix=label or "",
|
|
588
|
+
ephemeral_suffix=config.get("dev_suffix"),
|
|
567
589
|
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
568
590
|
custom_domains=_parse_custom_domains(custom_domains),
|
|
569
591
|
requires_proxy_auth=requires_proxy_auth,
|
|
@@ -625,6 +647,7 @@ def _web_server(
|
|
|
625
647
|
webhook_config = api_pb2.WebhookConfig(
|
|
626
648
|
type=api_pb2.WEBHOOK_TYPE_WEB_SERVER,
|
|
627
649
|
requested_suffix=label or "",
|
|
650
|
+
ephemeral_suffix=config.get("dev_suffix"),
|
|
628
651
|
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
629
652
|
custom_domains=_parse_custom_domains(custom_domains),
|
|
630
653
|
web_server_port=port,
|
|
@@ -646,61 +669,8 @@ def _web_server(
|
|
|
646
669
|
return wrapper
|
|
647
670
|
|
|
648
671
|
|
|
649
|
-
def _build(
|
|
650
|
-
_warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
|
|
651
|
-
) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
|
|
652
|
-
"""mdmd:hidden
|
|
653
|
-
Decorator for methods that execute at _build time_ to create a new Image layer.
|
|
654
|
-
|
|
655
|
-
**Deprecated**: This function is deprecated. We recommend using `modal.Volume`
|
|
656
|
-
to store large assets (such as model weights) instead of writing them to the
|
|
657
|
-
Image during the build process. For other use cases, you can replace this
|
|
658
|
-
decorator with the `Image.run_function` method.
|
|
659
|
-
|
|
660
|
-
**Usage**
|
|
661
|
-
|
|
662
|
-
```python notest
|
|
663
|
-
@app.cls(gpu="A10G")
|
|
664
|
-
class AlpacaLoRAModel:
|
|
665
|
-
@build()
|
|
666
|
-
def download_models(self):
|
|
667
|
-
model = LlamaForCausalLM.from_pretrained(
|
|
668
|
-
base_model,
|
|
669
|
-
)
|
|
670
|
-
PeftModel.from_pretrained(model, lora_weights)
|
|
671
|
-
LlamaTokenizer.from_pretrained(base_model)
|
|
672
|
-
```
|
|
673
|
-
"""
|
|
674
|
-
if _warn_parentheses_missing is not None:
|
|
675
|
-
raise InvalidError(
|
|
676
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.build()`."
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
deprecation_warning(
|
|
680
|
-
(2025, 1, 15),
|
|
681
|
-
"The `@modal.build` decorator is deprecated and will be removed in a future release."
|
|
682
|
-
"\n\nWe now recommend storing large assets (such as model weights) using a `modal.Volume`"
|
|
683
|
-
" instead of writing them directly into the `modal.Image` filesystem."
|
|
684
|
-
" For other use cases we recommend using `Image.run_function` instead."
|
|
685
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
flags = _PartialFunctionFlags.BUILD
|
|
689
|
-
params = _PartialFunctionParams(force_build=force, build_timeout=timeout)
|
|
690
|
-
|
|
691
|
-
def wrapper(obj: Union[_PartialFunction, NullaryMethod]) -> _PartialFunction:
|
|
692
|
-
if isinstance(obj, _PartialFunction):
|
|
693
|
-
pf = obj.stack(flags, params)
|
|
694
|
-
else:
|
|
695
|
-
pf = _PartialFunction(obj, flags, params)
|
|
696
|
-
pf.validate_obj_compatibility("build")
|
|
697
|
-
return pf
|
|
698
|
-
|
|
699
|
-
return wrapper
|
|
700
|
-
|
|
701
|
-
|
|
702
672
|
def _enter(
|
|
703
|
-
_warn_parentheses_missing=None,
|
|
673
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
704
674
|
*,
|
|
705
675
|
snap: bool = False,
|
|
706
676
|
) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
|
|
@@ -751,7 +721,7 @@ def _exit(_warn_parentheses_missing=None) -> Callable[[NullaryMethod], _PartialF
|
|
|
751
721
|
|
|
752
722
|
|
|
753
723
|
def _batched(
|
|
754
|
-
_warn_parentheses_missing=None,
|
|
724
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
755
725
|
*,
|
|
756
726
|
max_batch_size: int,
|
|
757
727
|
wait_ms: int,
|
|
@@ -789,12 +759,12 @@ def _batched(
|
|
|
789
759
|
)
|
|
790
760
|
if max_batch_size < 1:
|
|
791
761
|
raise InvalidError("max_batch_size must be a positive integer.")
|
|
792
|
-
if max_batch_size
|
|
793
|
-
raise InvalidError(f"max_batch_size
|
|
762
|
+
if max_batch_size > MAX_MAX_BATCH_SIZE:
|
|
763
|
+
raise InvalidError(f"max_batch_size cannot be greater than {MAX_MAX_BATCH_SIZE}.")
|
|
794
764
|
if wait_ms < 0:
|
|
795
765
|
raise InvalidError("wait_ms must be a non-negative integer.")
|
|
796
|
-
if wait_ms
|
|
797
|
-
raise InvalidError(f"wait_ms
|
|
766
|
+
if wait_ms > MAX_BATCH_WAIT_MS:
|
|
767
|
+
raise InvalidError(f"wait_ms cannot be greater than {MAX_BATCH_WAIT_MS}.")
|
|
798
768
|
|
|
799
769
|
flags = _PartialFunctionFlags.CALLABLE_INTERFACE | _PartialFunctionFlags.BATCHED
|
|
800
770
|
params = _PartialFunctionParams(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
|
|
@@ -813,9 +783,9 @@ def _batched(
|
|
|
813
783
|
|
|
814
784
|
|
|
815
785
|
def _concurrent(
|
|
816
|
-
_warn_parentheses_missing=None,
|
|
786
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
817
787
|
*,
|
|
818
|
-
max_inputs: int, # Hard limit on each container's input concurrency
|
|
788
|
+
max_inputs: Optional[int] = None, # Hard limit on each container's input concurrency
|
|
819
789
|
target_inputs: Optional[int] = None, # Input concurrency that Modal's autoscaler should target
|
|
820
790
|
) -> Callable[
|
|
821
791
|
[Union[Callable[P, ReturnType], _PartialFunction[P, ReturnType, ReturnType]]],
|
|
@@ -867,7 +837,7 @@ def _concurrent(
|
|
|
867
837
|
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.concurrent()`."
|
|
868
838
|
)
|
|
869
839
|
|
|
870
|
-
if target_inputs and target_inputs > max_inputs:
|
|
840
|
+
if max_inputs is not None and target_inputs is not None and target_inputs > max_inputs:
|
|
871
841
|
raise InvalidError("`target_inputs` parameter cannot be greater than `max_inputs`.")
|
|
872
842
|
|
|
873
843
|
flags = _PartialFunctionFlags.CONCURRENT
|
|
@@ -891,7 +861,12 @@ def _concurrent(
|
|
|
891
861
|
|
|
892
862
|
|
|
893
863
|
# NOTE: clustered is currently exposed through modal.experimental, not the top-level namespace
|
|
894
|
-
def _clustered(
|
|
864
|
+
def _clustered(
|
|
865
|
+
size: int, broadcast: bool = True, rdma: bool = False
|
|
866
|
+
) -> Callable[
|
|
867
|
+
[Union[Callable[P, ReturnType], _PartialFunction[P, ReturnType, ReturnType]]],
|
|
868
|
+
_PartialFunction[P, ReturnType, ReturnType],
|
|
869
|
+
]:
|
|
895
870
|
"""Provision clusters of colocated and networked containers for the Function.
|
|
896
871
|
|
|
897
872
|
Parameters:
|
modal/_pty.py
CHANGED
|
@@ -7,8 +7,11 @@ from typing import Optional
|
|
|
7
7
|
from modal_proto import api_pb2
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def get_winsz(fd) -> tuple[Optional[int], Optional[int]]:
|
|
10
|
+
def get_winsz(fd=None) -> tuple[Optional[int], Optional[int]]:
|
|
11
11
|
try:
|
|
12
|
+
if fd is None:
|
|
13
|
+
fd = sys.stdin.fileno()
|
|
14
|
+
|
|
12
15
|
import fcntl
|
|
13
16
|
import struct
|
|
14
17
|
import termios
|
|
@@ -40,8 +43,8 @@ def raw_terminal():
|
|
|
40
43
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
41
44
|
|
|
42
45
|
|
|
43
|
-
def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
|
|
44
|
-
rows, cols = get_winsz(
|
|
46
|
+
def get_pty_info(shell: bool, no_terminate_on_idle_stdin: bool = False) -> api_pb2.PTYInfo:
|
|
47
|
+
rows, cols = get_winsz()
|
|
45
48
|
return api_pb2.PTYInfo(
|
|
46
49
|
enabled=True, # TODO(erikbern): deprecated
|
|
47
50
|
winsz_rows=rows,
|
|
@@ -50,4 +53,5 @@ def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
|
|
|
50
53
|
env_colorterm=os.environ.get("COLORTERM"),
|
|
51
54
|
env_term_program=os.environ.get("TERM_PROGRAM"),
|
|
52
55
|
pty_type=api_pb2.PTYInfo.PTY_TYPE_SHELL if shell else api_pb2.PTYInfo.PTY_TYPE_FUNCTION,
|
|
56
|
+
no_terminate_on_idle_stdin=no_terminate_on_idle_stdin,
|
|
53
57
|
)
|
modal/_resolver.py
CHANGED
|
@@ -8,19 +8,16 @@ from asyncio import Future
|
|
|
8
8
|
from collections.abc import Hashable
|
|
9
9
|
from typing import TYPE_CHECKING, Optional
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import modal._object
|
|
12
|
+
from modal._traceback import suppress_tb_frames
|
|
13
13
|
from modal_proto import api_pb2
|
|
14
14
|
|
|
15
|
+
from ._load_context import LoadContext
|
|
15
16
|
from ._utils.async_utils import TaskContext
|
|
16
|
-
from .client import _Client
|
|
17
|
-
from .exception import NotFoundError
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
20
19
|
from rich.tree import Tree
|
|
21
20
|
|
|
22
|
-
import modal._object
|
|
23
|
-
|
|
24
21
|
|
|
25
22
|
class StatusRow:
|
|
26
23
|
def __init__(self, progress: "typing.Optional[Tree]"):
|
|
@@ -50,19 +47,10 @@ class StatusRow:
|
|
|
50
47
|
|
|
51
48
|
class Resolver:
|
|
52
49
|
_local_uuid_to_future: dict[str, Future]
|
|
53
|
-
_environment_name: Optional[str]
|
|
54
|
-
_app_id: Optional[str]
|
|
55
50
|
_deduplication_cache: dict[Hashable, Future]
|
|
56
|
-
_client: _Client
|
|
57
51
|
_build_start: float
|
|
58
52
|
|
|
59
|
-
def __init__(
|
|
60
|
-
self,
|
|
61
|
-
client: _Client,
|
|
62
|
-
*,
|
|
63
|
-
environment_name: Optional[str] = None,
|
|
64
|
-
app_id: Optional[str] = None,
|
|
65
|
-
):
|
|
53
|
+
def __init__(self):
|
|
66
54
|
try:
|
|
67
55
|
# TODO(michael) If we don't clean this up more thoroughly, it would probably
|
|
68
56
|
# be good to have a single source of truth for "rich is installed" rather than
|
|
@@ -77,9 +65,6 @@ class Resolver:
|
|
|
77
65
|
|
|
78
66
|
self._local_uuid_to_future = {}
|
|
79
67
|
self._tree = tree
|
|
80
|
-
self._client = client
|
|
81
|
-
self._app_id = app_id
|
|
82
|
-
self._environment_name = environment_name
|
|
83
68
|
self._deduplication_cache = {}
|
|
84
69
|
|
|
85
70
|
with tempfile.TemporaryFile() as temp_file:
|
|
@@ -87,27 +72,24 @@ class Resolver:
|
|
|
87
72
|
# to the mtime on mounted files, and want those measurements to have the same resolution.
|
|
88
73
|
self._build_start = os.fstat(temp_file.fileno()).st_mtime
|
|
89
74
|
|
|
90
|
-
@property
|
|
91
|
-
def app_id(self) -> Optional[str]:
|
|
92
|
-
return self._app_id
|
|
93
|
-
|
|
94
|
-
@property
|
|
95
|
-
def client(self):
|
|
96
|
-
return self._client
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def environment_name(self):
|
|
100
|
-
return self._environment_name
|
|
101
|
-
|
|
102
75
|
@property
|
|
103
76
|
def build_start(self) -> float:
|
|
104
77
|
return self._build_start
|
|
105
78
|
|
|
106
|
-
async def preload(
|
|
79
|
+
async def preload(
|
|
80
|
+
self, obj: "modal._object._Object", parent_load_context: "LoadContext", existing_object_id: Optional[str]
|
|
81
|
+
):
|
|
107
82
|
if obj._preload is not None:
|
|
108
|
-
|
|
83
|
+
load_context = obj._load_context_overrides.merged_with(parent_load_context)
|
|
84
|
+
await obj._preload(obj, self, load_context, existing_object_id)
|
|
109
85
|
|
|
110
|
-
async def load(
|
|
86
|
+
async def load(
|
|
87
|
+
self,
|
|
88
|
+
obj: "modal._object._Object",
|
|
89
|
+
parent_load_context: "LoadContext",
|
|
90
|
+
*,
|
|
91
|
+
existing_object_id: Optional[str] = None,
|
|
92
|
+
):
|
|
111
93
|
if obj._is_hydrated and obj._is_another_app:
|
|
112
94
|
# No need to reload this, it won't typically change
|
|
113
95
|
if obj.local_uuid not in self._local_uuid_to_future:
|
|
@@ -131,25 +113,23 @@ class Resolver:
|
|
|
131
113
|
cached_future = self._deduplication_cache.get(deduplication_key)
|
|
132
114
|
if cached_future:
|
|
133
115
|
hydrated_object = await cached_future
|
|
134
|
-
|
|
116
|
+
# Use the client from the already-hydrated object
|
|
117
|
+
obj._hydrate(hydrated_object.object_id, hydrated_object.client, hydrated_object._get_metadata())
|
|
135
118
|
return obj
|
|
136
119
|
|
|
137
120
|
if not cached_future:
|
|
138
121
|
# don't run any awaits within this if-block to prevent race conditions
|
|
139
122
|
async def loader():
|
|
140
|
-
|
|
123
|
+
load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
|
|
124
|
+
|
|
141
125
|
# TODO(erikbern): do we need existing_object_id for those?
|
|
142
|
-
await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
|
|
126
|
+
await TaskContext.gather(*[self.load(dep, load_context) for dep in obj.deps()])
|
|
143
127
|
|
|
144
128
|
# Load the object itself
|
|
145
129
|
if not obj._load:
|
|
146
130
|
raise Exception(f"Object {obj} has no loader function")
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
except GRPCError as exc:
|
|
150
|
-
if exc.status == Status.NOT_FOUND:
|
|
151
|
-
raise NotFoundError(exc.message)
|
|
152
|
-
raise
|
|
131
|
+
|
|
132
|
+
await obj._load(obj, self, load_context, existing_object_id)
|
|
153
133
|
|
|
154
134
|
# Check that the id of functions didn't change
|
|
155
135
|
# Persisted refs are ignored because their life cycle is managed independently.
|
|
@@ -169,9 +149,9 @@ class Resolver:
|
|
|
169
149
|
self._local_uuid_to_future[obj.local_uuid] = cached_future
|
|
170
150
|
if deduplication_key is not None:
|
|
171
151
|
self._deduplication_cache[deduplication_key] = cached_future
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
152
|
+
with suppress_tb_frames(2):
|
|
153
|
+
# skip current frame + `loader()` closure frame from above
|
|
154
|
+
return await cached_future
|
|
175
155
|
|
|
176
156
|
def objects(self) -> list["modal._object._Object"]:
|
|
177
157
|
unique_objects: dict[str, "modal._object._Object"] = {}
|
modal/_runtime/asgi.py
CHANGED
|
@@ -16,7 +16,7 @@ from modal.config import logger
|
|
|
16
16
|
from modal.exception import ExecutionError, InvalidError
|
|
17
17
|
from modal.experimental import stop_fetching_inputs
|
|
18
18
|
|
|
19
|
-
from .execution_context import current_function_call_id
|
|
19
|
+
from .execution_context import current_attempt_token, current_function_call_id
|
|
20
20
|
|
|
21
21
|
FIRST_MESSAGE_TIMEOUT_SECONDS = 5.0
|
|
22
22
|
|
|
@@ -106,6 +106,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
|
|
|
106
106
|
raise ExecutionError("Unpexected state in ASGI scope")
|
|
107
107
|
scope["state"] = state
|
|
108
108
|
function_call_id = current_function_call_id()
|
|
109
|
+
attempt_token = current_attempt_token()
|
|
109
110
|
assert function_call_id, "internal error: function_call_id not set in asgi_app() scope"
|
|
110
111
|
|
|
111
112
|
messages_from_app: asyncio.Queue[dict[str, Any]] = asyncio.Queue(1)
|
|
@@ -119,7 +120,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
|
|
|
119
120
|
|
|
120
121
|
async def handle_first_input_timeout():
|
|
121
122
|
if scope["type"] == "http":
|
|
122
|
-
await messages_from_app.put({"type": "http.response.start", "status":
|
|
123
|
+
await messages_from_app.put({"type": "http.response.start", "status": 408})
|
|
123
124
|
await messages_from_app.put(
|
|
124
125
|
{
|
|
125
126
|
"type": "http.response.body",
|
|
@@ -142,7 +143,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
|
|
|
142
143
|
# This initial message, "http.request" or "websocket.connect", should be sent
|
|
143
144
|
# immediately after starting the ASGI app's function call. If it is not received, that
|
|
144
145
|
# indicates a request cancellation or other abnormal circumstance.
|
|
145
|
-
message_gen = container_io_manager.get_data_in.aio(function_call_id)
|
|
146
|
+
message_gen = container_io_manager.get_data_in.aio(function_call_id, attempt_token)
|
|
146
147
|
first_message_task = asyncio.create_task(message_gen.__anext__())
|
|
147
148
|
|
|
148
149
|
try:
|