modal 0.73.131__tar.gz → 0.73.132__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-0.73.131 → modal-0.73.132}/PKG-INFO +1 -1
- {modal-0.73.131 → modal-0.73.132}/modal/__init__.py +2 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_container_entrypoint.py +2 -2
- {modal-0.73.131 → modal-0.73.132}/modal/_functions.py +4 -2
- {modal-0.73.131 → modal-0.73.132}/modal/_partial_function.py +78 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/container_io_manager.py +9 -4
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/container_io_manager.pyi +6 -0
- {modal-0.73.131 → modal-0.73.132}/modal/app.py +34 -5
- {modal-0.73.131 → modal-0.73.132}/modal/app.pyi +3 -2
- {modal-0.73.131 → modal-0.73.132}/modal/client.pyi +2 -2
- {modal-0.73.131 → modal-0.73.132}/modal/functions.pyi +8 -7
- {modal-0.73.131 → modal-0.73.132}/modal/partial_function.py +2 -0
- {modal-0.73.131 → modal-0.73.132}/modal/partial_function.pyi +9 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/PKG-INFO +1 -1
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api.proto +2 -2
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api_pb2.py +1 -1
- {modal-0.73.131 → modal-0.73.132}/modal_version/_version_generated.py +1 -1
- {modal-0.73.131 → modal-0.73.132}/LICENSE +0 -0
- {modal-0.73.131 → modal-0.73.132}/README.md +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/__main__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_clustered_functions.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_clustered_functions.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_ipython.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_location.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_object.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_output.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_proxy_tunnel.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_pty.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_resolver.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_resources.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/asgi.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/execution_context.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/execution_context.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/telemetry.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_runtime/user_code_imports.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_serialization.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_traceback.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_tunnel.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_tunnel.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_type_manager.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/app_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/async_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/blob_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/deprecation.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/docker_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/function_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/git_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/grpc_testing.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/grpc_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/hash_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/http_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/jwt_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/logger.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/mount_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/name_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/package_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/pattern_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_utils/shell_utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_vendor/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_vendor/cloudpickle.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_vendor/tblib.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/_watcher.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/call_graph.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/_download.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/_traceback.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/app.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/config.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/container.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/dict.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/entry_point.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/environment.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/import_refs.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/launch.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/network_file_system.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/profile.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/programs/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/programs/vscode.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/queues.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/run.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/secret.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/token.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/utils.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cli/volume.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/client.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cloud_bucket_mount.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cls.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/cls.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/config.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/container_process.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/container_process.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/dict.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/dict.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/environments.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/environments.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/exception.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/experimental.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/experimental.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/extensions/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/extensions/ipython.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/file_io.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/file_io.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/file_pattern_matcher.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/functions.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/gpu.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/image.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/image.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/io_streams.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/io_streams.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/mount.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/mount.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/network_file_system.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/network_file_system.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/object.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/object.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/output.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/parallel_map.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/parallel_map.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/proxy.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/proxy.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/py.typed +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/queue.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/queue.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/2023.12.312.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/2023.12.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/2024.04.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/2024.10.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/PREVIEW.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/README.md +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/requirements/base-images.json +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/retries.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/runner.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/runner.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/running_app.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/sandbox.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/sandbox.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/schedule.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/scheduler_placement.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/secret.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/secret.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/serving.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/serving.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/snapshot.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/snapshot.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/stream_type.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/token_flow.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/token_flow.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/volume.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal/volume.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/SOURCES.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/dependency_links.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/entry_points.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/requires.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal.egg-info/top_level.txt +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/gen_cli_docs.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/gen_reference_docs.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/mdmd/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_docs/mdmd/signatures.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api_pb2.pyi +6 -6
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/modal_api_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/modal_options_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options.proto +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options_pb2.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options_pb2.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options_pb2_grpc.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/options_pb2_grpc.pyi +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_proto/py.typed +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_version/__init__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/modal_version/__main__.py +0 -0
- {modal-0.73.131 → modal-0.73.132}/pyproject.toml +0 -0
- {modal-0.73.131 → modal-0.73.132}/setup.cfg +0 -0
@@ -27,6 +27,7 @@ try:
|
|
27
27
|
asgi_app,
|
28
28
|
batched,
|
29
29
|
build,
|
30
|
+
concurrent,
|
30
31
|
enter,
|
31
32
|
exit,
|
32
33
|
fastapi_endpoint,
|
@@ -82,6 +83,7 @@ __all__ = [
|
|
82
83
|
"asgi_app",
|
83
84
|
"batched",
|
84
85
|
"build",
|
86
|
+
"concurrent",
|
85
87
|
"current_function_call_id",
|
86
88
|
"current_input_id",
|
87
89
|
"enable_output",
|
@@ -273,7 +273,7 @@ def call_function(
|
|
273
273
|
)
|
274
274
|
reset_context()
|
275
275
|
|
276
|
-
if container_io_manager.
|
276
|
+
if container_io_manager.input_concurrency_enabled:
|
277
277
|
with DaemonizedThreadPool(max_threads=container_io_manager.max_concurrency) as thread_pool:
|
278
278
|
|
279
279
|
def make_async_cancel_callback(task):
|
@@ -293,7 +293,7 @@ def call_function(
|
|
293
293
|
if not did_sigint:
|
294
294
|
did_sigint = True
|
295
295
|
logger.warning(
|
296
|
-
"User cancelling input of non-async functions with
|
296
|
+
"User cancelling input of non-async functions with input concurrency enabled.\n"
|
297
297
|
"This shuts down the container, causing concurrently running inputs to be "
|
298
298
|
"rescheduled in other containers."
|
299
299
|
)
|
@@ -440,7 +440,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
440
440
|
max_containers: Optional[int] = None,
|
441
441
|
buffer_containers: Optional[int] = None,
|
442
442
|
scaledown_window: Optional[int] = None,
|
443
|
-
|
443
|
+
max_concurrent_inputs: Optional[int] = None,
|
444
|
+
target_concurrent_inputs: Optional[int] = None,
|
444
445
|
batch_max_size: Optional[int] = None,
|
445
446
|
batch_wait_ms: Optional[int] = None,
|
446
447
|
cloud: Optional[str] = None,
|
@@ -791,7 +792,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
791
792
|
runtime_perf_record=config.get("runtime_perf_record"),
|
792
793
|
app_name=app_name,
|
793
794
|
is_builder_function=is_builder_function,
|
794
|
-
|
795
|
+
max_concurrent_inputs=max_concurrent_inputs or 0,
|
796
|
+
target_concurrent_inputs=target_concurrent_inputs or 0,
|
795
797
|
batch_max_size=batch_max_size or 0,
|
796
798
|
batch_linger_ms=batch_wait_ms or 0,
|
797
799
|
worker_id=config.get("worker_id"),
|
@@ -59,6 +59,8 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
59
59
|
force_build: bool
|
60
60
|
cluster_size: Optional[int] # Experimental: Clustered functions
|
61
61
|
build_timeout: Optional[int]
|
62
|
+
max_concurrent_inputs: Optional[int]
|
63
|
+
target_concurrent_inputs: Optional[int]
|
62
64
|
|
63
65
|
def __init__(
|
64
66
|
self,
|
@@ -72,6 +74,8 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
72
74
|
cluster_size: Optional[int] = None, # Experimental: Clustered functions
|
73
75
|
force_build: bool = False,
|
74
76
|
build_timeout: Optional[int] = None,
|
77
|
+
max_concurrent_inputs: Optional[int] = None,
|
78
|
+
target_concurrent_inputs: Optional[int] = None,
|
75
79
|
):
|
76
80
|
self.raw_f = raw_f
|
77
81
|
self.flags = flags
|
@@ -89,6 +93,8 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
89
93
|
self.cluster_size = cluster_size # Experimental: Clustered functions
|
90
94
|
self.force_build = force_build
|
91
95
|
self.build_timeout = build_timeout
|
96
|
+
self.max_concurrent_inputs = max_concurrent_inputs
|
97
|
+
self.target_concurrent_inputs = target_concurrent_inputs
|
92
98
|
|
93
99
|
def _get_raw_f(self) -> Callable[P, ReturnType]:
|
94
100
|
return self.raw_f
|
@@ -143,6 +149,8 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
143
149
|
batch_wait_ms=self.batch_wait_ms,
|
144
150
|
force_build=self.force_build,
|
145
151
|
build_timeout=self.build_timeout,
|
152
|
+
max_concurrent_inputs=self.max_concurrent_inputs,
|
153
|
+
target_concurrent_inputs=self.target_concurrent_inputs,
|
146
154
|
)
|
147
155
|
|
148
156
|
|
@@ -722,3 +730,73 @@ def _batched(
|
|
722
730
|
)
|
723
731
|
|
724
732
|
return wrapper
|
733
|
+
|
734
|
+
|
735
|
+
def _concurrent(
|
736
|
+
_warn_parentheses_missing=None,
|
737
|
+
*,
|
738
|
+
max_inputs: int, # Hard limit on each container's input concurrency
|
739
|
+
target_inputs: Optional[int] = None, # Input concurrency that Modal's autoscaler should target
|
740
|
+
) -> Callable[[Union[Callable[..., Any], _PartialFunction]], _PartialFunction]:
|
741
|
+
"""Decorator that allows individual containers to handle multiple inputs concurrently.
|
742
|
+
|
743
|
+
The concurrency mechanism depends on whether the function is async or not:
|
744
|
+
- Async functions will run inputs on a single thread as asyncio tasks.
|
745
|
+
- Synchronous functions will use multi-threading. The code must be thread-safe.
|
746
|
+
|
747
|
+
Input concurrency will be most useful for workflows that are IO-bound
|
748
|
+
(e.g., making network requests) or when running an inference server that supports
|
749
|
+
dynamic batching.
|
750
|
+
|
751
|
+
When `target_inputs` is set, Modal's autoscaler will try to provision resources
|
752
|
+
such that each container is running that many inputs concurrently, rather than
|
753
|
+
autoscaling based on `max_inputs`. Containers may burst up to up to `max_inputs`
|
754
|
+
if resources are insufficient to remain at the target concurrency, e.g. when the
|
755
|
+
arrival rate of inputs increases. This can trade-off a small increase in average
|
756
|
+
latency to avoid larger tail latencies from input queuing.
|
757
|
+
|
758
|
+
**Examples:**
|
759
|
+
```python
|
760
|
+
# Stack the decorator under `@app.function()` to enable input concurrency
|
761
|
+
@app.function()
|
762
|
+
@modal.concurrent(max_inputs=100)
|
763
|
+
async def f(data):
|
764
|
+
# Async function; will be scheduled as asyncio task
|
765
|
+
...
|
766
|
+
|
767
|
+
# With `@app.cls()`, apply the decorator at the class level, not on individual methods
|
768
|
+
@app.cls()
|
769
|
+
@modal.concurrent(max_inputs=100, target_inputs=80)
|
770
|
+
class C:
|
771
|
+
@modal.method()
|
772
|
+
def f(self, data):
|
773
|
+
# Sync function; must be thread-safe
|
774
|
+
...
|
775
|
+
|
776
|
+
```
|
777
|
+
|
778
|
+
"""
|
779
|
+
if _warn_parentheses_missing is not None:
|
780
|
+
raise InvalidError(
|
781
|
+
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.concurrent()`."
|
782
|
+
)
|
783
|
+
|
784
|
+
if target_inputs and target_inputs > max_inputs:
|
785
|
+
raise InvalidError("`target_inputs` parameter cannot be greater than `max_inputs`.")
|
786
|
+
|
787
|
+
def wrapper(obj: Union[Callable[..., Any], _PartialFunction]) -> _PartialFunction:
|
788
|
+
if isinstance(obj, _PartialFunction):
|
789
|
+
# Risky that we need to mutate the parameters here; should make this safer
|
790
|
+
obj.max_concurrent_inputs = max_inputs
|
791
|
+
obj.target_concurrent_inputs = target_inputs
|
792
|
+
obj.add_flags(_PartialFunctionFlags.FUNCTION)
|
793
|
+
return obj
|
794
|
+
|
795
|
+
return _PartialFunction(
|
796
|
+
obj,
|
797
|
+
_PartialFunctionFlags.FUNCTION,
|
798
|
+
max_concurrent_inputs=max_inputs,
|
799
|
+
target_concurrent_inputs=target_inputs,
|
800
|
+
)
|
801
|
+
|
802
|
+
return wrapper
|
@@ -264,6 +264,7 @@ class _ContainerIOManager:
|
|
264
264
|
current_inputs: dict[str, IOContext] # input_id -> IOContext
|
265
265
|
current_input_started_at: Optional[float]
|
266
266
|
|
267
|
+
_input_concurrency_enabled: bool
|
267
268
|
_target_concurrency: int
|
268
269
|
_max_concurrency: int
|
269
270
|
_concurrency_loop: Optional[asyncio.Task]
|
@@ -296,14 +297,14 @@ class _ContainerIOManager:
|
|
296
297
|
self.current_input_started_at = None
|
297
298
|
|
298
299
|
if container_args.function_def.pty_info.pty_type == api_pb2.PTYInfo.PTY_TYPE_SHELL:
|
299
|
-
target_concurrency = 1
|
300
300
|
max_concurrency = 1
|
301
|
+
target_concurrency = 1
|
301
302
|
else:
|
302
|
-
|
303
|
-
|
303
|
+
max_concurrency = container_args.function_def.max_concurrent_inputs or 1
|
304
|
+
target_concurrency = container_args.function_def.target_concurrent_inputs or max_concurrency
|
304
305
|
|
305
|
-
self._target_concurrency = target_concurrency
|
306
306
|
self._max_concurrency = max_concurrency
|
307
|
+
self._target_concurrency = target_concurrency
|
307
308
|
self._concurrency_loop = None
|
308
309
|
self._stop_concurrency_loop = False
|
309
310
|
self._input_slots = InputSlots(target_concurrency)
|
@@ -976,6 +977,10 @@ class _ContainerIOManager:
|
|
976
977
|
def max_concurrency(self) -> int:
|
977
978
|
return self._max_concurrency
|
978
979
|
|
980
|
+
@property
|
981
|
+
def input_concurrency_enabled(self) -> int:
|
982
|
+
return max(self._max_concurrency, self._target_concurrency) > 1
|
983
|
+
|
979
984
|
@classmethod
|
980
985
|
def get_input_concurrency(cls) -> int:
|
981
986
|
"""
|
@@ -69,6 +69,7 @@ class _ContainerIOManager:
|
|
69
69
|
current_input_id: typing.Optional[str]
|
70
70
|
current_inputs: dict[str, IOContext]
|
71
71
|
current_input_started_at: typing.Optional[float]
|
72
|
+
_input_concurrency_enabled: bool
|
72
73
|
_target_concurrency: int
|
73
74
|
_max_concurrency: int
|
74
75
|
_concurrency_loop: typing.Optional[asyncio.Task]
|
@@ -149,6 +150,8 @@ class _ContainerIOManager:
|
|
149
150
|
def target_concurrency(self) -> int: ...
|
150
151
|
@property
|
151
152
|
def max_concurrency(self) -> int: ...
|
153
|
+
@property
|
154
|
+
def input_concurrency_enabled(self) -> int: ...
|
152
155
|
@classmethod
|
153
156
|
def get_input_concurrency(cls) -> int: ...
|
154
157
|
@classmethod
|
@@ -169,6 +172,7 @@ class ContainerIOManager:
|
|
169
172
|
current_input_id: typing.Optional[str]
|
170
173
|
current_inputs: dict[str, IOContext]
|
171
174
|
current_input_started_at: typing.Optional[float]
|
175
|
+
_input_concurrency_enabled: bool
|
172
176
|
_target_concurrency: int
|
173
177
|
_max_concurrency: int
|
174
178
|
_concurrency_loop: typing.Optional[asyncio.Task]
|
@@ -384,6 +388,8 @@ class ContainerIOManager:
|
|
384
388
|
def target_concurrency(self) -> int: ...
|
385
389
|
@property
|
386
390
|
def max_concurrency(self) -> int: ...
|
391
|
+
@property
|
392
|
+
def input_concurrency_enabled(self) -> int: ...
|
387
393
|
@classmethod
|
388
394
|
def get_input_concurrency(cls) -> int: ...
|
389
395
|
@classmethod
|
@@ -678,6 +678,12 @@ class _App:
|
|
678
678
|
is_generator = f.is_generator
|
679
679
|
batch_max_size = f.batch_max_size
|
680
680
|
batch_wait_ms = f.batch_wait_ms
|
681
|
+
if f.max_concurrent_inputs: # Using @modal.concurrent()
|
682
|
+
max_concurrent_inputs = f.max_concurrent_inputs
|
683
|
+
target_concurrent_inputs = f.target_concurrent_inputs
|
684
|
+
else:
|
685
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
686
|
+
target_concurrent_inputs = None
|
681
687
|
else:
|
682
688
|
if not is_global_object(f.__qualname__) and not serialized:
|
683
689
|
raise InvalidError(
|
@@ -709,10 +715,12 @@ class _App:
|
|
709
715
|
)
|
710
716
|
|
711
717
|
info = FunctionInfo(f, serialized=serialized, name_override=name)
|
718
|
+
raw_f = f
|
712
719
|
webhook_config = None
|
713
720
|
batch_max_size = None
|
714
721
|
batch_wait_ms = None
|
715
|
-
|
722
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
723
|
+
target_concurrent_inputs = None
|
716
724
|
|
717
725
|
cluster_size = None # Experimental: Clustered functions
|
718
726
|
i6pn_enabled = i6pn
|
@@ -753,7 +761,8 @@ class _App:
|
|
753
761
|
max_containers=max_containers,
|
754
762
|
buffer_containers=buffer_containers,
|
755
763
|
scaledown_window=scaledown_window,
|
756
|
-
|
764
|
+
max_concurrent_inputs=max_concurrent_inputs,
|
765
|
+
target_concurrent_inputs=target_concurrent_inputs,
|
757
766
|
batch_max_size=batch_max_size,
|
758
767
|
batch_wait_ms=batch_wait_ms,
|
759
768
|
timeout=timeout,
|
@@ -832,7 +841,7 @@ class _App:
|
|
832
841
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
833
842
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
834
843
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
835
|
-
) -> Callable[[CLS_T], CLS_T]:
|
844
|
+
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
836
845
|
"""
|
837
846
|
Decorator to register a new Modal [Cls](/docs/reference/modal.Cls) with this App.
|
838
847
|
"""
|
@@ -845,8 +854,21 @@ class _App:
|
|
845
854
|
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
846
855
|
scheduler_placement = SchedulerPlacement(region=region)
|
847
856
|
|
848
|
-
def wrapper(
|
857
|
+
def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
|
849
858
|
# Check if the decorated object is a class
|
859
|
+
if isinstance(wrapped_cls, _PartialFunction):
|
860
|
+
wrapped_cls.wrapped = True
|
861
|
+
user_cls = wrapped_cls.raw_f
|
862
|
+
if wrapped_cls.max_concurrent_inputs: # Using @modal.concurrent()
|
863
|
+
max_concurrent_inputs = wrapped_cls.max_concurrent_inputs
|
864
|
+
target_concurrent_inputs = wrapped_cls.target_concurrent_inputs
|
865
|
+
else:
|
866
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
867
|
+
target_concurrent_inputs = None
|
868
|
+
else:
|
869
|
+
user_cls = wrapped_cls
|
870
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
871
|
+
target_concurrent_inputs = None
|
850
872
|
if not inspect.isclass(user_cls):
|
851
873
|
raise TypeError("The @app.cls decorator must be used on a class.")
|
852
874
|
|
@@ -871,6 +893,12 @@ class _App:
|
|
871
893
|
):
|
872
894
|
raise InvalidError("A class must have `enable_memory_snapshot=True` to use `snap=True` on its methods.")
|
873
895
|
|
896
|
+
for method in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.FUNCTION).values():
|
897
|
+
if method.max_concurrent_inputs:
|
898
|
+
raise InvalidError(
|
899
|
+
"The `@modal.concurrent` decorator cannot be used on methods; decorate the class instead."
|
900
|
+
)
|
901
|
+
|
874
902
|
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)
|
875
903
|
|
876
904
|
cls_func = _Function.from_local(
|
@@ -892,7 +920,8 @@ class _App:
|
|
892
920
|
scaledown_window=scaledown_window,
|
893
921
|
proxy=proxy,
|
894
922
|
retries=retries,
|
895
|
-
|
923
|
+
max_concurrent_inputs=max_concurrent_inputs,
|
924
|
+
target_concurrent_inputs=target_concurrent_inputs,
|
896
925
|
batch_max_size=batch_max_size,
|
897
926
|
batch_wait_ms=batch_wait_ms,
|
898
927
|
timeout=timeout,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import collections.abc
|
2
2
|
import modal._functions
|
3
3
|
import modal._object
|
4
|
+
import modal._partial_function
|
4
5
|
import modal._utils.function_utils
|
5
6
|
import modal.client
|
6
7
|
import modal.cloud_bucket_mount
|
@@ -247,7 +248,7 @@ class _App:
|
|
247
248
|
concurrency_limit: typing.Optional[int] = None,
|
248
249
|
container_idle_timeout: typing.Optional[int] = None,
|
249
250
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
250
|
-
) -> collections.abc.Callable[[CLS_T], CLS_T]: ...
|
251
|
+
) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]: ...
|
251
252
|
async def spawn_sandbox(
|
252
253
|
self,
|
253
254
|
*entrypoint_args: str,
|
@@ -487,7 +488,7 @@ class App:
|
|
487
488
|
concurrency_limit: typing.Optional[int] = None,
|
488
489
|
container_idle_timeout: typing.Optional[int] = None,
|
489
490
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
490
|
-
) -> collections.abc.Callable[[CLS_T], CLS_T]: ...
|
491
|
+
) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]: ...
|
491
492
|
|
492
493
|
class __spawn_sandbox_spec(typing_extensions.Protocol[SUPERSELF]):
|
493
494
|
def __call__(
|
@@ -31,7 +31,7 @@ class _Client:
|
|
31
31
|
server_url: str,
|
32
32
|
client_type: int,
|
33
33
|
credentials: typing.Optional[tuple[str, str]],
|
34
|
-
version: str = "0.73.
|
34
|
+
version: str = "0.73.132",
|
35
35
|
): ...
|
36
36
|
def is_closed(self) -> bool: ...
|
37
37
|
@property
|
@@ -93,7 +93,7 @@ class Client:
|
|
93
93
|
server_url: str,
|
94
94
|
client_type: int,
|
95
95
|
credentials: typing.Optional[tuple[str, str]],
|
96
|
-
version: str = "0.73.
|
96
|
+
version: str = "0.73.132",
|
97
97
|
): ...
|
98
98
|
def is_closed(self) -> bool: ...
|
99
99
|
@property
|
@@ -82,7 +82,8 @@ class Function(
|
|
82
82
|
max_containers: typing.Optional[int] = None,
|
83
83
|
buffer_containers: typing.Optional[int] = None,
|
84
84
|
scaledown_window: typing.Optional[int] = None,
|
85
|
-
|
85
|
+
max_concurrent_inputs: typing.Optional[int] = None,
|
86
|
+
target_concurrent_inputs: typing.Optional[int] = None,
|
86
87
|
batch_max_size: typing.Optional[int] = None,
|
87
88
|
batch_wait_ms: typing.Optional[int] = None,
|
88
89
|
cloud: typing.Optional[str] = None,
|
@@ -198,11 +199,11 @@ class Function(
|
|
198
199
|
|
199
200
|
_call_generator_nowait: ___call_generator_nowait_spec[typing_extensions.Self]
|
200
201
|
|
201
|
-
class __remote_spec(typing_extensions.Protocol[
|
202
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
202
203
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
203
204
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
204
205
|
|
205
|
-
remote: __remote_spec[modal._functions.
|
206
|
+
remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
206
207
|
|
207
208
|
class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
|
208
209
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -217,19 +218,19 @@ class Function(
|
|
217
218
|
self, *args: modal._functions.P.args, **kwargs: modal._functions.P.kwargs
|
218
219
|
) -> modal._functions.OriginalReturnType: ...
|
219
220
|
|
220
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
221
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
221
222
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
222
223
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
223
224
|
|
224
225
|
_experimental_spawn: ___experimental_spawn_spec[
|
225
|
-
modal._functions.
|
226
|
+
modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
|
226
227
|
]
|
227
228
|
|
228
|
-
class __spawn_spec(typing_extensions.Protocol[
|
229
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
229
230
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
230
231
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
231
232
|
|
232
|
-
spawn: __spawn_spec[modal._functions.
|
233
|
+
spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
233
234
|
|
234
235
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]: ...
|
235
236
|
|
@@ -5,6 +5,7 @@ from ._partial_function import (
|
|
5
5
|
_asgi_app,
|
6
6
|
_batched,
|
7
7
|
_build,
|
8
|
+
_concurrent,
|
8
9
|
_enter,
|
9
10
|
_exit,
|
10
11
|
_fastapi_endpoint,
|
@@ -28,3 +29,4 @@ build = synchronize_api(_build, target_module=__name__)
|
|
28
29
|
enter = synchronize_api(_enter, target_module=__name__)
|
29
30
|
exit = synchronize_api(_exit, target_module=__name__)
|
30
31
|
batched = synchronize_api(_batched, target_module=__name__)
|
32
|
+
concurrent = synchronize_api(_concurrent, target_module=__name__)
|
@@ -18,6 +18,8 @@ class PartialFunction(
|
|
18
18
|
force_build: bool
|
19
19
|
cluster_size: typing.Optional[int]
|
20
20
|
build_timeout: typing.Optional[int]
|
21
|
+
max_concurrent_inputs: typing.Optional[int]
|
22
|
+
target_concurrent_inputs: typing.Optional[int]
|
21
23
|
|
22
24
|
def __init__(
|
23
25
|
self,
|
@@ -31,6 +33,8 @@ class PartialFunction(
|
|
31
33
|
cluster_size: typing.Optional[int] = None,
|
32
34
|
force_build: bool = False,
|
33
35
|
build_timeout: typing.Optional[int] = None,
|
36
|
+
max_concurrent_inputs: typing.Optional[int] = None,
|
37
|
+
target_concurrent_inputs: typing.Optional[int] = None,
|
34
38
|
): ...
|
35
39
|
def _get_raw_f(self) -> collections.abc.Callable[modal._partial_function.P, modal._partial_function.ReturnType]: ...
|
36
40
|
def _is_web_endpoint(self) -> bool: ...
|
@@ -118,3 +122,8 @@ def exit(
|
|
118
122
|
def batched(
|
119
123
|
_warn_parentheses_missing=None, *, max_batch_size: int, wait_ms: int
|
120
124
|
) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Any]], PartialFunction]: ...
|
125
|
+
def concurrent(
|
126
|
+
_warn_parentheses_missing=None, *, max_inputs: int, target_inputs: typing.Optional[int] = None
|
127
|
+
) -> collections.abc.Callable[
|
128
|
+
[typing.Union[collections.abc.Callable[..., typing.Any], PartialFunction]], PartialFunction
|
129
|
+
]: ...
|
@@ -1222,7 +1222,7 @@ message Function {
|
|
1222
1222
|
|
1223
1223
|
repeated VolumeMount volume_mounts = 33;
|
1224
1224
|
|
1225
|
-
uint32
|
1225
|
+
uint32 max_concurrent_inputs = 34;
|
1226
1226
|
|
1227
1227
|
repeated CustomDomainInfo custom_domain_info = 35;
|
1228
1228
|
|
@@ -1272,7 +1272,7 @@ message Function {
|
|
1272
1272
|
uint64 batch_linger_ms = 61; // Miliseconds to block before a response is needed
|
1273
1273
|
bool i6pn_enabled = 62;
|
1274
1274
|
bool _experimental_concurrent_cancellations = 63;
|
1275
|
-
uint32
|
1275
|
+
uint32 target_concurrent_inputs = 64;
|
1276
1276
|
|
1277
1277
|
// TODO(irfansharif): Remove, once https://github.com/modal-labs/modal/pull/15645 lands.
|
1278
1278
|
bool _experimental_task_templates_enabled = 65; // forces going through the new gpu-fallbacks integration path, even if no fallback options are specified
|