modal 0.73.131__tar.gz → 0.73.133__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.
Files changed (186) hide show
  1. {modal-0.73.131 → modal-0.73.133}/PKG-INFO +1 -1
  2. {modal-0.73.131 → modal-0.73.133}/modal/__init__.py +2 -0
  3. {modal-0.73.131 → modal-0.73.133}/modal/_container_entrypoint.py +2 -2
  4. {modal-0.73.131 → modal-0.73.133}/modal/_functions.py +4 -2
  5. {modal-0.73.131 → modal-0.73.133}/modal/_partial_function.py +78 -0
  6. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/container_io_manager.py +9 -4
  7. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/container_io_manager.pyi +6 -0
  8. {modal-0.73.131 → modal-0.73.133}/modal/app.py +34 -5
  9. {modal-0.73.131 → modal-0.73.133}/modal/app.pyi +3 -2
  10. {modal-0.73.131 → modal-0.73.133}/modal/client.pyi +2 -2
  11. {modal-0.73.131 → modal-0.73.133}/modal/functions.pyi +8 -7
  12. {modal-0.73.131 → modal-0.73.133}/modal/partial_function.py +2 -0
  13. {modal-0.73.131 → modal-0.73.133}/modal/partial_function.pyi +9 -0
  14. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/PKG-INFO +1 -1
  15. {modal-0.73.131 → modal-0.73.133}/modal_proto/api.proto +2 -2
  16. {modal-0.73.131 → modal-0.73.133}/modal_proto/api_pb2.py +1 -1
  17. {modal-0.73.131 → modal-0.73.133}/modal_version/_version_generated.py +1 -1
  18. {modal-0.73.131 → modal-0.73.133}/LICENSE +0 -0
  19. {modal-0.73.131 → modal-0.73.133}/README.md +0 -0
  20. {modal-0.73.131 → modal-0.73.133}/modal/__main__.py +0 -0
  21. {modal-0.73.131 → modal-0.73.133}/modal/_clustered_functions.py +0 -0
  22. {modal-0.73.131 → modal-0.73.133}/modal/_clustered_functions.pyi +0 -0
  23. {modal-0.73.131 → modal-0.73.133}/modal/_ipython.py +0 -0
  24. {modal-0.73.131 → modal-0.73.133}/modal/_location.py +0 -0
  25. {modal-0.73.131 → modal-0.73.133}/modal/_object.py +0 -0
  26. {modal-0.73.131 → modal-0.73.133}/modal/_output.py +0 -0
  27. {modal-0.73.131 → modal-0.73.133}/modal/_proxy_tunnel.py +0 -0
  28. {modal-0.73.131 → modal-0.73.133}/modal/_pty.py +0 -0
  29. {modal-0.73.131 → modal-0.73.133}/modal/_resolver.py +0 -0
  30. {modal-0.73.131 → modal-0.73.133}/modal/_resources.py +0 -0
  31. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/__init__.py +0 -0
  32. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/asgi.py +0 -0
  33. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/execution_context.py +0 -0
  34. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/execution_context.pyi +0 -0
  35. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  36. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/telemetry.py +0 -0
  37. {modal-0.73.131 → modal-0.73.133}/modal/_runtime/user_code_imports.py +0 -0
  38. {modal-0.73.131 → modal-0.73.133}/modal/_serialization.py +0 -0
  39. {modal-0.73.131 → modal-0.73.133}/modal/_traceback.py +0 -0
  40. {modal-0.73.131 → modal-0.73.133}/modal/_tunnel.py +0 -0
  41. {modal-0.73.131 → modal-0.73.133}/modal/_tunnel.pyi +0 -0
  42. {modal-0.73.131 → modal-0.73.133}/modal/_type_manager.py +0 -0
  43. {modal-0.73.131 → modal-0.73.133}/modal/_utils/__init__.py +0 -0
  44. {modal-0.73.131 → modal-0.73.133}/modal/_utils/app_utils.py +0 -0
  45. {modal-0.73.131 → modal-0.73.133}/modal/_utils/async_utils.py +0 -0
  46. {modal-0.73.131 → modal-0.73.133}/modal/_utils/blob_utils.py +0 -0
  47. {modal-0.73.131 → modal-0.73.133}/modal/_utils/bytes_io_segment_payload.py +0 -0
  48. {modal-0.73.131 → modal-0.73.133}/modal/_utils/deprecation.py +0 -0
  49. {modal-0.73.131 → modal-0.73.133}/modal/_utils/docker_utils.py +0 -0
  50. {modal-0.73.131 → modal-0.73.133}/modal/_utils/function_utils.py +0 -0
  51. {modal-0.73.131 → modal-0.73.133}/modal/_utils/git_utils.py +0 -0
  52. {modal-0.73.131 → modal-0.73.133}/modal/_utils/grpc_testing.py +0 -0
  53. {modal-0.73.131 → modal-0.73.133}/modal/_utils/grpc_utils.py +0 -0
  54. {modal-0.73.131 → modal-0.73.133}/modal/_utils/hash_utils.py +0 -0
  55. {modal-0.73.131 → modal-0.73.133}/modal/_utils/http_utils.py +0 -0
  56. {modal-0.73.131 → modal-0.73.133}/modal/_utils/jwt_utils.py +0 -0
  57. {modal-0.73.131 → modal-0.73.133}/modal/_utils/logger.py +0 -0
  58. {modal-0.73.131 → modal-0.73.133}/modal/_utils/mount_utils.py +0 -0
  59. {modal-0.73.131 → modal-0.73.133}/modal/_utils/name_utils.py +0 -0
  60. {modal-0.73.131 → modal-0.73.133}/modal/_utils/package_utils.py +0 -0
  61. {modal-0.73.131 → modal-0.73.133}/modal/_utils/pattern_utils.py +0 -0
  62. {modal-0.73.131 → modal-0.73.133}/modal/_utils/rand_pb_testing.py +0 -0
  63. {modal-0.73.131 → modal-0.73.133}/modal/_utils/shell_utils.py +0 -0
  64. {modal-0.73.131 → modal-0.73.133}/modal/_vendor/__init__.py +0 -0
  65. {modal-0.73.131 → modal-0.73.133}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  66. {modal-0.73.131 → modal-0.73.133}/modal/_vendor/cloudpickle.py +0 -0
  67. {modal-0.73.131 → modal-0.73.133}/modal/_vendor/tblib.py +0 -0
  68. {modal-0.73.131 → modal-0.73.133}/modal/_watcher.py +0 -0
  69. {modal-0.73.131 → modal-0.73.133}/modal/call_graph.py +0 -0
  70. {modal-0.73.131 → modal-0.73.133}/modal/cli/__init__.py +0 -0
  71. {modal-0.73.131 → modal-0.73.133}/modal/cli/_download.py +0 -0
  72. {modal-0.73.131 → modal-0.73.133}/modal/cli/_traceback.py +0 -0
  73. {modal-0.73.131 → modal-0.73.133}/modal/cli/app.py +0 -0
  74. {modal-0.73.131 → modal-0.73.133}/modal/cli/config.py +0 -0
  75. {modal-0.73.131 → modal-0.73.133}/modal/cli/container.py +0 -0
  76. {modal-0.73.131 → modal-0.73.133}/modal/cli/dict.py +0 -0
  77. {modal-0.73.131 → modal-0.73.133}/modal/cli/entry_point.py +0 -0
  78. {modal-0.73.131 → modal-0.73.133}/modal/cli/environment.py +0 -0
  79. {modal-0.73.131 → modal-0.73.133}/modal/cli/import_refs.py +0 -0
  80. {modal-0.73.131 → modal-0.73.133}/modal/cli/launch.py +0 -0
  81. {modal-0.73.131 → modal-0.73.133}/modal/cli/network_file_system.py +0 -0
  82. {modal-0.73.131 → modal-0.73.133}/modal/cli/profile.py +0 -0
  83. {modal-0.73.131 → modal-0.73.133}/modal/cli/programs/__init__.py +0 -0
  84. {modal-0.73.131 → modal-0.73.133}/modal/cli/programs/run_jupyter.py +0 -0
  85. {modal-0.73.131 → modal-0.73.133}/modal/cli/programs/vscode.py +0 -0
  86. {modal-0.73.131 → modal-0.73.133}/modal/cli/queues.py +0 -0
  87. {modal-0.73.131 → modal-0.73.133}/modal/cli/run.py +0 -0
  88. {modal-0.73.131 → modal-0.73.133}/modal/cli/secret.py +0 -0
  89. {modal-0.73.131 → modal-0.73.133}/modal/cli/token.py +0 -0
  90. {modal-0.73.131 → modal-0.73.133}/modal/cli/utils.py +0 -0
  91. {modal-0.73.131 → modal-0.73.133}/modal/cli/volume.py +0 -0
  92. {modal-0.73.131 → modal-0.73.133}/modal/client.py +0 -0
  93. {modal-0.73.131 → modal-0.73.133}/modal/cloud_bucket_mount.py +0 -0
  94. {modal-0.73.131 → modal-0.73.133}/modal/cloud_bucket_mount.pyi +0 -0
  95. {modal-0.73.131 → modal-0.73.133}/modal/cls.py +0 -0
  96. {modal-0.73.131 → modal-0.73.133}/modal/cls.pyi +0 -0
  97. {modal-0.73.131 → modal-0.73.133}/modal/config.py +0 -0
  98. {modal-0.73.131 → modal-0.73.133}/modal/container_process.py +0 -0
  99. {modal-0.73.131 → modal-0.73.133}/modal/container_process.pyi +0 -0
  100. {modal-0.73.131 → modal-0.73.133}/modal/dict.py +0 -0
  101. {modal-0.73.131 → modal-0.73.133}/modal/dict.pyi +0 -0
  102. {modal-0.73.131 → modal-0.73.133}/modal/environments.py +0 -0
  103. {modal-0.73.131 → modal-0.73.133}/modal/environments.pyi +0 -0
  104. {modal-0.73.131 → modal-0.73.133}/modal/exception.py +0 -0
  105. {modal-0.73.131 → modal-0.73.133}/modal/experimental.py +0 -0
  106. {modal-0.73.131 → modal-0.73.133}/modal/experimental.pyi +0 -0
  107. {modal-0.73.131 → modal-0.73.133}/modal/extensions/__init__.py +0 -0
  108. {modal-0.73.131 → modal-0.73.133}/modal/extensions/ipython.py +0 -0
  109. {modal-0.73.131 → modal-0.73.133}/modal/file_io.py +0 -0
  110. {modal-0.73.131 → modal-0.73.133}/modal/file_io.pyi +0 -0
  111. {modal-0.73.131 → modal-0.73.133}/modal/file_pattern_matcher.py +0 -0
  112. {modal-0.73.131 → modal-0.73.133}/modal/functions.py +0 -0
  113. {modal-0.73.131 → modal-0.73.133}/modal/gpu.py +0 -0
  114. {modal-0.73.131 → modal-0.73.133}/modal/image.py +0 -0
  115. {modal-0.73.131 → modal-0.73.133}/modal/image.pyi +0 -0
  116. {modal-0.73.131 → modal-0.73.133}/modal/io_streams.py +0 -0
  117. {modal-0.73.131 → modal-0.73.133}/modal/io_streams.pyi +0 -0
  118. {modal-0.73.131 → modal-0.73.133}/modal/mount.py +0 -0
  119. {modal-0.73.131 → modal-0.73.133}/modal/mount.pyi +0 -0
  120. {modal-0.73.131 → modal-0.73.133}/modal/network_file_system.py +0 -0
  121. {modal-0.73.131 → modal-0.73.133}/modal/network_file_system.pyi +0 -0
  122. {modal-0.73.131 → modal-0.73.133}/modal/object.py +0 -0
  123. {modal-0.73.131 → modal-0.73.133}/modal/object.pyi +0 -0
  124. {modal-0.73.131 → modal-0.73.133}/modal/output.py +0 -0
  125. {modal-0.73.131 → modal-0.73.133}/modal/parallel_map.py +0 -0
  126. {modal-0.73.131 → modal-0.73.133}/modal/parallel_map.pyi +0 -0
  127. {modal-0.73.131 → modal-0.73.133}/modal/proxy.py +0 -0
  128. {modal-0.73.131 → modal-0.73.133}/modal/proxy.pyi +0 -0
  129. {modal-0.73.131 → modal-0.73.133}/modal/py.typed +0 -0
  130. {modal-0.73.131 → modal-0.73.133}/modal/queue.py +0 -0
  131. {modal-0.73.131 → modal-0.73.133}/modal/queue.pyi +0 -0
  132. {modal-0.73.131 → modal-0.73.133}/modal/requirements/2023.12.312.txt +0 -0
  133. {modal-0.73.131 → modal-0.73.133}/modal/requirements/2023.12.txt +0 -0
  134. {modal-0.73.131 → modal-0.73.133}/modal/requirements/2024.04.txt +0 -0
  135. {modal-0.73.131 → modal-0.73.133}/modal/requirements/2024.10.txt +0 -0
  136. {modal-0.73.131 → modal-0.73.133}/modal/requirements/PREVIEW.txt +0 -0
  137. {modal-0.73.131 → modal-0.73.133}/modal/requirements/README.md +0 -0
  138. {modal-0.73.131 → modal-0.73.133}/modal/requirements/base-images.json +0 -0
  139. {modal-0.73.131 → modal-0.73.133}/modal/retries.py +0 -0
  140. {modal-0.73.131 → modal-0.73.133}/modal/runner.py +0 -0
  141. {modal-0.73.131 → modal-0.73.133}/modal/runner.pyi +0 -0
  142. {modal-0.73.131 → modal-0.73.133}/modal/running_app.py +0 -0
  143. {modal-0.73.131 → modal-0.73.133}/modal/sandbox.py +0 -0
  144. {modal-0.73.131 → modal-0.73.133}/modal/sandbox.pyi +0 -0
  145. {modal-0.73.131 → modal-0.73.133}/modal/schedule.py +0 -0
  146. {modal-0.73.131 → modal-0.73.133}/modal/scheduler_placement.py +0 -0
  147. {modal-0.73.131 → modal-0.73.133}/modal/secret.py +0 -0
  148. {modal-0.73.131 → modal-0.73.133}/modal/secret.pyi +0 -0
  149. {modal-0.73.131 → modal-0.73.133}/modal/serving.py +0 -0
  150. {modal-0.73.131 → modal-0.73.133}/modal/serving.pyi +0 -0
  151. {modal-0.73.131 → modal-0.73.133}/modal/snapshot.py +0 -0
  152. {modal-0.73.131 → modal-0.73.133}/modal/snapshot.pyi +0 -0
  153. {modal-0.73.131 → modal-0.73.133}/modal/stream_type.py +0 -0
  154. {modal-0.73.131 → modal-0.73.133}/modal/token_flow.py +0 -0
  155. {modal-0.73.131 → modal-0.73.133}/modal/token_flow.pyi +0 -0
  156. {modal-0.73.131 → modal-0.73.133}/modal/volume.py +0 -0
  157. {modal-0.73.131 → modal-0.73.133}/modal/volume.pyi +0 -0
  158. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/SOURCES.txt +0 -0
  159. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/dependency_links.txt +0 -0
  160. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/entry_points.txt +0 -0
  161. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/requires.txt +0 -0
  162. {modal-0.73.131 → modal-0.73.133}/modal.egg-info/top_level.txt +0 -0
  163. {modal-0.73.131 → modal-0.73.133}/modal_docs/__init__.py +0 -0
  164. {modal-0.73.131 → modal-0.73.133}/modal_docs/gen_cli_docs.py +0 -0
  165. {modal-0.73.131 → modal-0.73.133}/modal_docs/gen_reference_docs.py +0 -0
  166. {modal-0.73.131 → modal-0.73.133}/modal_docs/mdmd/__init__.py +0 -0
  167. {modal-0.73.131 → modal-0.73.133}/modal_docs/mdmd/mdmd.py +0 -0
  168. {modal-0.73.131 → modal-0.73.133}/modal_docs/mdmd/signatures.py +0 -0
  169. {modal-0.73.131 → modal-0.73.133}/modal_proto/__init__.py +0 -0
  170. {modal-0.73.131 → modal-0.73.133}/modal_proto/api_grpc.py +0 -0
  171. {modal-0.73.131 → modal-0.73.133}/modal_proto/api_pb2.pyi +6 -6
  172. {modal-0.73.131 → modal-0.73.133}/modal_proto/api_pb2_grpc.py +0 -0
  173. {modal-0.73.131 → modal-0.73.133}/modal_proto/api_pb2_grpc.pyi +0 -0
  174. {modal-0.73.131 → modal-0.73.133}/modal_proto/modal_api_grpc.py +0 -0
  175. {modal-0.73.131 → modal-0.73.133}/modal_proto/modal_options_grpc.py +0 -0
  176. {modal-0.73.131 → modal-0.73.133}/modal_proto/options.proto +0 -0
  177. {modal-0.73.131 → modal-0.73.133}/modal_proto/options_grpc.py +0 -0
  178. {modal-0.73.131 → modal-0.73.133}/modal_proto/options_pb2.py +0 -0
  179. {modal-0.73.131 → modal-0.73.133}/modal_proto/options_pb2.pyi +0 -0
  180. {modal-0.73.131 → modal-0.73.133}/modal_proto/options_pb2_grpc.py +0 -0
  181. {modal-0.73.131 → modal-0.73.133}/modal_proto/options_pb2_grpc.pyi +0 -0
  182. {modal-0.73.131 → modal-0.73.133}/modal_proto/py.typed +0 -0
  183. {modal-0.73.131 → modal-0.73.133}/modal_version/__init__.py +0 -0
  184. {modal-0.73.131 → modal-0.73.133}/modal_version/__main__.py +0 -0
  185. {modal-0.73.131 → modal-0.73.133}/pyproject.toml +0 -0
  186. {modal-0.73.131 → modal-0.73.133}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.131
3
+ Version: 0.73.133
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.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.target_concurrency > 1:
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 allow_concurrent_inputs > 1.\n"
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
- allow_concurrent_inputs: Optional[int] = None,
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
- target_concurrent_inputs=allow_concurrent_inputs or 0,
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
- target_concurrency = container_args.function_def.target_concurrent_inputs or 1
303
- max_concurrency = container_args.function_def.max_concurrent_inputs or target_concurrency
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
- raw_f = f
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
- allow_concurrent_inputs=allow_concurrent_inputs,
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(user_cls: CLS_T) -> CLS_T:
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
- allow_concurrent_inputs=allow_concurrent_inputs,
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.131",
34
+ version: str = "0.73.133",
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.131",
96
+ version: str = "0.73.133",
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
- allow_concurrent_inputs: typing.Optional[int] = None,
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[ReturnType_INNER, P_INNER, SUPERSELF]):
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.ReturnType, modal._functions.P, typing_extensions.Self]
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[ReturnType_INNER, P_INNER, SUPERSELF]):
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.ReturnType, modal._functions.P, typing_extensions.Self
226
+ modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
226
227
  ]
227
228
 
228
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
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.ReturnType, modal._functions.P, typing_extensions.Self]
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
+ ]: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.131
3
+ Version: 0.73.133
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1222,7 +1222,7 @@ message Function {
1222
1222
 
1223
1223
  repeated VolumeMount volume_mounts = 33;
1224
1224
 
1225
- uint32 target_concurrent_inputs = 34;
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 max_concurrent_inputs = 64;
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