modal 1.1.2.dev6__tar.gz → 1.1.2.dev8__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 (188) hide show
  1. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/PKG-INFO +1 -1
  2. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_functions.py +19 -0
  3. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/client.pyi +2 -2
  4. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/functions.pyi +33 -0
  5. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/parallel_map.py +333 -101
  6. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/parallel_map.pyi +105 -0
  7. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/PKG-INFO +1 -1
  8. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_version/__init__.py +1 -1
  9. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/LICENSE +0 -0
  10. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/README.md +0 -0
  11. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/__init__.py +0 -0
  12. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/__main__.py +0 -0
  13. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_clustered_functions.py +0 -0
  14. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_clustered_functions.pyi +0 -0
  15. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_container_entrypoint.py +0 -0
  16. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_ipython.py +0 -0
  17. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_location.py +0 -0
  18. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_object.py +0 -0
  19. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_output.py +0 -0
  20. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_partial_function.py +0 -0
  21. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_pty.py +0 -0
  22. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_resolver.py +0 -0
  23. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_resources.py +0 -0
  24. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/__init__.py +0 -0
  25. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/asgi.py +0 -0
  26. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/container_io_manager.py +0 -0
  27. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/container_io_manager.pyi +0 -0
  28. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/execution_context.py +0 -0
  29. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/execution_context.pyi +0 -0
  30. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  31. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/telemetry.py +0 -0
  32. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_runtime/user_code_imports.py +0 -0
  33. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_serialization.py +0 -0
  34. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_traceback.py +0 -0
  35. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_tunnel.py +0 -0
  36. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_tunnel.pyi +0 -0
  37. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_type_manager.py +0 -0
  38. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/__init__.py +0 -0
  39. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/app_utils.py +0 -0
  40. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/async_utils.py +0 -0
  41. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/auth_token_manager.py +0 -0
  42. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/blob_utils.py +0 -0
  43. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/bytes_io_segment_payload.py +0 -0
  44. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/deprecation.py +0 -0
  45. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/docker_utils.py +0 -0
  46. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/function_utils.py +0 -0
  47. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/git_utils.py +0 -0
  48. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/grpc_testing.py +0 -0
  49. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/grpc_utils.py +0 -0
  50. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/hash_utils.py +0 -0
  51. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/http_utils.py +0 -0
  52. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/jwt_utils.py +0 -0
  53. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/logger.py +0 -0
  54. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/mount_utils.py +0 -0
  55. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/name_utils.py +0 -0
  56. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/package_utils.py +0 -0
  57. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/pattern_utils.py +0 -0
  58. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/rand_pb_testing.py +0 -0
  59. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/shell_utils.py +0 -0
  60. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_utils/time_utils.py +0 -0
  61. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_vendor/__init__.py +0 -0
  62. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  63. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_vendor/cloudpickle.py +0 -0
  64. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_vendor/tblib.py +0 -0
  65. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/_watcher.py +0 -0
  66. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/app.py +0 -0
  67. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/app.pyi +0 -0
  68. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/2023.12.312.txt +0 -0
  69. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/2023.12.txt +0 -0
  70. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/2024.04.txt +0 -0
  71. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/2024.10.txt +0 -0
  72. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/2025.06.txt +0 -0
  73. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/PREVIEW.txt +0 -0
  74. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/README.md +0 -0
  75. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/builder/base-images.json +0 -0
  76. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/call_graph.py +0 -0
  77. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/__init__.py +0 -0
  78. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/_download.py +0 -0
  79. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/_traceback.py +0 -0
  80. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/app.py +0 -0
  81. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/cluster.py +0 -0
  82. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/config.py +0 -0
  83. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/container.py +0 -0
  84. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/dict.py +0 -0
  85. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/entry_point.py +0 -0
  86. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/environment.py +0 -0
  87. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/import_refs.py +0 -0
  88. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/launch.py +0 -0
  89. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/network_file_system.py +0 -0
  90. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/profile.py +0 -0
  91. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/programs/__init__.py +0 -0
  92. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/programs/run_jupyter.py +0 -0
  93. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/programs/vscode.py +0 -0
  94. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/queues.py +0 -0
  95. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/run.py +0 -0
  96. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/secret.py +0 -0
  97. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/token.py +0 -0
  98. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/utils.py +0 -0
  99. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cli/volume.py +0 -0
  100. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/client.py +0 -0
  101. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cloud_bucket_mount.py +0 -0
  102. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cloud_bucket_mount.pyi +0 -0
  103. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cls.py +0 -0
  104. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/cls.pyi +0 -0
  105. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/config.py +0 -0
  106. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/container_process.py +0 -0
  107. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/container_process.pyi +0 -0
  108. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/dict.py +0 -0
  109. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/dict.pyi +0 -0
  110. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/environments.py +0 -0
  111. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/environments.pyi +0 -0
  112. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/exception.py +0 -0
  113. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/experimental/__init__.py +0 -0
  114. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/experimental/flash.py +0 -0
  115. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/experimental/flash.pyi +0 -0
  116. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/experimental/ipython.py +0 -0
  117. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/file_io.py +0 -0
  118. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/file_io.pyi +0 -0
  119. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/file_pattern_matcher.py +0 -0
  120. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/functions.py +0 -0
  121. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/gpu.py +0 -0
  122. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/image.py +0 -0
  123. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/image.pyi +0 -0
  124. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/io_streams.py +0 -0
  125. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/io_streams.pyi +0 -0
  126. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/mount.py +0 -0
  127. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/mount.pyi +0 -0
  128. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/network_file_system.py +0 -0
  129. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/network_file_system.pyi +0 -0
  130. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/object.py +0 -0
  131. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/object.pyi +0 -0
  132. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/output.py +0 -0
  133. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/partial_function.py +0 -0
  134. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/partial_function.pyi +0 -0
  135. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/proxy.py +0 -0
  136. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/proxy.pyi +0 -0
  137. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/py.typed +0 -0
  138. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/queue.py +0 -0
  139. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/queue.pyi +0 -0
  140. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/retries.py +0 -0
  141. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/runner.py +0 -0
  142. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/runner.pyi +0 -0
  143. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/running_app.py +0 -0
  144. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/sandbox.py +0 -0
  145. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/sandbox.pyi +0 -0
  146. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/schedule.py +0 -0
  147. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/scheduler_placement.py +0 -0
  148. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/secret.py +0 -0
  149. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/secret.pyi +0 -0
  150. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/serving.py +0 -0
  151. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/serving.pyi +0 -0
  152. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/snapshot.py +0 -0
  153. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/snapshot.pyi +0 -0
  154. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/stream_type.py +0 -0
  155. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/token_flow.py +0 -0
  156. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/token_flow.pyi +0 -0
  157. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/volume.py +0 -0
  158. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal/volume.pyi +0 -0
  159. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/SOURCES.txt +0 -0
  160. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/dependency_links.txt +0 -0
  161. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/entry_points.txt +0 -0
  162. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/requires.txt +0 -0
  163. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal.egg-info/top_level.txt +0 -0
  164. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/__init__.py +0 -0
  165. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/gen_cli_docs.py +0 -0
  166. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/gen_reference_docs.py +0 -0
  167. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/mdmd/__init__.py +0 -0
  168. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/mdmd/mdmd.py +0 -0
  169. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_docs/mdmd/signatures.py +0 -0
  170. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/__init__.py +0 -0
  171. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api.proto +0 -0
  172. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api_grpc.py +0 -0
  173. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api_pb2.py +0 -0
  174. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api_pb2.pyi +0 -0
  175. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api_pb2_grpc.py +0 -0
  176. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/api_pb2_grpc.pyi +0 -0
  177. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/modal_api_grpc.py +0 -0
  178. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/modal_options_grpc.py +0 -0
  179. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options.proto +0 -0
  180. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options_grpc.py +0 -0
  181. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options_pb2.py +0 -0
  182. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options_pb2.pyi +0 -0
  183. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options_pb2_grpc.py +0 -0
  184. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/options_pb2_grpc.pyi +0 -0
  185. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_proto/py.typed +0 -0
  186. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/modal_version/__main__.py +0 -0
  187. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/pyproject.toml +0 -0
  188. {modal-1.1.2.dev6 → modal-1.1.2.dev8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.2.dev6
3
+ Version: 1.1.2.dev8
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -71,6 +71,8 @@ from .mount import _get_client_mount, _Mount
71
71
  from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
72
72
  from .output import _get_output_manager
73
73
  from .parallel_map import (
74
+ _experimental_spawn_map_async,
75
+ _experimental_spawn_map_sync,
74
76
  _for_each_async,
75
77
  _for_each_sync,
76
78
  _map_async,
@@ -78,6 +80,7 @@ from .parallel_map import (
78
80
  _map_invocation_inputplane,
79
81
  _map_sync,
80
82
  _spawn_map_async,
83
+ _spawn_map_invocation,
81
84
  _spawn_map_sync,
82
85
  _starmap_async,
83
86
  _starmap_sync,
@@ -1543,6 +1546,21 @@ Use the `Function.get_web_url()` method instead.
1543
1546
  async for item in stream:
1544
1547
  yield item
1545
1548
 
1549
+ @live_method
1550
+ async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
1551
+ self._check_no_web_url("spawn_map")
1552
+ if self._is_generator:
1553
+ raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
1554
+
1555
+ assert self._function_name
1556
+ function_call_id = await _spawn_map_invocation(
1557
+ self,
1558
+ input_queue,
1559
+ self.client,
1560
+ )
1561
+ fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
1562
+ return fc
1563
+
1546
1564
  async def _call_function(self, args, kwargs) -> ReturnType:
1547
1565
  invocation: Union[_Invocation, _InputPlaneInvocation]
1548
1566
  if self._input_plane_url:
@@ -1789,6 +1807,7 @@ Use the `Function.get_web_url()` method instead.
1789
1807
  starmap = MethodWithAio(_starmap_sync, _starmap_async, synchronizer)
1790
1808
  for_each = MethodWithAio(_for_each_sync, _for_each_async, synchronizer)
1791
1809
  spawn_map = MethodWithAio(_spawn_map_sync, _spawn_map_async, synchronizer)
1810
+ experimental_spawn_map = MethodWithAio(_experimental_spawn_map_sync, _experimental_spawn_map_async, synchronizer)
1792
1811
 
1793
1812
 
1794
1813
  class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.1.2.dev6",
36
+ version: str = "1.1.2.dev8",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.1.2.dev6",
167
+ version: str = "1.1.2.dev8",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -405,6 +405,12 @@ class Function(
405
405
 
406
406
  _map: ___map_spec[typing_extensions.Self]
407
407
 
408
+ class ___spawn_map_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
409
+ def __call__(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
410
+ async def aio(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
411
+
412
+ _spawn_map: ___spawn_map_spec[modal._functions.ReturnType, typing_extensions.Self]
413
+
408
414
  class ___call_function_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
409
415
  def __call__(self, /, args, kwargs) -> ReturnType_INNER: ...
410
416
  async def aio(self, /, args, kwargs) -> ReturnType_INNER: ...
@@ -693,6 +699,33 @@ class Function(
693
699
 
694
700
  spawn_map: __spawn_map_spec[typing_extensions.Self]
695
701
 
702
+ class __experimental_spawn_map_spec(typing_extensions.Protocol[SUPERSELF]):
703
+ def __call__(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall:
704
+ """Spawn parallel execution over a set of inputs, exiting as soon as the inputs are created (without waiting
705
+ for the map to complete).
706
+
707
+ Takes one iterator argument per argument in the function being mapped over.
708
+
709
+ Example:
710
+ ```python
711
+ @app.function()
712
+ def my_func(a):
713
+ return a ** 2
714
+
715
+
716
+ @app.local_entrypoint()
717
+ def main():
718
+ fc = my_func.spawn_map([1, 2, 3, 4])
719
+ ```
720
+
721
+ Returns a FunctionCall object that can be used to retrieve results
722
+ """
723
+ ...
724
+
725
+ async def aio(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall: ...
726
+
727
+ experimental_spawn_map: __experimental_spawn_map_spec[typing_extensions.Self]
728
+
696
729
  class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Object):
697
730
  """A reference to an executed function call.
698
731
 
@@ -86,6 +86,263 @@ if typing.TYPE_CHECKING:
86
86
  import modal.functions
87
87
 
88
88
 
89
+ class InputPreprocessor:
90
+ """
91
+ Constructs FunctionPutInputsItem objects from the raw-input queue, and puts them in the processed-input queue.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ client: "modal.client._Client",
97
+ *,
98
+ raw_input_queue: _SynchronizedQueue,
99
+ processed_input_queue: asyncio.Queue,
100
+ function: "modal.functions._Function",
101
+ created_callback: Callable[[int], None],
102
+ done_callback: Callable[[], None],
103
+ ):
104
+ self.client = client
105
+ self.function = function
106
+ self.inputs_created = 0
107
+ self.raw_input_queue = raw_input_queue
108
+ self.processed_input_queue = processed_input_queue
109
+ self.created_callback = created_callback
110
+ self.done_callback = done_callback
111
+
112
+ async def input_iter(self):
113
+ while 1:
114
+ raw_input = await self.raw_input_queue.get()
115
+ if raw_input is None: # end of input sentinel
116
+ break
117
+ yield raw_input # args, kwargs
118
+
119
+ def create_input_factory(self):
120
+ async def create_input(argskwargs):
121
+ idx = self.inputs_created
122
+ self.inputs_created += 1
123
+ self.created_callback(self.inputs_created)
124
+ (args, kwargs) = argskwargs
125
+ return await _create_input(
126
+ args,
127
+ kwargs,
128
+ self.client.stub,
129
+ max_object_size_bytes=self.function._max_object_size_bytes,
130
+ idx=idx,
131
+ method_name=self.function._use_method_name,
132
+ )
133
+
134
+ return create_input
135
+
136
+ async def drain_input_generator(self):
137
+ # Parallelize uploading blobs
138
+ async with aclosing(
139
+ async_map_ordered(self.input_iter(), self.create_input_factory(), concurrency=BLOB_MAX_PARALLELISM)
140
+ ) as streamer:
141
+ async for item in streamer:
142
+ await self.processed_input_queue.put(item)
143
+
144
+ # close queue iterator
145
+ await self.processed_input_queue.put(None)
146
+ self.done_callback()
147
+ yield
148
+
149
+
150
+ class InputPumper:
151
+ """
152
+ Reads inputs from a queue of FunctionPutInputsItems, and sends them to the server.
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ client: "modal.client._Client",
158
+ *,
159
+ input_queue: asyncio.Queue,
160
+ function: "modal.functions._Function",
161
+ function_call_id: str,
162
+ map_items_manager: Optional["_MapItemsManager"] = None,
163
+ ):
164
+ self.client = client
165
+ self.function = function
166
+ self.map_items_manager = map_items_manager
167
+ self.input_queue = input_queue
168
+ self.inputs_sent = 0
169
+ self.function_call_id = function_call_id
170
+
171
+ async def pump_inputs(self):
172
+ assert self.client.stub
173
+ async for items in queue_batch_iterator(self.input_queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
174
+ # Add items to the manager. Their state will be SENDING.
175
+ if self.map_items_manager is not None:
176
+ await self.map_items_manager.add_items(items)
177
+ request = api_pb2.FunctionPutInputsRequest(
178
+ function_id=self.function.object_id,
179
+ inputs=items,
180
+ function_call_id=self.function_call_id,
181
+ )
182
+ logger.debug(
183
+ f"Pushing {len(items)} inputs to server. Num queued inputs awaiting"
184
+ f" push is {self.input_queue.qsize()}. "
185
+ )
186
+
187
+ resp = await self._send_inputs(self.client.stub.FunctionPutInputs, request)
188
+ self.inputs_sent += len(items)
189
+ # Change item state to WAITING_FOR_OUTPUT, and set the input_id and input_jwt which are in the response.
190
+ if self.map_items_manager is not None:
191
+ self.map_items_manager.handle_put_inputs_response(resp.inputs)
192
+ logger.debug(
193
+ f"Successfully pushed {len(items)} inputs to server. "
194
+ f"Num queued inputs awaiting push is {self.input_queue.qsize()}."
195
+ )
196
+ yield
197
+
198
+ async def _send_inputs(
199
+ self,
200
+ fn: "modal.client.UnaryUnaryWrapper",
201
+ request: typing.Union[api_pb2.FunctionPutInputsRequest, api_pb2.FunctionRetryInputsRequest],
202
+ ) -> typing.Union[api_pb2.FunctionPutInputsResponse, api_pb2.FunctionRetryInputsResponse]:
203
+ # with 8 retries we log the warning below about every 30 seconds which isn't too spammy.
204
+ retry_warning_message = RetryWarningMessage(
205
+ message=f"Warning: map progress for function {self.function._function_name} is limited."
206
+ " Common bottlenecks include slow iteration over results, or function backlogs.",
207
+ warning_interval=8,
208
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
209
+ )
210
+ return await retry_transient_errors(
211
+ fn,
212
+ request,
213
+ max_retries=None,
214
+ max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
215
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
216
+ retry_warning_message=retry_warning_message,
217
+ )
218
+
219
+
220
+ class SyncInputPumper(InputPumper):
221
+ def __init__(
222
+ self,
223
+ client: "modal.client._Client",
224
+ *,
225
+ input_queue: asyncio.Queue,
226
+ retry_queue: TimestampPriorityQueue,
227
+ function: "modal.functions._Function",
228
+ function_call_jwt: str,
229
+ function_call_id: str,
230
+ map_items_manager: "_MapItemsManager",
231
+ ):
232
+ super().__init__(
233
+ client,
234
+ input_queue=input_queue,
235
+ function=function,
236
+ function_call_id=function_call_id,
237
+ map_items_manager=map_items_manager,
238
+ )
239
+ self.retry_queue = retry_queue
240
+ self.inputs_retried = 0
241
+ self.function_call_jwt = function_call_jwt
242
+
243
+ async def retry_inputs(self):
244
+ async for retriable_idxs in queue_batch_iterator(self.retry_queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
245
+ # For each index, use the context in the manager to create a FunctionRetryInputsItem.
246
+ # This will also update the context state to RETRYING.
247
+ inputs: list[api_pb2.FunctionRetryInputsItem] = await self.map_items_manager.prepare_items_for_retry(
248
+ retriable_idxs
249
+ )
250
+ request = api_pb2.FunctionRetryInputsRequest(
251
+ function_call_jwt=self.function_call_jwt,
252
+ inputs=inputs,
253
+ )
254
+ resp = await self._send_inputs(self.client.stub.FunctionRetryInputs, request)
255
+ # Update the state to WAITING_FOR_OUTPUT, and update the input_jwt in the context
256
+ # to the new value in the response.
257
+ self.map_items_manager.handle_retry_response(resp.input_jwts)
258
+ logger.debug(f"Successfully pushed retry for {len(inputs)} to server.")
259
+ self.inputs_retried += len(inputs)
260
+ yield
261
+
262
+
263
+ class AsyncInputPumper(InputPumper):
264
+ def __init__(
265
+ self,
266
+ client: "modal.client._Client",
267
+ *,
268
+ input_queue: asyncio.Queue,
269
+ function: "modal.functions._Function",
270
+ function_call_id: str,
271
+ ):
272
+ super().__init__(client, input_queue=input_queue, function=function, function_call_id=function_call_id)
273
+
274
+
275
+ async def _spawn_map_invocation(
276
+ function: "modal.functions._Function", raw_input_queue: _SynchronizedQueue, client: "modal.client._Client"
277
+ ) -> str:
278
+ assert client.stub
279
+ request = api_pb2.FunctionMapRequest(
280
+ function_id=function.object_id,
281
+ parent_input_id=current_input_id() or "",
282
+ function_call_type=api_pb2.FUNCTION_CALL_TYPE_MAP,
283
+ function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC,
284
+ )
285
+ response: api_pb2.FunctionMapResponse = await retry_transient_errors(client.stub.FunctionMap, request)
286
+ function_call_id = response.function_call_id
287
+
288
+ have_all_inputs = False
289
+ inputs_created = 0
290
+
291
+ def set_inputs_created(set_inputs_created):
292
+ nonlocal inputs_created
293
+ assert set_inputs_created is None or set_inputs_created > inputs_created
294
+ inputs_created = set_inputs_created
295
+
296
+ def set_have_all_inputs():
297
+ nonlocal have_all_inputs
298
+ have_all_inputs = True
299
+
300
+ input_queue: asyncio.Queue[api_pb2.FunctionPutInputsItem | None] = asyncio.Queue()
301
+ input_preprocessor = InputPreprocessor(
302
+ client=client,
303
+ raw_input_queue=raw_input_queue,
304
+ processed_input_queue=input_queue,
305
+ function=function,
306
+ created_callback=set_inputs_created,
307
+ done_callback=set_have_all_inputs,
308
+ )
309
+
310
+ input_pumper = AsyncInputPumper(
311
+ client=client,
312
+ input_queue=input_queue,
313
+ function=function,
314
+ function_call_id=function_call_id,
315
+ )
316
+
317
+ def log_stats():
318
+ logger.debug(
319
+ f"have_all_inputs={have_all_inputs} inputs_created={inputs_created} inputs_sent={input_pumper.inputs_sent} "
320
+ )
321
+
322
+ async def log_task():
323
+ while True:
324
+ log_stats()
325
+ try:
326
+ await asyncio.sleep(10)
327
+ except asyncio.CancelledError:
328
+ # Log final stats before exiting
329
+ log_stats()
330
+ break
331
+
332
+ async def consume_generator(gen):
333
+ async for _ in gen:
334
+ pass
335
+
336
+ log_debug_stats_task = asyncio.create_task(log_task())
337
+ await asyncio.gather(
338
+ consume_generator(input_preprocessor.drain_input_generator()),
339
+ consume_generator(input_pumper.pump_inputs()),
340
+ )
341
+ log_debug_stats_task.cancel()
342
+ await log_debug_stats_task
343
+ return function_call_id
344
+
345
+
89
346
  async def _map_invocation(
90
347
  function: "modal.functions._Function",
91
348
  raw_input_queue: _SynchronizedQueue,
@@ -117,8 +374,6 @@ async def _map_invocation(
117
374
  have_all_inputs = False
118
375
  map_done_event = asyncio.Event()
119
376
  inputs_created = 0
120
- inputs_sent = 0
121
- inputs_retried = 0
122
377
  outputs_completed = 0
123
378
  outputs_received = 0
124
379
  retried_outputs = 0
@@ -135,25 +390,24 @@ async def _map_invocation(
135
390
  retry_policy, function_call_invocation_type, retry_queue, sync_client_retries_enabled, max_inputs_outstanding
136
391
  )
137
392
 
138
- async def create_input(argskwargs):
139
- idx = inputs_created
140
- update_state(set_inputs_created=inputs_created + 1)
141
- (args, kwargs) = argskwargs
142
- return await _create_input(
143
- args,
144
- kwargs,
145
- client.stub,
146
- max_object_size_bytes=function._max_object_size_bytes,
147
- idx=idx,
148
- method_name=function._use_method_name,
149
- )
393
+ input_preprocessor = InputPreprocessor(
394
+ client=client,
395
+ raw_input_queue=raw_input_queue,
396
+ processed_input_queue=input_queue,
397
+ function=function,
398
+ created_callback=lambda x: update_state(set_inputs_created=x),
399
+ done_callback=lambda: update_state(set_have_all_inputs=True),
400
+ )
150
401
 
151
- async def input_iter():
152
- while 1:
153
- raw_input = await raw_input_queue.get()
154
- if raw_input is None: # end of input sentinel
155
- break
156
- yield raw_input # args, kwargs
402
+ input_pumper = SyncInputPumper(
403
+ client=client,
404
+ input_queue=input_queue,
405
+ retry_queue=retry_queue,
406
+ function=function,
407
+ map_items_manager=map_items_manager,
408
+ function_call_jwt=function_call_jwt,
409
+ function_call_id=function_call_id,
410
+ )
157
411
 
158
412
  def update_state(set_have_all_inputs=None, set_inputs_created=None, set_outputs_completed=None):
159
413
  # This should be the only method that needs nonlocal of the following vars
@@ -175,84 +429,6 @@ async def _map_invocation(
175
429
  # map is done
176
430
  map_done_event.set()
177
431
 
178
- async def drain_input_generator():
179
- # Parallelize uploading blobs
180
- async with aclosing(
181
- async_map_ordered(input_iter(), create_input, concurrency=BLOB_MAX_PARALLELISM)
182
- ) as streamer:
183
- async for item in streamer:
184
- await input_queue.put(item)
185
-
186
- # close queue iterator
187
- await input_queue.put(None)
188
- update_state(set_have_all_inputs=True)
189
- yield
190
-
191
- async def pump_inputs():
192
- assert client.stub
193
- nonlocal inputs_sent
194
- async for items in queue_batch_iterator(input_queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
195
- # Add items to the manager. Their state will be SENDING.
196
- await map_items_manager.add_items(items)
197
- request = api_pb2.FunctionPutInputsRequest(
198
- function_id=function.object_id,
199
- inputs=items,
200
- function_call_id=function_call_id,
201
- )
202
- logger.debug(
203
- f"Pushing {len(items)} inputs to server. Num queued inputs awaiting push is {input_queue.qsize()}."
204
- )
205
-
206
- resp = await send_inputs(client.stub.FunctionPutInputs, request)
207
- inputs_sent += len(items)
208
- # Change item state to WAITING_FOR_OUTPUT, and set the input_id and input_jwt which are in the response.
209
- map_items_manager.handle_put_inputs_response(resp.inputs)
210
- logger.debug(
211
- f"Successfully pushed {len(items)} inputs to server. "
212
- f"Num queued inputs awaiting push is {input_queue.qsize()}."
213
- )
214
- yield
215
-
216
- async def retry_inputs():
217
- nonlocal inputs_retried
218
- async for retriable_idxs in queue_batch_iterator(retry_queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
219
- # For each index, use the context in the manager to create a FunctionRetryInputsItem.
220
- # This will also update the context state to RETRYING.
221
- inputs: list[api_pb2.FunctionRetryInputsItem] = await map_items_manager.prepare_items_for_retry(
222
- retriable_idxs
223
- )
224
- request = api_pb2.FunctionRetryInputsRequest(
225
- function_call_jwt=function_call_jwt,
226
- inputs=inputs,
227
- )
228
- resp = await send_inputs(client.stub.FunctionRetryInputs, request)
229
- # Update the state to WAITING_FOR_OUTPUT, and update the input_jwt in the context
230
- # to the new value in the response.
231
- map_items_manager.handle_retry_response(resp.input_jwts)
232
- logger.debug(f"Successfully pushed retry for {len(inputs)} to server.")
233
- inputs_retried += len(inputs)
234
- yield
235
-
236
- async def send_inputs(
237
- fn: "modal.client.UnaryUnaryWrapper",
238
- request: typing.Union[api_pb2.FunctionPutInputsRequest, api_pb2.FunctionRetryInputsRequest],
239
- ) -> typing.Union[api_pb2.FunctionPutInputsResponse, api_pb2.FunctionRetryInputsResponse]:
240
- # with 8 retries we log the warning below about every 30 seconds which isn't too spammy.
241
- retry_warning_message = RetryWarningMessage(
242
- message=f"Warning: map progress for function {function._function_name} is limited."
243
- " Common bottlenecks include slow iteration over results, or function backlogs.",
244
- warning_interval=8,
245
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
246
- )
247
- return await retry_transient_errors(
248
- fn,
249
- request,
250
- max_retries=None,
251
- max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
252
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
253
- retry_warning_message=retry_warning_message,
254
- )
255
-
256
432
  async def get_all_outputs():
257
433
  assert client.stub
258
434
  nonlocal \
@@ -395,8 +571,11 @@ async def _map_invocation(
395
571
  def log_stats():
396
572
  logger.debug(
397
573
  f"Map stats: sync_client_retries_enabled={sync_client_retries_enabled} "
398
- f"have_all_inputs={have_all_inputs} inputs_created={inputs_created} input_sent={inputs_sent} "
399
- f"inputs_retried={inputs_retried} outputs_received={outputs_received} "
574
+ f"have_all_inputs={have_all_inputs} "
575
+ f"inputs_created={inputs_created} "
576
+ f"input_sent={input_pumper.inputs_sent} "
577
+ f"inputs_retried={input_pumper.inputs_retried} "
578
+ f"outputs_received={outputs_received} "
400
579
  f"successful_completions={successful_completions} failed_completions={failed_completions} "
401
580
  f"no_context_duplicates={no_context_duplicates} old_retry_duplicates={stale_retry_duplicates} "
402
581
  f"already_complete_duplicates={already_complete_duplicates} "
@@ -415,7 +594,12 @@ async def _map_invocation(
415
594
 
416
595
  log_debug_stats_task = asyncio.create_task(log_debug_stats())
417
596
  async with aclosing(
418
- async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())
597
+ async_merge(
598
+ input_preprocessor.drain_input_generator(),
599
+ input_pumper.pump_inputs(),
600
+ input_pumper.retry_inputs(),
601
+ poll_outputs(),
602
+ )
419
603
  ) as streamer:
420
604
  async for response in streamer:
421
605
  if response is not None: # type: ignore[unreachable]
@@ -962,6 +1146,54 @@ def _map_sync(
962
1146
  )
963
1147
 
964
1148
 
1149
+ async def _experimental_spawn_map_async(self, *input_iterators, kwargs={}) -> "modal.functions._FunctionCall":
1150
+ async_input_gen = async_zip(*[sync_or_async_iter(it) for it in input_iterators])
1151
+ return await _spawn_map_helper(self, async_input_gen, kwargs)
1152
+
1153
+
1154
+ async def _spawn_map_helper(
1155
+ self: "modal.functions.Function", async_input_gen, kwargs={}
1156
+ ) -> "modal.functions._FunctionCall":
1157
+ raw_input_queue: Any = SynchronizedQueue() # type: ignore
1158
+ await raw_input_queue.init.aio()
1159
+
1160
+ async def feed_queue():
1161
+ async with aclosing(async_input_gen) as streamer:
1162
+ async for args in streamer:
1163
+ await raw_input_queue.put.aio((args, kwargs))
1164
+ await raw_input_queue.put.aio(None) # end-of-input sentinel
1165
+
1166
+ fc, _ = await asyncio.gather(self._spawn_map.aio(raw_input_queue), feed_queue())
1167
+ return fc
1168
+
1169
+
1170
+ def _experimental_spawn_map_sync(self, *input_iterators, kwargs={}) -> "modal.functions._FunctionCall":
1171
+ """Spawn parallel execution over a set of inputs, exiting as soon as the inputs are created (without waiting
1172
+ for the map to complete).
1173
+
1174
+ Takes one iterator argument per argument in the function being mapped over.
1175
+
1176
+ Example:
1177
+ ```python
1178
+ @app.function()
1179
+ def my_func(a):
1180
+ return a ** 2
1181
+
1182
+
1183
+ @app.local_entrypoint()
1184
+ def main():
1185
+ fc = my_func.spawn_map([1, 2, 3, 4])
1186
+ ```
1187
+
1188
+ Returns a FunctionCall object that can be used to retrieve results
1189
+ """
1190
+
1191
+ return run_coroutine_in_temporary_event_loop(
1192
+ _experimental_spawn_map_async(self, *input_iterators, kwargs=kwargs),
1193
+ "You can't run Function.spawn_map() from an async function. Use Function.spawn_map.aio() instead.",
1194
+ )
1195
+
1196
+
965
1197
  async def _spawn_map_async(self, *input_iterators, kwargs={}) -> None:
966
1198
  """This runs in an event loop on the main thread. It consumes inputs from the input iterators and creates async
967
1199
  function calls for each.