modal 0.73.130__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.130 → modal-0.73.132}/PKG-INFO +1 -1
- {modal-0.73.130 → modal-0.73.132}/modal/__init__.py +2 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_container_entrypoint.py +2 -2
- {modal-0.73.130 → modal-0.73.132}/modal/_functions.py +12 -5
- {modal-0.73.130 → modal-0.73.132}/modal/_partial_function.py +78 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_resolver.py +6 -1
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/container_io_manager.py +9 -4
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/container_io_manager.pyi +6 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_serialization.py +78 -96
- modal-0.73.132/modal/_type_manager.py +229 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/function_utils.py +4 -27
- {modal-0.73.130 → modal-0.73.132}/modal/app.py +34 -5
- {modal-0.73.130 → modal-0.73.132}/modal/app.pyi +3 -2
- {modal-0.73.130 → modal-0.73.132}/modal/client.pyi +2 -2
- {modal-0.73.130 → modal-0.73.132}/modal/cls.py +6 -2
- {modal-0.73.130 → modal-0.73.132}/modal/functions.pyi +2 -1
- {modal-0.73.130 → modal-0.73.132}/modal/partial_function.py +2 -0
- {modal-0.73.130 → modal-0.73.132}/modal/partial_function.pyi +9 -0
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/PKG-INFO +1 -1
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/SOURCES.txt +1 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api.proto +17 -6
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2.py +717 -704
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2.pyi +46 -8
- {modal-0.73.130 → modal-0.73.132}/modal_version/_version_generated.py +1 -1
- {modal-0.73.130 → modal-0.73.132}/LICENSE +0 -0
- {modal-0.73.130 → modal-0.73.132}/README.md +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/__main__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_clustered_functions.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_clustered_functions.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_ipython.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_location.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_object.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_output.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_proxy_tunnel.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_pty.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_resources.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/asgi.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/execution_context.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/execution_context.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/telemetry.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_runtime/user_code_imports.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_traceback.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_tunnel.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_tunnel.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/app_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/async_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/blob_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/deprecation.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/docker_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/git_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/grpc_testing.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/grpc_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/hash_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/http_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/jwt_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/logger.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/mount_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/name_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/package_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/pattern_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_utils/shell_utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_vendor/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_vendor/cloudpickle.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_vendor/tblib.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/_watcher.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/call_graph.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/_download.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/_traceback.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/app.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/config.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/container.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/dict.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/entry_point.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/environment.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/import_refs.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/launch.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/network_file_system.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/profile.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/vscode.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/queues.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/run.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/secret.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/token.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/utils.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cli/volume.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/client.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cloud_bucket_mount.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/cls.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/config.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/container_process.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/container_process.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/dict.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/dict.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/environments.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/environments.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/exception.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/experimental.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/experimental.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/extensions/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/extensions/ipython.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/file_io.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/file_io.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/file_pattern_matcher.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/functions.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/gpu.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/image.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/image.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/io_streams.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/io_streams.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/mount.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/mount.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/network_file_system.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/network_file_system.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/object.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/object.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/output.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/parallel_map.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/parallel_map.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/proxy.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/proxy.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/py.typed +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/queue.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/queue.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/2023.12.312.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/2023.12.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/2024.04.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/2024.10.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/PREVIEW.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/README.md +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/requirements/base-images.json +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/retries.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/runner.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/runner.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/running_app.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/sandbox.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/sandbox.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/schedule.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/scheduler_placement.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/secret.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/secret.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/serving.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/serving.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/snapshot.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/snapshot.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/stream_type.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/token_flow.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/token_flow.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/volume.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal/volume.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/dependency_links.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/entry_points.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/requires.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal.egg-info/top_level.txt +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/gen_cli_docs.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/gen_reference_docs.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/signatures.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/modal_api_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/modal_options_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options.proto +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2_grpc.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2_grpc.pyi +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_proto/py.typed +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_version/__init__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/modal_version/__main__.py +0 -0
- {modal-0.73.130 → modal-0.73.132}/pyproject.toml +0 -0
- {modal-0.73.130 → 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
|
)
|
@@ -25,7 +25,12 @@ from ._pty import get_pty_info
|
|
25
25
|
from ._resolver import Resolver
|
26
26
|
from ._resources import convert_fn_config_to_resources_config
|
27
27
|
from ._runtime.execution_context import current_input_id, is_local
|
28
|
-
from ._serialization import
|
28
|
+
from ._serialization import (
|
29
|
+
apply_defaults,
|
30
|
+
serialize,
|
31
|
+
serialize_proto_params,
|
32
|
+
validate_parameter_values,
|
33
|
+
)
|
29
34
|
from ._traceback import print_server_warnings
|
30
35
|
from ._utils.async_utils import (
|
31
36
|
TaskContext,
|
@@ -435,7 +440,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
435
440
|
max_containers: Optional[int] = None,
|
436
441
|
buffer_containers: Optional[int] = None,
|
437
442
|
scaledown_window: Optional[int] = None,
|
438
|
-
|
443
|
+
max_concurrent_inputs: Optional[int] = None,
|
444
|
+
target_concurrent_inputs: Optional[int] = None,
|
439
445
|
batch_max_size: Optional[int] = None,
|
440
446
|
batch_wait_ms: Optional[int] = None,
|
441
447
|
cloud: Optional[str] = None,
|
@@ -786,7 +792,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
786
792
|
runtime_perf_record=config.get("runtime_perf_record"),
|
787
793
|
app_name=app_name,
|
788
794
|
is_builder_function=is_builder_function,
|
789
|
-
|
795
|
+
max_concurrent_inputs=max_concurrent_inputs or 0,
|
796
|
+
target_concurrent_inputs=target_concurrent_inputs or 0,
|
790
797
|
batch_max_size=batch_max_size or 0,
|
791
798
|
batch_linger_ms=batch_wait_ms or 0,
|
792
799
|
worker_id=config.get("worker_id"),
|
@@ -975,7 +982,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
975
982
|
)
|
976
983
|
schema = parent._class_parameter_info.schema
|
977
984
|
kwargs_with_defaults = apply_defaults(kwargs, schema)
|
978
|
-
|
985
|
+
validate_parameter_values(kwargs_with_defaults, schema)
|
979
986
|
serialized_params = serialize_proto_params(kwargs_with_defaults)
|
980
987
|
can_use_parent = len(parent._class_parameter_info.schema) == 0 # no parameters
|
981
988
|
else:
|
@@ -1312,7 +1319,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1312
1319
|
order_outputs,
|
1313
1320
|
return_exceptions,
|
1314
1321
|
count_update_callback,
|
1315
|
-
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
1322
|
+
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
1316
1323
|
)
|
1317
1324
|
) as stream:
|
1318
1325
|
async for item in stream:
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
2
|
import asyncio
|
3
3
|
import contextlib
|
4
|
+
import traceback
|
4
5
|
import typing
|
5
6
|
from asyncio import Future
|
6
7
|
from collections.abc import Hashable
|
@@ -153,7 +154,11 @@ class Resolver:
|
|
153
154
|
self._deduplication_cache[deduplication_key] = cached_future
|
154
155
|
|
155
156
|
# TODO(elias): print original exception/trace rather than the Resolver-internal trace
|
156
|
-
|
157
|
+
try:
|
158
|
+
return await cached_future
|
159
|
+
except Exception:
|
160
|
+
traceback.print_exc()
|
161
|
+
raise
|
157
162
|
|
158
163
|
def objects(self) -> list["modal._object._Object"]:
|
159
164
|
unique_objects: dict[str, "modal._object._Object"] = {}
|
@@ -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
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
+
import inspect
|
2
3
|
import io
|
3
4
|
import pickle
|
4
5
|
import typing
|
5
|
-
from
|
6
|
+
from inspect import Parameter
|
6
7
|
from typing import Any
|
7
8
|
|
8
9
|
from modal._utils.async_utils import synchronizer
|
9
10
|
from modal_proto import api_pb2
|
10
11
|
|
11
12
|
from ._object import _Object
|
13
|
+
from ._type_manager import parameter_serde_registry, schema_registry
|
12
14
|
from ._vendor import cloudpickle
|
13
15
|
from .config import logger
|
14
16
|
from .exception import DeserializationError, ExecutionError, InvalidError
|
@@ -389,50 +391,6 @@ def check_valid_cls_constructor_arg(key, obj):
|
|
389
391
|
)
|
390
392
|
|
391
393
|
|
392
|
-
def assert_bytes(obj: Any):
|
393
|
-
if not isinstance(obj, bytes):
|
394
|
-
raise TypeError(f"Expected bytes, got {type(obj)}")
|
395
|
-
return obj
|
396
|
-
|
397
|
-
|
398
|
-
@dataclass
|
399
|
-
class ParamTypeInfo:
|
400
|
-
default_field: str
|
401
|
-
proto_field: str
|
402
|
-
converter: typing.Callable[[str], typing.Any]
|
403
|
-
type: type
|
404
|
-
|
405
|
-
|
406
|
-
PYTHON_TO_PROTO_TYPE: dict[type, "api_pb2.ParameterType.ValueType"] = {
|
407
|
-
# python type -> protobuf type enum
|
408
|
-
str: api_pb2.PARAM_TYPE_STRING,
|
409
|
-
int: api_pb2.PARAM_TYPE_INT,
|
410
|
-
bytes: api_pb2.PARAM_TYPE_BYTES,
|
411
|
-
}
|
412
|
-
|
413
|
-
PROTO_TYPE_INFO = {
|
414
|
-
# Protobuf type enum -> encode/decode helper metadata
|
415
|
-
api_pb2.PARAM_TYPE_STRING: ParamTypeInfo(
|
416
|
-
default_field="string_default",
|
417
|
-
proto_field="string_value",
|
418
|
-
converter=str,
|
419
|
-
type=str,
|
420
|
-
),
|
421
|
-
api_pb2.PARAM_TYPE_INT: ParamTypeInfo(
|
422
|
-
default_field="int_default",
|
423
|
-
proto_field="int_value",
|
424
|
-
converter=int,
|
425
|
-
type=int,
|
426
|
-
),
|
427
|
-
api_pb2.PARAM_TYPE_BYTES: ParamTypeInfo(
|
428
|
-
default_field="bytes_default",
|
429
|
-
proto_field="bytes_value",
|
430
|
-
converter=assert_bytes,
|
431
|
-
type=bytes,
|
432
|
-
),
|
433
|
-
}
|
434
|
-
|
435
|
-
|
436
394
|
def apply_defaults(
|
437
395
|
python_params: typing.Mapping[str, Any], schema: typing.Sequence[api_pb2.ClassParameterSpec]
|
438
396
|
) -> dict[str, Any]:
|
@@ -453,68 +411,56 @@ def apply_defaults(
|
|
453
411
|
return result
|
454
412
|
|
455
413
|
|
414
|
+
def encode_parameter_value(name: str, python_value: Any) -> api_pb2.ClassParameterValue:
|
415
|
+
"""Map to proto parameter representation using python runtime type information"""
|
416
|
+
struct = parameter_serde_registry.encode(python_value)
|
417
|
+
struct.name = name
|
418
|
+
return struct
|
419
|
+
|
420
|
+
|
456
421
|
def serialize_proto_params(python_params: dict[str, Any]) -> bytes:
|
457
422
|
proto_params: list[api_pb2.ClassParameterValue] = []
|
458
423
|
for param_name, python_value in python_params.items():
|
459
|
-
|
460
|
-
protobuf_type = get_proto_parameter_type(python_type)
|
461
|
-
type_info = PROTO_TYPE_INFO.get(protobuf_type)
|
462
|
-
proto_param = api_pb2.ClassParameterValue(
|
463
|
-
name=param_name,
|
464
|
-
type=protobuf_type,
|
465
|
-
)
|
466
|
-
try:
|
467
|
-
converted_value = type_info.converter(python_value)
|
468
|
-
except ValueError as exc:
|
469
|
-
raise ValueError(f"Invalid type for parameter {param_name}: {exc}")
|
470
|
-
setattr(proto_param, type_info.proto_field, converted_value)
|
471
|
-
proto_params.append(proto_param)
|
424
|
+
proto_params.append(encode_parameter_value(param_name, python_value))
|
472
425
|
proto_bytes = api_pb2.ClassParameterSet(parameters=proto_params).SerializeToString(deterministic=True)
|
473
426
|
return proto_bytes
|
474
427
|
|
475
428
|
|
476
429
|
def deserialize_proto_params(serialized_params: bytes) -> dict[str, Any]:
|
477
|
-
proto_struct = api_pb2.ClassParameterSet()
|
478
|
-
proto_struct.ParseFromString(serialized_params)
|
430
|
+
proto_struct = api_pb2.ClassParameterSet.FromString(serialized_params)
|
479
431
|
python_params = {}
|
480
432
|
for param in proto_struct.parameters:
|
481
|
-
|
482
|
-
if param.type == api_pb2.PARAM_TYPE_STRING:
|
483
|
-
python_value = param.string_value
|
484
|
-
elif param.type == api_pb2.PARAM_TYPE_INT:
|
485
|
-
python_value = param.int_value
|
486
|
-
elif param.type == api_pb2.PARAM_TYPE_BYTES:
|
487
|
-
python_value = param.bytes_value
|
488
|
-
else:
|
489
|
-
raise NotImplementedError(f"Unimplemented parameter type: {param.type}.")
|
490
|
-
|
491
|
-
python_params[param.name] = python_value
|
433
|
+
python_params[param.name] = parameter_serde_registry.decode(param)
|
492
434
|
|
493
435
|
return python_params
|
494
436
|
|
495
437
|
|
496
|
-
def
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
if
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
438
|
+
def validate_parameter_values(payload: dict[str, Any], schema: typing.Sequence[api_pb2.ClassParameterSpec]):
|
439
|
+
"""Ensure parameter payload conforms to the schema of a class
|
440
|
+
|
441
|
+
Checks that:
|
442
|
+
* All fields are specified (defaults are expected to already be applied on the payload)
|
443
|
+
* No extra fields are specified
|
444
|
+
* The type of each field is correct
|
445
|
+
"""
|
446
|
+
for param_spec in schema:
|
447
|
+
if param_spec.name not in payload:
|
448
|
+
raise InvalidError(f"Missing required parameter: {param_spec.name}")
|
449
|
+
python_value = payload[param_spec.name]
|
450
|
+
if param_spec.HasField("full_type") and param_spec.full_type.base_type:
|
451
|
+
type_enum_value = param_spec.full_type.base_type
|
452
|
+
else:
|
453
|
+
type_enum_value = param_spec.type # backwards compatibility pre-full_type
|
454
|
+
|
455
|
+
parameter_serde_registry.validate_value_for_enum_type(type_enum_value, python_value)
|
511
456
|
|
512
457
|
schema_fields = {p.name for p in schema}
|
513
458
|
# then check that no extra values are provided
|
514
|
-
non_declared_fields =
|
459
|
+
non_declared_fields = payload.keys() - schema_fields
|
515
460
|
if non_declared_fields:
|
516
461
|
raise InvalidError(
|
517
|
-
f"The following parameter names were provided but are not
|
462
|
+
f"The following parameter names were provided but are not defined class modal.parameters for the class: "
|
463
|
+
f"{', '.join(non_declared_fields)}"
|
518
464
|
)
|
519
465
|
|
520
466
|
|
@@ -528,8 +474,6 @@ def deserialize_params(serialized_params: bytes, function_def: api_pb2.Function,
|
|
528
474
|
elif function_def.class_parameter_info.format == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO:
|
529
475
|
param_args = () # we use kwargs only for our implicit constructors
|
530
476
|
param_kwargs = deserialize_proto_params(serialized_params)
|
531
|
-
# TODO: We can probably remove the validation below since we do validation in the caller?
|
532
|
-
validate_params(param_kwargs, list(function_def.class_parameter_info.schema))
|
533
477
|
else:
|
534
478
|
raise ExecutionError(
|
535
479
|
f"Unknown class parameter serialization format: {function_def.class_parameter_info.format}"
|
@@ -538,9 +482,47 @@ def deserialize_params(serialized_params: bytes, function_def: api_pb2.Function,
|
|
538
482
|
return param_args, param_kwargs
|
539
483
|
|
540
484
|
|
541
|
-
def
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
485
|
+
def _signature_parameter_to_spec(
|
486
|
+
python_signature_parameter: inspect.Parameter, include_legacy_parameter_fields: bool = False
|
487
|
+
) -> api_pb2.ClassParameterSpec:
|
488
|
+
"""Returns proto representation of Parameter as returned by inspect.signature()
|
489
|
+
|
490
|
+
Setting include_legacy_parameter_fields makes the output backwards compatible with
|
491
|
+
pre v0.74 clients looking at class parameter specifications, and should not be used
|
492
|
+
when registering *function* schemas.
|
493
|
+
"""
|
494
|
+
declared_type = python_signature_parameter.annotation
|
495
|
+
full_proto_type = schema_registry.get_proto_generic_type(declared_type)
|
496
|
+
has_default = python_signature_parameter.default is not Parameter.empty
|
497
|
+
|
498
|
+
field_spec = api_pb2.ClassParameterSpec(
|
499
|
+
name=python_signature_parameter.name,
|
500
|
+
full_type=full_proto_type,
|
501
|
+
has_default=has_default,
|
502
|
+
)
|
503
|
+
if include_legacy_parameter_fields:
|
504
|
+
# add the .{type}_default and `.type` values as required by legacy clients
|
505
|
+
# looking at class parameter specs
|
506
|
+
if full_proto_type.base_type == api_pb2.PARAM_TYPE_INT:
|
507
|
+
if has_default:
|
508
|
+
field_spec.int_default = python_signature_parameter.default
|
509
|
+
field_spec.type = api_pb2.PARAM_TYPE_INT
|
510
|
+
elif full_proto_type.base_type == api_pb2.PARAM_TYPE_STRING:
|
511
|
+
if has_default:
|
512
|
+
field_spec.string_default = python_signature_parameter.default
|
513
|
+
field_spec.type = api_pb2.PARAM_TYPE_STRING
|
514
|
+
elif full_proto_type.base_type == api_pb2.PARAM_TYPE_BYTES:
|
515
|
+
if has_default:
|
516
|
+
field_spec.bytes_default = python_signature_parameter.default
|
517
|
+
field_spec.type = api_pb2.PARAM_TYPE_BYTES
|
518
|
+
|
519
|
+
return field_spec
|
520
|
+
|
521
|
+
|
522
|
+
def signature_to_parameter_specs(signature: inspect.Signature) -> list[api_pb2.ClassParameterSpec]:
|
523
|
+
# only used for modal.parameter() specs, uses backwards compatible fields and types
|
524
|
+
modal_parameters: list[api_pb2.ClassParameterSpec] = []
|
525
|
+
for param in signature.parameters.values():
|
526
|
+
field_spec = _signature_parameter_to_spec(param, include_legacy_parameter_fields=True)
|
527
|
+
modal_parameters.append(field_spec)
|
528
|
+
return modal_parameters
|