modAL 1.3.6.dev13__tar.gz → 1.4.0__tar.gz
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-1.3.6.dev13 → modal-1.4.0}/PKG-INFO +1 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_functions.py +7 -90
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_output/pty.py +5 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_partial_function.py +7 -63
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_resources.py +3 -4
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/async_utils.py +27 -9
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/deprecation.py +23 -44
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/function_utils.py +17 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/grpc_utils.py +2 -3
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/task_command_router_client.py +16 -4
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/app.py +11 -85
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/app.pyi +6 -36
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/app.py +7 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/container.py +11 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/entry_point.py +1 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/import_refs.py +2 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/client.pyi +2 -10
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cls.py +3 -48
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cls.pyi +2 -71
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/dict.py +3 -21
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/dict.pyi +5 -9
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/experimental/__init__.py +9 -87
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/functions.py +1 -2
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/functions.pyi +8 -83
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/image.py +37 -38
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/image.pyi +26 -27
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/mount.py +1 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/network_file_system.py +1 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/network_file_system.pyi +0 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/output.py +2 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/parallel_map.py +18 -59
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/parallel_map.pyi +6 -13
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/partial_function.pyi +2 -33
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/queue.py +1 -3
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/queue.pyi +0 -2
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/runner.py +21 -13
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/runner.pyi +9 -9
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/sandbox.py +204 -33
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/sandbox.pyi +113 -47
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/secret.py +3 -12
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/secret.pyi +0 -13
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/serving.py +3 -3
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/volume.py +2 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/volume.pyi +0 -5
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/SOURCES.txt +0 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/gen_reference_docs.py +0 -1
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/api_grpc.py +16 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/api_pb2.py +494 -453
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/api_pb2.pyi +101 -12
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_version/__init__.py +1 -1
- modal-1.3.6.dev13/modal/gpu.py +0 -225
- {modal-1.3.6.dev13 → modal-1.4.0}/LICENSE +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/README.md +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/__main__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_billing.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_clustered_functions.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_container_entrypoint.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_grpc_client.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_ipython.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_load_context.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_location.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_logs.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_object.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_output/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_output/manager.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_output/rich.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_output/status.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_resolver.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_serialization.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_server.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_traceback.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_tunnel.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_tunnel.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_type_manager.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/blob_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/browser_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/logger.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/mount_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_vendor/version.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/_watcher.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/billing.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/2025.06.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/README.md +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/builder/base-images.json +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/call_graph.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/_download.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/_traceback.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/billing.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/changelog.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/cluster.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/config.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/dashboard.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/dict.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/environment.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/launch.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/profile.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/queues.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/run.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/secret.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/selector.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/shell.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/token.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/utils.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cli/volume.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/client.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/config.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/container_process.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/container_process.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/environments.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/environments.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/exception.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/experimental/flash.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/experimental/ipython.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/file_io.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/file_io.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/io_streams.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/io_streams.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/mount.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/object.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/object.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/partial_function.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/proxy.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/proxy.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/py.typed +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/retries.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/running_app.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/sandbox_fs.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/sandbox_fs.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/schedule.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/scheduler_placement.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/server.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/server.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/serving.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/snapshot.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/snapshot.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/stream_type.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/token_flow.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal/token_flow.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/requires.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/__init__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/py.typed +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/modal_version/__main__.py +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/pyproject.toml +0 -0
- {modal-1.3.6.dev13 → modal-1.4.0}/setup.cfg +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import dataclasses
|
|
4
4
|
import inspect
|
|
5
|
-
import textwrap
|
|
6
5
|
import time
|
|
7
6
|
import typing
|
|
8
7
|
import warnings
|
|
@@ -44,7 +43,6 @@ from ._utils.async_utils import (
|
|
|
44
43
|
warn_if_generator_is_not_consumed,
|
|
45
44
|
)
|
|
46
45
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
47
|
-
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
48
46
|
from ._utils.function_utils import (
|
|
49
47
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
50
48
|
OUTPUTS_TIMEOUT,
|
|
@@ -54,6 +52,7 @@ from ._utils.function_utils import (
|
|
|
54
52
|
_stream_function_call_data,
|
|
55
53
|
get_function_type,
|
|
56
54
|
is_async,
|
|
55
|
+
parse_gpu_config,
|
|
57
56
|
)
|
|
58
57
|
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
59
58
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
|
|
@@ -67,7 +66,6 @@ from .exception import (
|
|
|
67
66
|
NotFoundError,
|
|
68
67
|
OutputExpiredError,
|
|
69
68
|
)
|
|
70
|
-
from .gpu import GPU_T, parse_gpu_config
|
|
71
69
|
from .image import _Image
|
|
72
70
|
from .mount import _get_client_mount, _Mount
|
|
73
71
|
from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
|
|
@@ -587,7 +585,7 @@ class _FunctionSpec:
|
|
|
587
585
|
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
|
|
588
586
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
|
589
587
|
# TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
|
590
|
-
gpus:
|
|
588
|
+
gpus: Optional[str | list[str]]
|
|
591
589
|
cloud: Optional[str]
|
|
592
590
|
cpu: Optional[Union[float, tuple[float, float]]]
|
|
593
591
|
memory: Optional[Union[int, tuple[int, int]]]
|
|
@@ -666,7 +664,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
666
664
|
secrets: Optional[Collection[_Secret]] = None,
|
|
667
665
|
schedule: Optional[Schedule] = None,
|
|
668
666
|
is_generator: bool = False,
|
|
669
|
-
gpu:
|
|
667
|
+
gpu: Optional[str | list[str]] = None,
|
|
670
668
|
# TODO: maybe break this out into a separate decorator for notebooks.
|
|
671
669
|
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
|
672
670
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
|
@@ -703,7 +701,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
703
701
|
include_source: bool = True,
|
|
704
702
|
experimental_options: Optional[dict[str, str]] = None,
|
|
705
703
|
_experimental_proxy_ip: Optional[str] = None,
|
|
706
|
-
_experimental_custom_scaling_factor: Optional[float] = None,
|
|
707
704
|
restrict_output: bool = False,
|
|
708
705
|
http_config: Optional[api_pb2.HTTPConfig] = None,
|
|
709
706
|
) -> "_Function":
|
|
@@ -799,11 +796,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
799
796
|
f"for clustered Functions"
|
|
800
797
|
)
|
|
801
798
|
|
|
802
|
-
if _experimental_custom_scaling_factor is not None and (
|
|
803
|
-
_experimental_custom_scaling_factor < 0 or _experimental_custom_scaling_factor > 1
|
|
804
|
-
):
|
|
805
|
-
raise InvalidError("`_experimental_custom_scaling_factor` must be between 0.0 and 1.0 inclusive.")
|
|
806
|
-
|
|
807
799
|
if not cloud and not is_builder_function:
|
|
808
800
|
cloud = config.get("default_cloud")
|
|
809
801
|
|
|
@@ -1050,7 +1042,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1050
1042
|
_experimental_group_size=cluster_size or 0, # Experimental: Clustered functions
|
|
1051
1043
|
_experimental_concurrent_cancellations=True,
|
|
1052
1044
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1053
|
-
_experimental_custom_scaling=_experimental_custom_scaling_factor is not None,
|
|
1054
1045
|
# --- These are deprecated in favor of autoscaler_settings
|
|
1055
1046
|
warm_pool_size=min_containers or 0,
|
|
1056
1047
|
concurrency_limit=max_containers or 0,
|
|
@@ -1122,8 +1113,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1122
1113
|
function_data.ranked_functions.extend(ranked_functions)
|
|
1123
1114
|
function_definition = None # function_definition is not used in this case
|
|
1124
1115
|
else:
|
|
1125
|
-
# TODO(irfansharif): Assert on this specific type once we get rid of python 3.9.
|
|
1126
|
-
# assert isinstance(gpu, GPU_T) # includes the case where gpu==None case
|
|
1127
1116
|
function_definition.resources.CopyFrom(
|
|
1128
1117
|
convert_fn_config_to_resources_config(
|
|
1129
1118
|
cpu=cpu, memory=memory, gpu=gpu, ephemeral_disk=ephemeral_disk, rdma=rdma
|
|
@@ -1170,7 +1159,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1170
1159
|
obj._webhook_config = webhook_config # only set locally
|
|
1171
1160
|
|
|
1172
1161
|
# Used to check whether we should rebuild a modal.Image which uses `run_function`.
|
|
1173
|
-
gpus: list[
|
|
1162
|
+
gpus: list[str] = gpu if isinstance(gpu, list) else [gpu] if gpu else []
|
|
1174
1163
|
obj._build_args = dict( # See get_build_def
|
|
1175
1164
|
secrets=repr(secrets),
|
|
1176
1165
|
gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
|
|
@@ -1358,43 +1347,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1358
1347
|
# One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
|
|
1359
1348
|
# and then we could return them here (would need some ad hoc dataclass, which I don't love)
|
|
1360
1349
|
|
|
1361
|
-
@live_method
|
|
1362
|
-
async def keep_warm(self, warm_pool_size: int) -> None:
|
|
1363
|
-
"""mdmd:hidden
|
|
1364
|
-
Set the warm pool size for the Function.
|
|
1365
|
-
|
|
1366
|
-
DEPRECATED: Please adapt your code to use the more general `update_autoscaler` method instead:
|
|
1367
|
-
|
|
1368
|
-
```python notest
|
|
1369
|
-
f = modal.Function.from_name("my-app", "function")
|
|
1370
|
-
|
|
1371
|
-
# Old pattern (deprecated)
|
|
1372
|
-
f.keep_warm(2)
|
|
1373
|
-
|
|
1374
|
-
# New pattern
|
|
1375
|
-
f.update_autoscaler(min_containers=2)
|
|
1376
|
-
```
|
|
1377
|
-
"""
|
|
1378
|
-
if self._is_method:
|
|
1379
|
-
raise InvalidError(
|
|
1380
|
-
textwrap.dedent(
|
|
1381
|
-
"""
|
|
1382
|
-
The `.keep_warm()` method can not be used on Modal class *methods*.
|
|
1383
|
-
|
|
1384
|
-
Call `.keep_warm()` on the class *instance* instead. All methods of a class are run by the same
|
|
1385
|
-
container pool, and this method applies to the size of that container pool.
|
|
1386
|
-
"""
|
|
1387
|
-
)
|
|
1388
|
-
)
|
|
1389
|
-
|
|
1390
|
-
deprecation_warning(
|
|
1391
|
-
(2025, 5, 5),
|
|
1392
|
-
"The .keep_warm() method has been deprecated in favor of the more general "
|
|
1393
|
-
".update_autoscaler(min_containers=...) method.",
|
|
1394
|
-
show_source=True,
|
|
1395
|
-
)
|
|
1396
|
-
await self.update_autoscaler(min_containers=warm_pool_size)
|
|
1397
|
-
|
|
1398
1350
|
@classmethod
|
|
1399
1351
|
def _from_name(
|
|
1400
1352
|
cls,
|
|
@@ -1444,7 +1396,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1444
1396
|
app_name: str,
|
|
1445
1397
|
name: str,
|
|
1446
1398
|
*,
|
|
1447
|
-
namespace=None, # mdmd:line-hidden
|
|
1448
1399
|
environment_name: Optional[str] = None,
|
|
1449
1400
|
client: Optional[_Client] = None,
|
|
1450
1401
|
) -> "_Function":
|
|
@@ -1459,17 +1410,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1459
1410
|
```
|
|
1460
1411
|
"""
|
|
1461
1412
|
if "." in name:
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
"Looking up class methods using Function.from_name will be deprecated"
|
|
1466
|
-
" in a future version of Modal.\nUse modal.Cls.from_name instead, e.g.\n\n"
|
|
1467
|
-
f'{class_name} = modal.Cls.from_name("{app_name}", "{class_name}")\n'
|
|
1468
|
-
f"instance = {class_name}(...)\n"
|
|
1469
|
-
f"instance.{method_name}.remote(...)\n",
|
|
1413
|
+
raise InvalidError(
|
|
1414
|
+
f"Invalid Function name: {name!r}. "
|
|
1415
|
+
"If you are trying to look up a class method, use `modal.Cls.from_name` instead."
|
|
1470
1416
|
)
|
|
1471
1417
|
|
|
1472
|
-
warn_if_passing_namespace(namespace, "modal.Function.from_name")
|
|
1473
1418
|
return cls._from_name(
|
|
1474
1419
|
app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
|
|
1475
1420
|
)
|
|
@@ -1587,21 +1532,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1587
1532
|
+ f"or call it locally: {self._function_name}.local()"
|
|
1588
1533
|
)
|
|
1589
1534
|
|
|
1590
|
-
# TODO (live_method on properties is not great, since it could be blocking the event loop from async contexts)
|
|
1591
|
-
@property
|
|
1592
|
-
@live_method
|
|
1593
|
-
async def web_url(self) -> Optional[str]:
|
|
1594
|
-
"""mdmd:hidden
|
|
1595
|
-
Deprecated. Use the `Function.get_web_url()` method instead.
|
|
1596
|
-
|
|
1597
|
-
URL of a Function running as a web endpoint.
|
|
1598
|
-
"""
|
|
1599
|
-
deprecation_msg = """The Function.web_url property will be removed in a future version of Modal.
|
|
1600
|
-
Use the `Function.get_web_url()` method instead.
|
|
1601
|
-
"""
|
|
1602
|
-
deprecation_warning((2025, 5, 6), deprecation_msg, pending=True)
|
|
1603
|
-
return self._web_url
|
|
1604
|
-
|
|
1605
1535
|
@live_method
|
|
1606
1536
|
async def get_web_url(self) -> Optional[str]:
|
|
1607
1537
|
"""URL of a Function running as a web endpoint."""
|
|
@@ -1632,7 +1562,6 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1632
1562
|
input_queue: _SynchronizedQueue,
|
|
1633
1563
|
order_outputs: bool,
|
|
1634
1564
|
return_exceptions: bool,
|
|
1635
|
-
wrap_returned_exceptions: bool,
|
|
1636
1565
|
) -> AsyncGenerator[Any, None]:
|
|
1637
1566
|
"""mdmd:hidden
|
|
1638
1567
|
|
|
@@ -1658,7 +1587,6 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1658
1587
|
self.client,
|
|
1659
1588
|
order_outputs,
|
|
1660
1589
|
return_exceptions,
|
|
1661
|
-
wrap_returned_exceptions,
|
|
1662
1590
|
count_update_callback,
|
|
1663
1591
|
)
|
|
1664
1592
|
) as stream:
|
|
@@ -1672,7 +1600,6 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1672
1600
|
self.client,
|
|
1673
1601
|
order_outputs,
|
|
1674
1602
|
return_exceptions,
|
|
1675
|
-
wrap_returned_exceptions,
|
|
1676
1603
|
count_update_callback,
|
|
1677
1604
|
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
|
1678
1605
|
)
|
|
@@ -2110,13 +2037,3 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2110
2037
|
except Exception as exc:
|
|
2111
2038
|
# TODO: kill all running function calls
|
|
2112
2039
|
raise exc
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
async def _gather(*function_calls: _FunctionCall[T]) -> typing.Sequence[T]:
|
|
2116
|
-
"""mdmd:hidden
|
|
2117
|
-
Deprecated: Please use `modal.FunctionCall.gather()` instead."""
|
|
2118
|
-
deprecation_warning(
|
|
2119
|
-
(2025, 2, 24),
|
|
2120
|
-
"`modal.functions.gather()` is deprecated; please use `modal.FunctionCall.gather()` instead.",
|
|
2121
|
-
)
|
|
2122
|
-
return await _FunctionCall.gather(*function_calls)
|
|
@@ -147,6 +147,11 @@ async def get_app_logs_loop(
|
|
|
147
147
|
# doesn't support server-side search)
|
|
148
148
|
if search_text and search_text not in log.data:
|
|
149
149
|
return
|
|
150
|
+
# Search results may lack trailing newlines; ensure each
|
|
151
|
+
# match is a complete line so the LineBufferedOutput buffer
|
|
152
|
+
# doesn't merge it with the next match.
|
|
153
|
+
if search_text and not log.data.endswith("\n"):
|
|
154
|
+
log.data += "\n"
|
|
150
155
|
if pty_shell_finish_event:
|
|
151
156
|
await output_mgr.put_pty_content(log)
|
|
152
157
|
else:
|
|
@@ -17,7 +17,7 @@ from modal_proto import api_pb2
|
|
|
17
17
|
|
|
18
18
|
from ._functions import _Function
|
|
19
19
|
from ._utils.async_utils import synchronizer
|
|
20
|
-
from ._utils.deprecation import
|
|
20
|
+
from ._utils.deprecation import deprecation_error
|
|
21
21
|
from ._utils.function_utils import callable_has_non_self_params
|
|
22
22
|
from .config import config
|
|
23
23
|
from .exception import InvalidError
|
|
@@ -310,7 +310,7 @@ def _method(
|
|
|
310
310
|
def wrapper(obj: Union[Callable[..., Any], _PartialFunction]) -> _PartialFunction:
|
|
311
311
|
flags = _PartialFunctionFlags.CALLABLE_INTERFACE
|
|
312
312
|
|
|
313
|
-
nonlocal is_generator
|
|
313
|
+
nonlocal is_generator
|
|
314
314
|
if is_generator is None:
|
|
315
315
|
callable = obj.raw_f if isinstance(obj, _PartialFunction) else obj
|
|
316
316
|
is_generator = inspect.isgeneratorfunction(callable) or inspect.isasyncgenfunction(callable)
|
|
@@ -403,72 +403,16 @@ def _fastapi_endpoint(
|
|
|
403
403
|
|
|
404
404
|
def _web_endpoint(
|
|
405
405
|
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
custom_domains: Optional[
|
|
411
|
-
Iterable[str]
|
|
412
|
-
] = None, # Create an endpoint using a custom domain fully-qualified domain name (FQDN).
|
|
413
|
-
requires_proxy_auth: bool = False, # Require Modal-Key and Modal-Secret HTTP Headers on requests.
|
|
414
|
-
) -> Callable[
|
|
415
|
-
[Union[_PartialFunction[P, ReturnType, ReturnType], Callable[P, ReturnType]]],
|
|
416
|
-
_PartialFunction[P, ReturnType, ReturnType],
|
|
417
|
-
]:
|
|
418
|
-
"""Register a basic web endpoint with this application.
|
|
419
|
-
|
|
406
|
+
*args,
|
|
407
|
+
**kwargs,
|
|
408
|
+
) -> None:
|
|
409
|
+
"""mdmd:hidden
|
|
420
410
|
DEPRECATED: This decorator has been renamed to `@modal.fastapi_endpoint`.
|
|
421
|
-
|
|
422
|
-
This is the simple way to create a web endpoint on Modal. The function
|
|
423
|
-
behaves as a [FastAPI](https://fastapi.tiangolo.com/) handler and should
|
|
424
|
-
return a response object to the caller.
|
|
425
|
-
|
|
426
|
-
Endpoints created with `@modal.web_endpoint` are meant to be simple, single
|
|
427
|
-
request handlers and automatically have
|
|
428
|
-
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) enabled.
|
|
429
|
-
For more flexibility, use `@modal.asgi_app`.
|
|
430
|
-
|
|
431
|
-
To learn how to use Modal with popular web frameworks, see the
|
|
432
|
-
[guide on web endpoints](https://modal.com/docs/guide/webhooks).
|
|
433
411
|
"""
|
|
434
|
-
|
|
435
|
-
# Probably passing the method string as a positional argument.
|
|
436
|
-
raise InvalidError('Positional arguments are not allowed. Suggestion: `@modal.web_endpoint(method="GET")`.')
|
|
437
|
-
elif _warn_parentheses_missing is not None:
|
|
438
|
-
raise InvalidError(
|
|
439
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.web_endpoint()`."
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
deprecation_warning(
|
|
412
|
+
deprecation_error(
|
|
443
413
|
(2025, 3, 5), "The `@modal.web_endpoint` decorator has been renamed to `@modal.fastapi_endpoint`."
|
|
444
414
|
)
|
|
445
415
|
|
|
446
|
-
webhook_config = api_pb2.WebhookConfig(
|
|
447
|
-
type=api_pb2.WEBHOOK_TYPE_FUNCTION,
|
|
448
|
-
method=method,
|
|
449
|
-
web_endpoint_docs=docs,
|
|
450
|
-
requested_suffix=label or "",
|
|
451
|
-
ephemeral_suffix=config.get("dev_suffix"),
|
|
452
|
-
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
|
453
|
-
custom_domains=_parse_custom_domains(custom_domains),
|
|
454
|
-
requires_proxy_auth=requires_proxy_auth,
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
flags = _PartialFunctionFlags.WEB_INTERFACE
|
|
458
|
-
params = _PartialFunctionParams(webhook_config=webhook_config)
|
|
459
|
-
|
|
460
|
-
def wrapper(
|
|
461
|
-
obj: Union[_PartialFunction[P, ReturnType, ReturnType], Callable[P, ReturnType]],
|
|
462
|
-
) -> _PartialFunction[P, ReturnType, ReturnType]:
|
|
463
|
-
if isinstance(obj, _PartialFunction):
|
|
464
|
-
pf = obj.stack(flags, params)
|
|
465
|
-
else:
|
|
466
|
-
pf = _PartialFunction(obj, flags, params)
|
|
467
|
-
pf.validate_obj_compatibility("web_endpoint")
|
|
468
|
-
return pf
|
|
469
|
-
|
|
470
|
-
return wrapper
|
|
471
|
-
|
|
472
416
|
|
|
473
417
|
def _asgi_app(
|
|
474
418
|
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
@@ -3,19 +3,18 @@ from typing import Optional, Union
|
|
|
3
3
|
|
|
4
4
|
from modal_proto import api_pb2
|
|
5
5
|
|
|
6
|
+
from ._utils.function_utils import parse_gpu_config
|
|
6
7
|
from .exception import InvalidError
|
|
7
|
-
from .gpu import GPU_T, parse_gpu_config
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def convert_fn_config_to_resources_config(
|
|
11
11
|
*,
|
|
12
12
|
cpu: Optional[Union[float, tuple[float, float]]],
|
|
13
13
|
memory: Optional[Union[int, tuple[int, int]]],
|
|
14
|
-
gpu:
|
|
14
|
+
gpu: Optional[str],
|
|
15
15
|
ephemeral_disk: Optional[int],
|
|
16
16
|
rdma: Optional[bool] = None,
|
|
17
17
|
) -> api_pb2.Resources:
|
|
18
|
-
gpu_config = parse_gpu_config(gpu)
|
|
19
18
|
if cpu and isinstance(cpu, tuple):
|
|
20
19
|
if not cpu[0]:
|
|
21
20
|
raise InvalidError("CPU request must be a positive number")
|
|
@@ -45,7 +44,7 @@ def convert_fn_config_to_resources_config(
|
|
|
45
44
|
return api_pb2.Resources(
|
|
46
45
|
milli_cpu=milli_cpu,
|
|
47
46
|
milli_cpu_max=milli_cpu_max,
|
|
48
|
-
gpu_config=
|
|
47
|
+
gpu_config=parse_gpu_config(gpu),
|
|
49
48
|
memory_mb=memory_mb,
|
|
50
49
|
memory_mb_max=memory_mb_max,
|
|
51
50
|
ephemeral_disk_mb=ephemeral_disk,
|
|
@@ -349,19 +349,18 @@ def retry(
|
|
|
349
349
|
direct_fn=None,
|
|
350
350
|
*,
|
|
351
351
|
n_attempts=3,
|
|
352
|
-
base_delay=0,
|
|
352
|
+
base_delay=0.0,
|
|
353
353
|
delay_factor=2,
|
|
354
|
+
max_delay: Optional[float] = None,
|
|
354
355
|
attempt_timeout: Optional[float] = 90,
|
|
355
|
-
|
|
356
|
+
total_timeout: Optional[float] = None,
|
|
356
357
|
):
|
|
357
358
|
"""Decorator that calls an async function multiple times, with a given timeout.
|
|
358
359
|
|
|
359
360
|
If a `base_delay` is provided, the function is given an exponentially
|
|
360
361
|
increasing delay on each run, up until the maximum number of attempts.
|
|
361
362
|
|
|
362
|
-
If `
|
|
363
|
-
increases exponentially (starting from `attempt_timeout`, multiplied by
|
|
364
|
-
`attempt_timeout_factor` after each failed attempt).
|
|
363
|
+
If `max_delay` is set, the delay between attempts is capped at this value.
|
|
365
364
|
|
|
366
365
|
Usage:
|
|
367
366
|
|
|
@@ -377,6 +376,7 @@ def retry(
|
|
|
377
376
|
pass
|
|
378
377
|
```
|
|
379
378
|
"""
|
|
379
|
+
ATTEMPT_TIMEOUT_FLOOR = 2.0
|
|
380
380
|
|
|
381
381
|
def decorator(fn):
|
|
382
382
|
@functools.wraps(fn)
|
|
@@ -387,17 +387,35 @@ def retry(
|
|
|
387
387
|
local_n_attempts = n_attempts
|
|
388
388
|
|
|
389
389
|
delay = base_delay
|
|
390
|
-
|
|
390
|
+
if total_timeout is not None:
|
|
391
|
+
total_deadline = time.time() + total_timeout
|
|
392
|
+
else:
|
|
393
|
+
total_deadline = None
|
|
394
|
+
|
|
391
395
|
for i in range(local_n_attempts):
|
|
392
396
|
t0 = time.time()
|
|
397
|
+
|
|
398
|
+
timeouts = []
|
|
399
|
+
if attempt_timeout is not None:
|
|
400
|
+
timeouts.append(attempt_timeout)
|
|
401
|
+
if total_deadline is not None:
|
|
402
|
+
timeouts.append(max(total_deadline - time.time(), ATTEMPT_TIMEOUT_FLOOR))
|
|
403
|
+
timeout = min(timeouts) if timeouts else None
|
|
404
|
+
|
|
393
405
|
try:
|
|
394
|
-
return await asyncio.wait_for(fn(*args, **kwargs), timeout=
|
|
406
|
+
return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
|
|
395
407
|
except asyncio.CancelledError:
|
|
396
408
|
logger.debug(f"Function {fn} was cancelled")
|
|
397
409
|
raise
|
|
398
410
|
except Exception as e:
|
|
399
411
|
if i >= local_n_attempts - 1:
|
|
400
412
|
raise
|
|
413
|
+
|
|
414
|
+
# We check if the timeout will be reached **after** the sleep, so we can raise an error early
|
|
415
|
+
# without needing to actually sleep.
|
|
416
|
+
if total_deadline is not None and time.time() + delay + ATTEMPT_TIMEOUT_FLOOR >= total_deadline:
|
|
417
|
+
raise
|
|
418
|
+
|
|
401
419
|
logger.debug(
|
|
402
420
|
f"Failed invoking function {fn}: {e}"
|
|
403
421
|
f" (took {time.time() - t0}s, sleeping {delay}s"
|
|
@@ -405,8 +423,8 @@ def retry(
|
|
|
405
423
|
)
|
|
406
424
|
await asyncio.sleep(delay)
|
|
407
425
|
delay *= delay_factor
|
|
408
|
-
if
|
|
409
|
-
|
|
426
|
+
if max_delay is not None:
|
|
427
|
+
delay = min(delay, max_delay)
|
|
410
428
|
|
|
411
429
|
return f_wrapped
|
|
412
430
|
|
|
@@ -89,55 +89,34 @@ def renamed_parameter(
|
|
|
89
89
|
return decorator
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"container_idle_timeout": "scaledown_window",
|
|
98
|
-
}
|
|
92
|
+
def handle_deprecated_parameters(func: Callable[P, R]) -> Callable[P, R]:
|
|
93
|
+
"""Meta-decorator for modal.Function decorators that errors when deprecated parameters are passed.
|
|
94
|
+
|
|
95
|
+
We use this for a select parameters that might still be suggested by LLMs.
|
|
96
|
+
"""
|
|
99
97
|
|
|
100
98
|
@functools.wraps(func)
|
|
101
99
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
_kwargs: dict[str, Any] = locals()["kwargs"] # Avoid referencing kwargs directly due to bug in sigtools
|
|
101
|
+
|
|
102
|
+
# Basic autoscaler parameters
|
|
103
|
+
autoscaler_settings = {
|
|
104
|
+
"keep_warm": "min_containers",
|
|
105
|
+
"concurrency_limit": "max_containers",
|
|
106
|
+
"container_idle_timeout": "scaledown_window",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for old_name, new_name in autoscaler_settings.items():
|
|
110
|
+
if old_name in _kwargs:
|
|
111
|
+
deprecation_error((2025, 2, 24), f"The '{old_name}' parameter has been renamed to '{new_name}'.")
|
|
112
|
+
|
|
113
|
+
# Input concurrency
|
|
114
|
+
if "allow_concurrent_inputs" in _kwargs:
|
|
115
|
+
deprecation_error(
|
|
116
|
+
(2025, 4, 9),
|
|
117
|
+
"The 'allow_concurrent_inputs' parameter is deprecated. Use the '@modal.concurrent' decorator instead.",
|
|
119
118
|
)
|
|
120
|
-
deprecation_warning((2025, 2, 24), message, show_source=True)
|
|
121
119
|
|
|
122
120
|
return func(*args, **kwargs)
|
|
123
121
|
|
|
124
122
|
return wrapper
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def warn_if_passing_namespace(
|
|
128
|
-
namespace: Any,
|
|
129
|
-
resource_name: str,
|
|
130
|
-
) -> None:
|
|
131
|
-
"""Issue deprecation warning for namespace parameter if non-None value is passed.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
namespace: The namespace parameter value (may be None or actual value)
|
|
135
|
-
resource_name: Name of the resource type for the warning message
|
|
136
|
-
"""
|
|
137
|
-
if namespace is not None:
|
|
138
|
-
deprecation_warning(
|
|
139
|
-
(2025, 6, 30),
|
|
140
|
-
f"The `namespace` parameter for `{resource_name}` is deprecated and will be"
|
|
141
|
-
" removed in a future release. It is no longer needed, so can be removed"
|
|
142
|
-
" from your code.",
|
|
143
|
-
)
|
|
@@ -602,3 +602,20 @@ async def _create_input(
|
|
|
602
602
|
),
|
|
603
603
|
idx=idx,
|
|
604
604
|
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def parse_gpu_config(value: Optional[str]) -> api_pb2.GPUConfig:
|
|
608
|
+
if value is None:
|
|
609
|
+
return api_pb2.GPUConfig()
|
|
610
|
+
elif isinstance(value, str):
|
|
611
|
+
count = 1
|
|
612
|
+
if ":" in value:
|
|
613
|
+
value, count_str = value.split(":", 1)
|
|
614
|
+
try:
|
|
615
|
+
count = int(count_str)
|
|
616
|
+
except ValueError:
|
|
617
|
+
raise InvalidError(f"Invalid GPU count: {count_str}. Value must be an integer.")
|
|
618
|
+
gpu_type = value.upper()
|
|
619
|
+
return api_pb2.GPUConfig(gpu_type=gpu_type, count=count)
|
|
620
|
+
else:
|
|
621
|
+
raise InvalidError(f"Invalid GPU config: {value!r}. Value must be a string or `None`")
|
|
@@ -90,8 +90,6 @@ RETRYABLE_GRPC_STATUS_CODES = [
|
|
|
90
90
|
]
|
|
91
91
|
SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
|
|
92
92
|
# Initial per-attempt timeout for TCP/TLS handshake in connect_channel.
|
|
93
|
-
# Doubles each attempt (attempt_timeout_factor=2): 0.5 + 1 + 2 + 4 + 8 = 15.5s + ~1.5s inter-attempt delays.
|
|
94
|
-
CONNECT_TIMEOUT = 0.5
|
|
95
93
|
DEFAULT_MAX_RETRIES = 3
|
|
96
94
|
|
|
97
95
|
|
|
@@ -225,7 +223,8 @@ def create_channel(
|
|
|
225
223
|
return channel
|
|
226
224
|
|
|
227
225
|
|
|
228
|
-
|
|
226
|
+
# With 18 attempts, the max delays between calls is: 0.1 + 0.2 + 0.4 + 0.8 + 1.6 + 3.2 + 5*11 ~ 62
|
|
227
|
+
@retry(n_attempts=18, base_delay=0.1, attempt_timeout=10.0, max_delay=5.0, total_timeout=63.0)
|
|
229
228
|
async def connect_channel(channel: grpclib.client.Channel):
|
|
230
229
|
"""Connect to socket and raise exceptions when there is a connection issue."""
|
|
231
230
|
await channel.__connect__()
|
|
@@ -11,7 +11,6 @@ from typing import AsyncGenerator, Optional
|
|
|
11
11
|
|
|
12
12
|
import grpclib.client
|
|
13
13
|
import grpclib.config
|
|
14
|
-
import grpclib.events
|
|
15
14
|
from grpclib import GRPCError, Status
|
|
16
15
|
from grpclib.exceptions import StreamTerminatedError
|
|
17
16
|
|
|
@@ -22,8 +21,21 @@ from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
|
22
21
|
|
|
23
22
|
from .._grpc_client import grpc_error_converter
|
|
24
23
|
from .._utils.grpc_utils import PermanentCloseableChannel
|
|
25
|
-
from .async_utils import aclosing
|
|
26
|
-
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
24
|
+
from .async_utils import aclosing, retry
|
|
25
|
+
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
|
|
29
|
+
async def _connect_channel(channel: grpclib.client.Channel):
|
|
30
|
+
"""Connect to the command router channel.
|
|
31
|
+
|
|
32
|
+
Uses a longer retry budget than grpc_utils.connect_channel. In rare cases the sandbox
|
|
33
|
+
may take a long time to start on the worker after scheduling.
|
|
34
|
+
|
|
35
|
+
Retries with exponential backoff (1, 2, 4, 8, 10, 10, ...) capped at 10s per delay.
|
|
36
|
+
Total sleep between attempts: 1 + 2 + 4 + 8 + 10*29 = 305s (~5 min).
|
|
37
|
+
"""
|
|
38
|
+
await channel.__connect__()
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
def _b64url_decode(data: str) -> bytes:
|
|
@@ -169,7 +181,7 @@ class TaskCommandRouterClient:
|
|
|
169
181
|
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
170
182
|
)
|
|
171
183
|
|
|
172
|
-
await
|
|
184
|
+
await _connect_channel(channel)
|
|
173
185
|
loop = asyncio.get_running_loop()
|
|
174
186
|
jwt_refresh_lock = asyncio.Lock()
|
|
175
187
|
|