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.
Files changed (186) hide show
  1. {modal-0.73.130 → modal-0.73.132}/PKG-INFO +1 -1
  2. {modal-0.73.130 → modal-0.73.132}/modal/__init__.py +2 -0
  3. {modal-0.73.130 → modal-0.73.132}/modal/_container_entrypoint.py +2 -2
  4. {modal-0.73.130 → modal-0.73.132}/modal/_functions.py +12 -5
  5. {modal-0.73.130 → modal-0.73.132}/modal/_partial_function.py +78 -0
  6. {modal-0.73.130 → modal-0.73.132}/modal/_resolver.py +6 -1
  7. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/container_io_manager.py +9 -4
  8. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/container_io_manager.pyi +6 -0
  9. {modal-0.73.130 → modal-0.73.132}/modal/_serialization.py +78 -96
  10. modal-0.73.132/modal/_type_manager.py +229 -0
  11. {modal-0.73.130 → modal-0.73.132}/modal/_utils/function_utils.py +4 -27
  12. {modal-0.73.130 → modal-0.73.132}/modal/app.py +34 -5
  13. {modal-0.73.130 → modal-0.73.132}/modal/app.pyi +3 -2
  14. {modal-0.73.130 → modal-0.73.132}/modal/client.pyi +2 -2
  15. {modal-0.73.130 → modal-0.73.132}/modal/cls.py +6 -2
  16. {modal-0.73.130 → modal-0.73.132}/modal/functions.pyi +2 -1
  17. {modal-0.73.130 → modal-0.73.132}/modal/partial_function.py +2 -0
  18. {modal-0.73.130 → modal-0.73.132}/modal/partial_function.pyi +9 -0
  19. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/PKG-INFO +1 -1
  20. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/SOURCES.txt +1 -0
  21. {modal-0.73.130 → modal-0.73.132}/modal_proto/api.proto +17 -6
  22. {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2.py +717 -704
  23. {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2.pyi +46 -8
  24. {modal-0.73.130 → modal-0.73.132}/modal_version/_version_generated.py +1 -1
  25. {modal-0.73.130 → modal-0.73.132}/LICENSE +0 -0
  26. {modal-0.73.130 → modal-0.73.132}/README.md +0 -0
  27. {modal-0.73.130 → modal-0.73.132}/modal/__main__.py +0 -0
  28. {modal-0.73.130 → modal-0.73.132}/modal/_clustered_functions.py +0 -0
  29. {modal-0.73.130 → modal-0.73.132}/modal/_clustered_functions.pyi +0 -0
  30. {modal-0.73.130 → modal-0.73.132}/modal/_ipython.py +0 -0
  31. {modal-0.73.130 → modal-0.73.132}/modal/_location.py +0 -0
  32. {modal-0.73.130 → modal-0.73.132}/modal/_object.py +0 -0
  33. {modal-0.73.130 → modal-0.73.132}/modal/_output.py +0 -0
  34. {modal-0.73.130 → modal-0.73.132}/modal/_proxy_tunnel.py +0 -0
  35. {modal-0.73.130 → modal-0.73.132}/modal/_pty.py +0 -0
  36. {modal-0.73.130 → modal-0.73.132}/modal/_resources.py +0 -0
  37. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/__init__.py +0 -0
  38. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/asgi.py +0 -0
  39. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/execution_context.py +0 -0
  40. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/execution_context.pyi +0 -0
  41. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  42. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/telemetry.py +0 -0
  43. {modal-0.73.130 → modal-0.73.132}/modal/_runtime/user_code_imports.py +0 -0
  44. {modal-0.73.130 → modal-0.73.132}/modal/_traceback.py +0 -0
  45. {modal-0.73.130 → modal-0.73.132}/modal/_tunnel.py +0 -0
  46. {modal-0.73.130 → modal-0.73.132}/modal/_tunnel.pyi +0 -0
  47. {modal-0.73.130 → modal-0.73.132}/modal/_utils/__init__.py +0 -0
  48. {modal-0.73.130 → modal-0.73.132}/modal/_utils/app_utils.py +0 -0
  49. {modal-0.73.130 → modal-0.73.132}/modal/_utils/async_utils.py +0 -0
  50. {modal-0.73.130 → modal-0.73.132}/modal/_utils/blob_utils.py +0 -0
  51. {modal-0.73.130 → modal-0.73.132}/modal/_utils/bytes_io_segment_payload.py +0 -0
  52. {modal-0.73.130 → modal-0.73.132}/modal/_utils/deprecation.py +0 -0
  53. {modal-0.73.130 → modal-0.73.132}/modal/_utils/docker_utils.py +0 -0
  54. {modal-0.73.130 → modal-0.73.132}/modal/_utils/git_utils.py +0 -0
  55. {modal-0.73.130 → modal-0.73.132}/modal/_utils/grpc_testing.py +0 -0
  56. {modal-0.73.130 → modal-0.73.132}/modal/_utils/grpc_utils.py +0 -0
  57. {modal-0.73.130 → modal-0.73.132}/modal/_utils/hash_utils.py +0 -0
  58. {modal-0.73.130 → modal-0.73.132}/modal/_utils/http_utils.py +0 -0
  59. {modal-0.73.130 → modal-0.73.132}/modal/_utils/jwt_utils.py +0 -0
  60. {modal-0.73.130 → modal-0.73.132}/modal/_utils/logger.py +0 -0
  61. {modal-0.73.130 → modal-0.73.132}/modal/_utils/mount_utils.py +0 -0
  62. {modal-0.73.130 → modal-0.73.132}/modal/_utils/name_utils.py +0 -0
  63. {modal-0.73.130 → modal-0.73.132}/modal/_utils/package_utils.py +0 -0
  64. {modal-0.73.130 → modal-0.73.132}/modal/_utils/pattern_utils.py +0 -0
  65. {modal-0.73.130 → modal-0.73.132}/modal/_utils/rand_pb_testing.py +0 -0
  66. {modal-0.73.130 → modal-0.73.132}/modal/_utils/shell_utils.py +0 -0
  67. {modal-0.73.130 → modal-0.73.132}/modal/_vendor/__init__.py +0 -0
  68. {modal-0.73.130 → modal-0.73.132}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  69. {modal-0.73.130 → modal-0.73.132}/modal/_vendor/cloudpickle.py +0 -0
  70. {modal-0.73.130 → modal-0.73.132}/modal/_vendor/tblib.py +0 -0
  71. {modal-0.73.130 → modal-0.73.132}/modal/_watcher.py +0 -0
  72. {modal-0.73.130 → modal-0.73.132}/modal/call_graph.py +0 -0
  73. {modal-0.73.130 → modal-0.73.132}/modal/cli/__init__.py +0 -0
  74. {modal-0.73.130 → modal-0.73.132}/modal/cli/_download.py +0 -0
  75. {modal-0.73.130 → modal-0.73.132}/modal/cli/_traceback.py +0 -0
  76. {modal-0.73.130 → modal-0.73.132}/modal/cli/app.py +0 -0
  77. {modal-0.73.130 → modal-0.73.132}/modal/cli/config.py +0 -0
  78. {modal-0.73.130 → modal-0.73.132}/modal/cli/container.py +0 -0
  79. {modal-0.73.130 → modal-0.73.132}/modal/cli/dict.py +0 -0
  80. {modal-0.73.130 → modal-0.73.132}/modal/cli/entry_point.py +0 -0
  81. {modal-0.73.130 → modal-0.73.132}/modal/cli/environment.py +0 -0
  82. {modal-0.73.130 → modal-0.73.132}/modal/cli/import_refs.py +0 -0
  83. {modal-0.73.130 → modal-0.73.132}/modal/cli/launch.py +0 -0
  84. {modal-0.73.130 → modal-0.73.132}/modal/cli/network_file_system.py +0 -0
  85. {modal-0.73.130 → modal-0.73.132}/modal/cli/profile.py +0 -0
  86. {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/__init__.py +0 -0
  87. {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/run_jupyter.py +0 -0
  88. {modal-0.73.130 → modal-0.73.132}/modal/cli/programs/vscode.py +0 -0
  89. {modal-0.73.130 → modal-0.73.132}/modal/cli/queues.py +0 -0
  90. {modal-0.73.130 → modal-0.73.132}/modal/cli/run.py +0 -0
  91. {modal-0.73.130 → modal-0.73.132}/modal/cli/secret.py +0 -0
  92. {modal-0.73.130 → modal-0.73.132}/modal/cli/token.py +0 -0
  93. {modal-0.73.130 → modal-0.73.132}/modal/cli/utils.py +0 -0
  94. {modal-0.73.130 → modal-0.73.132}/modal/cli/volume.py +0 -0
  95. {modal-0.73.130 → modal-0.73.132}/modal/client.py +0 -0
  96. {modal-0.73.130 → modal-0.73.132}/modal/cloud_bucket_mount.py +0 -0
  97. {modal-0.73.130 → modal-0.73.132}/modal/cloud_bucket_mount.pyi +0 -0
  98. {modal-0.73.130 → modal-0.73.132}/modal/cls.pyi +0 -0
  99. {modal-0.73.130 → modal-0.73.132}/modal/config.py +0 -0
  100. {modal-0.73.130 → modal-0.73.132}/modal/container_process.py +0 -0
  101. {modal-0.73.130 → modal-0.73.132}/modal/container_process.pyi +0 -0
  102. {modal-0.73.130 → modal-0.73.132}/modal/dict.py +0 -0
  103. {modal-0.73.130 → modal-0.73.132}/modal/dict.pyi +0 -0
  104. {modal-0.73.130 → modal-0.73.132}/modal/environments.py +0 -0
  105. {modal-0.73.130 → modal-0.73.132}/modal/environments.pyi +0 -0
  106. {modal-0.73.130 → modal-0.73.132}/modal/exception.py +0 -0
  107. {modal-0.73.130 → modal-0.73.132}/modal/experimental.py +0 -0
  108. {modal-0.73.130 → modal-0.73.132}/modal/experimental.pyi +0 -0
  109. {modal-0.73.130 → modal-0.73.132}/modal/extensions/__init__.py +0 -0
  110. {modal-0.73.130 → modal-0.73.132}/modal/extensions/ipython.py +0 -0
  111. {modal-0.73.130 → modal-0.73.132}/modal/file_io.py +0 -0
  112. {modal-0.73.130 → modal-0.73.132}/modal/file_io.pyi +0 -0
  113. {modal-0.73.130 → modal-0.73.132}/modal/file_pattern_matcher.py +0 -0
  114. {modal-0.73.130 → modal-0.73.132}/modal/functions.py +0 -0
  115. {modal-0.73.130 → modal-0.73.132}/modal/gpu.py +0 -0
  116. {modal-0.73.130 → modal-0.73.132}/modal/image.py +0 -0
  117. {modal-0.73.130 → modal-0.73.132}/modal/image.pyi +0 -0
  118. {modal-0.73.130 → modal-0.73.132}/modal/io_streams.py +0 -0
  119. {modal-0.73.130 → modal-0.73.132}/modal/io_streams.pyi +0 -0
  120. {modal-0.73.130 → modal-0.73.132}/modal/mount.py +0 -0
  121. {modal-0.73.130 → modal-0.73.132}/modal/mount.pyi +0 -0
  122. {modal-0.73.130 → modal-0.73.132}/modal/network_file_system.py +0 -0
  123. {modal-0.73.130 → modal-0.73.132}/modal/network_file_system.pyi +0 -0
  124. {modal-0.73.130 → modal-0.73.132}/modal/object.py +0 -0
  125. {modal-0.73.130 → modal-0.73.132}/modal/object.pyi +0 -0
  126. {modal-0.73.130 → modal-0.73.132}/modal/output.py +0 -0
  127. {modal-0.73.130 → modal-0.73.132}/modal/parallel_map.py +0 -0
  128. {modal-0.73.130 → modal-0.73.132}/modal/parallel_map.pyi +0 -0
  129. {modal-0.73.130 → modal-0.73.132}/modal/proxy.py +0 -0
  130. {modal-0.73.130 → modal-0.73.132}/modal/proxy.pyi +0 -0
  131. {modal-0.73.130 → modal-0.73.132}/modal/py.typed +0 -0
  132. {modal-0.73.130 → modal-0.73.132}/modal/queue.py +0 -0
  133. {modal-0.73.130 → modal-0.73.132}/modal/queue.pyi +0 -0
  134. {modal-0.73.130 → modal-0.73.132}/modal/requirements/2023.12.312.txt +0 -0
  135. {modal-0.73.130 → modal-0.73.132}/modal/requirements/2023.12.txt +0 -0
  136. {modal-0.73.130 → modal-0.73.132}/modal/requirements/2024.04.txt +0 -0
  137. {modal-0.73.130 → modal-0.73.132}/modal/requirements/2024.10.txt +0 -0
  138. {modal-0.73.130 → modal-0.73.132}/modal/requirements/PREVIEW.txt +0 -0
  139. {modal-0.73.130 → modal-0.73.132}/modal/requirements/README.md +0 -0
  140. {modal-0.73.130 → modal-0.73.132}/modal/requirements/base-images.json +0 -0
  141. {modal-0.73.130 → modal-0.73.132}/modal/retries.py +0 -0
  142. {modal-0.73.130 → modal-0.73.132}/modal/runner.py +0 -0
  143. {modal-0.73.130 → modal-0.73.132}/modal/runner.pyi +0 -0
  144. {modal-0.73.130 → modal-0.73.132}/modal/running_app.py +0 -0
  145. {modal-0.73.130 → modal-0.73.132}/modal/sandbox.py +0 -0
  146. {modal-0.73.130 → modal-0.73.132}/modal/sandbox.pyi +0 -0
  147. {modal-0.73.130 → modal-0.73.132}/modal/schedule.py +0 -0
  148. {modal-0.73.130 → modal-0.73.132}/modal/scheduler_placement.py +0 -0
  149. {modal-0.73.130 → modal-0.73.132}/modal/secret.py +0 -0
  150. {modal-0.73.130 → modal-0.73.132}/modal/secret.pyi +0 -0
  151. {modal-0.73.130 → modal-0.73.132}/modal/serving.py +0 -0
  152. {modal-0.73.130 → modal-0.73.132}/modal/serving.pyi +0 -0
  153. {modal-0.73.130 → modal-0.73.132}/modal/snapshot.py +0 -0
  154. {modal-0.73.130 → modal-0.73.132}/modal/snapshot.pyi +0 -0
  155. {modal-0.73.130 → modal-0.73.132}/modal/stream_type.py +0 -0
  156. {modal-0.73.130 → modal-0.73.132}/modal/token_flow.py +0 -0
  157. {modal-0.73.130 → modal-0.73.132}/modal/token_flow.pyi +0 -0
  158. {modal-0.73.130 → modal-0.73.132}/modal/volume.py +0 -0
  159. {modal-0.73.130 → modal-0.73.132}/modal/volume.pyi +0 -0
  160. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/dependency_links.txt +0 -0
  161. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/entry_points.txt +0 -0
  162. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/requires.txt +0 -0
  163. {modal-0.73.130 → modal-0.73.132}/modal.egg-info/top_level.txt +0 -0
  164. {modal-0.73.130 → modal-0.73.132}/modal_docs/__init__.py +0 -0
  165. {modal-0.73.130 → modal-0.73.132}/modal_docs/gen_cli_docs.py +0 -0
  166. {modal-0.73.130 → modal-0.73.132}/modal_docs/gen_reference_docs.py +0 -0
  167. {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/__init__.py +0 -0
  168. {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/mdmd.py +0 -0
  169. {modal-0.73.130 → modal-0.73.132}/modal_docs/mdmd/signatures.py +0 -0
  170. {modal-0.73.130 → modal-0.73.132}/modal_proto/__init__.py +0 -0
  171. {modal-0.73.130 → modal-0.73.132}/modal_proto/api_grpc.py +0 -0
  172. {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2_grpc.py +0 -0
  173. {modal-0.73.130 → modal-0.73.132}/modal_proto/api_pb2_grpc.pyi +0 -0
  174. {modal-0.73.130 → modal-0.73.132}/modal_proto/modal_api_grpc.py +0 -0
  175. {modal-0.73.130 → modal-0.73.132}/modal_proto/modal_options_grpc.py +0 -0
  176. {modal-0.73.130 → modal-0.73.132}/modal_proto/options.proto +0 -0
  177. {modal-0.73.130 → modal-0.73.132}/modal_proto/options_grpc.py +0 -0
  178. {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2.py +0 -0
  179. {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2.pyi +0 -0
  180. {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2_grpc.py +0 -0
  181. {modal-0.73.130 → modal-0.73.132}/modal_proto/options_pb2_grpc.pyi +0 -0
  182. {modal-0.73.130 → modal-0.73.132}/modal_proto/py.typed +0 -0
  183. {modal-0.73.130 → modal-0.73.132}/modal_version/__init__.py +0 -0
  184. {modal-0.73.130 → modal-0.73.132}/modal_version/__main__.py +0 -0
  185. {modal-0.73.130 → modal-0.73.132}/pyproject.toml +0 -0
  186. {modal-0.73.130 → modal-0.73.132}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.130
3
+ Version: 0.73.132
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
  )
@@ -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 apply_defaults, serialize, serialize_proto_params, validate_params
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
- allow_concurrent_inputs: Optional[int] = None,
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
- 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,
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
- validate_params(kwargs_with_defaults, schema)
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
- return await cached_future
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
- 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
@@ -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 dataclasses import dataclass
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
- python_type = type(python_value)
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
- python_value: Any
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 validate_params(params: dict[str, Any], schema: typing.Sequence[api_pb2.ClassParameterSpec]):
497
- # first check that all declared values are provided
498
- for schema_param in schema:
499
- if schema_param.name not in params:
500
- # we expect all values to be present - even defaulted ones (defaults are applied on payload construction)
501
- raise InvalidError(f"Missing required parameter: {schema_param.name}")
502
- python_value = params[schema_param.name]
503
- python_type = type(python_value)
504
- param_protobuf_type = get_proto_parameter_type(python_type)
505
- if schema_param.type != param_protobuf_type:
506
- expected_python_type = PROTO_TYPE_INFO[schema_param.type].type
507
- raise TypeError(
508
- f"Parameter '{schema_param.name}' type error: expected {expected_python_type.__name__}, "
509
- f"got {python_type.__name__}"
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 = params.keys() - schema_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 present in the schema: {non_declared_fields}"
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 get_proto_parameter_type(parameter_type: type) -> "api_pb2.ParameterType.ValueType":
542
- if parameter_type not in PYTHON_TO_PROTO_TYPE:
543
- type_name = getattr(parameter_type, "__name__", repr(parameter_type))
544
- supported = ", ".join(parameter_type.__name__ for parameter_type in PYTHON_TO_PROTO_TYPE.keys())
545
- raise InvalidError(f"{type_name} is not a supported parameter type. Use one of: {supported}")
546
- return PYTHON_TO_PROTO_TYPE[parameter_type]
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