modAL 1.1.1__tar.gz → 1.1.1.dev0__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 (190) hide show
  1. {modal-1.1.1 → modal-1.1.1.dev0}/PKG-INFO +2 -2
  2. {modal-1.1.1 → modal-1.1.1.dev0}/modal/__main__.py +2 -2
  3. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_clustered_functions.py +0 -3
  4. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_clustered_functions.pyi +2 -3
  5. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_functions.py +26 -78
  6. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_object.py +1 -9
  7. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_output.py +25 -14
  8. modal-1.1.1.dev0/modal/_runtime/gpu_memory_snapshot.py +199 -0
  9. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/async_utils.py +4 -6
  10. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/auth_token_manager.py +1 -1
  11. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/blob_utils.py +21 -16
  12. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/function_utils.py +4 -16
  13. modal-1.1.1.dev0/modal/_utils/time_utils.py +15 -0
  14. {modal-1.1.1 → modal-1.1.1.dev0}/modal/app.py +4 -0
  15. {modal-1.1.1 → modal-1.1.1.dev0}/modal/app.pyi +4 -0
  16. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/_traceback.py +2 -3
  17. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/app.py +4 -4
  18. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/cluster.py +4 -4
  19. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/config.py +2 -2
  20. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/container.py +2 -2
  21. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/dict.py +4 -4
  22. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/entry_point.py +2 -2
  23. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/import_refs.py +3 -3
  24. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/network_file_system.py +9 -8
  25. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/profile.py +2 -2
  26. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/queues.py +5 -5
  27. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/secret.py +5 -5
  28. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/utils.py +4 -3
  29. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/volume.py +9 -8
  30. {modal-1.1.1 → modal-1.1.1.dev0}/modal/client.py +1 -8
  31. {modal-1.1.1 → modal-1.1.1.dev0}/modal/client.pyi +10 -9
  32. {modal-1.1.1 → modal-1.1.1.dev0}/modal/container_process.py +2 -2
  33. {modal-1.1.1 → modal-1.1.1.dev0}/modal/dict.py +3 -47
  34. {modal-1.1.1 → modal-1.1.1.dev0}/modal/dict.pyi +0 -55
  35. {modal-1.1.1 → modal-1.1.1.dev0}/modal/exception.py +0 -4
  36. {modal-1.1.1 → modal-1.1.1.dev0}/modal/experimental/__init__.py +1 -1
  37. {modal-1.1.1 → modal-1.1.1.dev0}/modal/experimental/flash.py +2 -18
  38. {modal-1.1.1 → modal-1.1.1.dev0}/modal/experimental/flash.pyi +0 -19
  39. {modal-1.1.1 → modal-1.1.1.dev0}/modal/functions.pyi +1 -0
  40. {modal-1.1.1 → modal-1.1.1.dev0}/modal/image.py +10 -26
  41. {modal-1.1.1 → modal-1.1.1.dev0}/modal/image.pyi +4 -12
  42. {modal-1.1.1 → modal-1.1.1.dev0}/modal/mount.py +1 -1
  43. {modal-1.1.1 → modal-1.1.1.dev0}/modal/object.pyi +0 -4
  44. {modal-1.1.1 → modal-1.1.1.dev0}/modal/parallel_map.py +4 -432
  45. {modal-1.1.1 → modal-1.1.1.dev0}/modal/parallel_map.pyi +0 -28
  46. {modal-1.1.1 → modal-1.1.1.dev0}/modal/queue.py +3 -46
  47. {modal-1.1.1 → modal-1.1.1.dev0}/modal/queue.pyi +0 -53
  48. {modal-1.1.1 → modal-1.1.1.dev0}/modal/sandbox.py +25 -105
  49. {modal-1.1.1 → modal-1.1.1.dev0}/modal/sandbox.pyi +18 -108
  50. {modal-1.1.1 → modal-1.1.1.dev0}/modal/secret.py +5 -48
  51. {modal-1.1.1 → modal-1.1.1.dev0}/modal/secret.pyi +0 -55
  52. {modal-1.1.1 → modal-1.1.1.dev0}/modal/token_flow.py +3 -3
  53. {modal-1.1.1 → modal-1.1.1.dev0}/modal/volume.py +18 -49
  54. {modal-1.1.1 → modal-1.1.1.dev0}/modal/volume.pyi +8 -50
  55. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/PKG-INFO +2 -2
  56. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/SOURCES.txt +8 -8
  57. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/requires.txt +1 -1
  58. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api.proto +14 -140
  59. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api_grpc.py +0 -80
  60. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api_pb2.py +756 -927
  61. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api_pb2.pyi +34 -488
  62. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api_pb2_grpc.py +0 -166
  63. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/api_pb2_grpc.pyi +0 -52
  64. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/modal_api_grpc.py +0 -5
  65. {modal-1.1.1 → modal-1.1.1.dev0}/modal_version/__init__.py +1 -1
  66. {modal-1.1.1 → modal-1.1.1.dev0}/pyproject.toml +2 -2
  67. modal-1.1.1/modal/_runtime/gpu_memory_snapshot.py +0 -303
  68. modal-1.1.1/modal/_utils/time_utils.py +0 -19
  69. {modal-1.1.1 → modal-1.1.1.dev0}/LICENSE +0 -0
  70. {modal-1.1.1 → modal-1.1.1.dev0}/README.md +0 -0
  71. {modal-1.1.1 → modal-1.1.1.dev0}/modal/__init__.py +0 -0
  72. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_container_entrypoint.py +0 -0
  73. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_ipython.py +0 -0
  74. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_location.py +0 -0
  75. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_partial_function.py +0 -0
  76. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_pty.py +0 -0
  77. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_resolver.py +0 -0
  78. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_resources.py +0 -0
  79. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/__init__.py +0 -0
  80. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/asgi.py +0 -0
  81. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/container_io_manager.py +0 -0
  82. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
  83. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/execution_context.py +0 -0
  84. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/execution_context.pyi +0 -0
  85. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/telemetry.py +0 -0
  86. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_runtime/user_code_imports.py +0 -0
  87. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_serialization.py +0 -0
  88. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_traceback.py +0 -0
  89. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_tunnel.py +0 -0
  90. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_tunnel.pyi +0 -0
  91. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_type_manager.py +0 -0
  92. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/__init__.py +0 -0
  93. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/app_utils.py +0 -0
  94. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
  95. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/deprecation.py +0 -0
  96. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/docker_utils.py +0 -0
  97. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/git_utils.py +0 -0
  98. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/grpc_testing.py +0 -0
  99. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/grpc_utils.py +0 -0
  100. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/hash_utils.py +0 -0
  101. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/http_utils.py +0 -0
  102. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/jwt_utils.py +0 -0
  103. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/logger.py +0 -0
  104. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/mount_utils.py +0 -0
  105. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/name_utils.py +0 -0
  106. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/package_utils.py +0 -0
  107. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/pattern_utils.py +0 -0
  108. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/rand_pb_testing.py +0 -0
  109. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_utils/shell_utils.py +0 -0
  110. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_vendor/__init__.py +0 -0
  111. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  112. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_vendor/cloudpickle.py +0 -0
  113. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_vendor/tblib.py +0 -0
  114. {modal-1.1.1 → modal-1.1.1.dev0}/modal/_watcher.py +0 -0
  115. {modal-1.1.1 → modal-1.1.1.dev0}/modal/call_graph.py +0 -0
  116. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/__init__.py +0 -0
  117. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/_download.py +0 -0
  118. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/environment.py +0 -0
  119. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/launch.py +0 -0
  120. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/programs/__init__.py +0 -0
  121. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/programs/run_jupyter.py +0 -0
  122. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/programs/vscode.py +0 -0
  123. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/run.py +0 -0
  124. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cli/token.py +0 -0
  125. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cloud_bucket_mount.py +0 -0
  126. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cloud_bucket_mount.pyi +0 -0
  127. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cls.py +0 -0
  128. {modal-1.1.1 → modal-1.1.1.dev0}/modal/cls.pyi +0 -0
  129. {modal-1.1.1 → modal-1.1.1.dev0}/modal/config.py +0 -0
  130. {modal-1.1.1 → modal-1.1.1.dev0}/modal/container_process.pyi +0 -0
  131. {modal-1.1.1 → modal-1.1.1.dev0}/modal/environments.py +0 -0
  132. {modal-1.1.1 → modal-1.1.1.dev0}/modal/environments.pyi +0 -0
  133. {modal-1.1.1 → modal-1.1.1.dev0}/modal/experimental/ipython.py +0 -0
  134. {modal-1.1.1 → modal-1.1.1.dev0}/modal/file_io.py +0 -0
  135. {modal-1.1.1 → modal-1.1.1.dev0}/modal/file_io.pyi +0 -0
  136. {modal-1.1.1 → modal-1.1.1.dev0}/modal/file_pattern_matcher.py +0 -0
  137. {modal-1.1.1 → modal-1.1.1.dev0}/modal/functions.py +0 -0
  138. {modal-1.1.1 → modal-1.1.1.dev0}/modal/gpu.py +0 -0
  139. {modal-1.1.1 → modal-1.1.1.dev0}/modal/io_streams.py +0 -0
  140. {modal-1.1.1 → modal-1.1.1.dev0}/modal/io_streams.pyi +0 -0
  141. {modal-1.1.1 → modal-1.1.1.dev0}/modal/mount.pyi +0 -0
  142. {modal-1.1.1 → modal-1.1.1.dev0}/modal/network_file_system.py +0 -0
  143. {modal-1.1.1 → modal-1.1.1.dev0}/modal/network_file_system.pyi +0 -0
  144. {modal-1.1.1 → modal-1.1.1.dev0}/modal/object.py +0 -0
  145. {modal-1.1.1 → modal-1.1.1.dev0}/modal/output.py +0 -0
  146. {modal-1.1.1 → modal-1.1.1.dev0}/modal/partial_function.py +0 -0
  147. {modal-1.1.1 → modal-1.1.1.dev0}/modal/partial_function.pyi +0 -0
  148. {modal-1.1.1 → modal-1.1.1.dev0}/modal/proxy.py +0 -0
  149. {modal-1.1.1 → modal-1.1.1.dev0}/modal/proxy.pyi +0 -0
  150. {modal-1.1.1 → modal-1.1.1.dev0}/modal/py.typed +0 -0
  151. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/2023.12.312.txt +0 -0
  152. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/2023.12.txt +0 -0
  153. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/2024.04.txt +0 -0
  154. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/2024.10.txt +0 -0
  155. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/2025.06.txt +0 -0
  156. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/PREVIEW.txt +0 -0
  157. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/README.md +0 -0
  158. {modal-1.1.1/modal/builder → modal-1.1.1.dev0/modal/requirements}/base-images.json +0 -0
  159. {modal-1.1.1 → modal-1.1.1.dev0}/modal/retries.py +0 -0
  160. {modal-1.1.1 → modal-1.1.1.dev0}/modal/runner.py +0 -0
  161. {modal-1.1.1 → modal-1.1.1.dev0}/modal/runner.pyi +0 -0
  162. {modal-1.1.1 → modal-1.1.1.dev0}/modal/running_app.py +0 -0
  163. {modal-1.1.1 → modal-1.1.1.dev0}/modal/schedule.py +0 -0
  164. {modal-1.1.1 → modal-1.1.1.dev0}/modal/scheduler_placement.py +0 -0
  165. {modal-1.1.1 → modal-1.1.1.dev0}/modal/serving.py +0 -0
  166. {modal-1.1.1 → modal-1.1.1.dev0}/modal/serving.pyi +0 -0
  167. {modal-1.1.1 → modal-1.1.1.dev0}/modal/snapshot.py +0 -0
  168. {modal-1.1.1 → modal-1.1.1.dev0}/modal/snapshot.pyi +0 -0
  169. {modal-1.1.1 → modal-1.1.1.dev0}/modal/stream_type.py +0 -0
  170. {modal-1.1.1 → modal-1.1.1.dev0}/modal/token_flow.pyi +0 -0
  171. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/dependency_links.txt +0 -0
  172. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/entry_points.txt +0 -0
  173. {modal-1.1.1 → modal-1.1.1.dev0}/modal.egg-info/top_level.txt +0 -0
  174. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/__init__.py +0 -0
  175. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/gen_cli_docs.py +0 -0
  176. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/gen_reference_docs.py +0 -0
  177. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/mdmd/__init__.py +0 -0
  178. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/mdmd/mdmd.py +0 -0
  179. {modal-1.1.1 → modal-1.1.1.dev0}/modal_docs/mdmd/signatures.py +0 -0
  180. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/__init__.py +0 -0
  181. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/modal_options_grpc.py +0 -0
  182. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options.proto +0 -0
  183. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options_grpc.py +0 -0
  184. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options_pb2.py +0 -0
  185. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options_pb2.pyi +0 -0
  186. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options_pb2_grpc.py +0 -0
  187. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/options_pb2_grpc.pyi +0 -0
  188. {modal-1.1.1 → modal-1.1.1.dev0}/modal_proto/py.typed +0 -0
  189. {modal-1.1.1 → modal-1.1.1.dev0}/modal_version/__main__.py +0 -0
  190. {modal-1.1.1 → modal-1.1.1.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1
3
+ Version: 1.1.1.dev0
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: aiohttp
20
20
  Requires-Dist: certifi
21
- Requires-Dist: click~=8.1
21
+ Requires-Dist: click~=8.1.0
22
22
  Requires-Dist: grpclib<0.4.9,>=0.4.7
23
23
  Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
24
24
  Requires-Dist: rich>=12.0.0
@@ -1,7 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import sys
3
3
 
4
- from ._output import make_console
5
4
  from ._traceback import reduce_traceback_to_user_code
6
5
  from .cli._traceback import highlight_modal_warnings, setup_rich_traceback
7
6
  from .cli.entry_point import entrypoint_cli
@@ -36,6 +35,7 @@ def main():
36
35
  raise
37
36
 
38
37
  from grpclib import GRPCError, Status
38
+ from rich.console import Console
39
39
  from rich.panel import Panel
40
40
  from rich.text import Text
41
41
 
@@ -68,7 +68,7 @@ def main():
68
68
  if notes := getattr(exc, "__notes__", []):
69
69
  content = f"{content}\n\nNote: {' '.join(notes)}"
70
70
 
71
- console = make_console(stderr=True)
71
+ console = Console(stderr=True)
72
72
  panel = Panel(Text(content), title=title, title_align="left", border_style="red")
73
73
  console.print(panel, highlight=False)
74
74
  sys.exit(1)
@@ -15,7 +15,6 @@ from modal_proto import api_pb2
15
15
  class ClusterInfo:
16
16
  rank: int
17
17
  container_ips: list[str]
18
- container_ipv4_ips: list[str]
19
18
 
20
19
 
21
20
  cluster_info: Optional[ClusterInfo] = None
@@ -70,13 +69,11 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
70
69
  cluster_info = ClusterInfo(
71
70
  rank=resp.cluster_rank,
72
71
  container_ips=resp.container_ips,
73
- container_ipv4_ips=resp.container_ipv4_ips,
74
72
  )
75
73
  else:
76
74
  cluster_info = ClusterInfo(
77
75
  rank=0,
78
76
  container_ips=[container_ip],
79
- container_ipv4_ips=[], # No IPv4 IPs for single-node
80
77
  )
81
78
 
82
79
 
@@ -3,13 +3,12 @@ import typing
3
3
  import typing_extensions
4
4
 
5
5
  class ClusterInfo:
6
- """ClusterInfo(rank: int, container_ips: list[str], container_ipv4_ips: list[str])"""
6
+ """ClusterInfo(rank: int, container_ips: list[str])"""
7
7
 
8
8
  rank: int
9
9
  container_ips: list[str]
10
- container_ipv4_ips: list[str]
11
10
 
12
- def __init__(self, rank: int, container_ips: list[str], container_ipv4_ips: list[str]) -> None:
11
+ def __init__(self, rank: int, container_ips: list[str]) -> None:
13
12
  """Initialize self. See help(type(self)) for accurate signature."""
14
13
  ...
15
14
 
@@ -75,7 +75,6 @@ from .parallel_map import (
75
75
  _for_each_sync,
76
76
  _map_async,
77
77
  _map_invocation,
78
- _map_invocation_inputplane,
79
78
  _map_sync,
80
79
  _spawn_map_async,
81
80
  _spawn_map_sync,
@@ -400,8 +399,7 @@ class _InputPlaneInvocation:
400
399
  parent_input_id=current_input_id() or "",
401
400
  input=input_item,
402
401
  )
403
-
404
- metadata = await client.get_input_plane_metadata(input_plane_region)
402
+ metadata = await _InputPlaneInvocation._get_metadata(input_plane_region, client)
405
403
  response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
406
404
  attempt_token = response.attempt_token
407
405
 
@@ -417,7 +415,7 @@ class _InputPlaneInvocation:
417
415
  timeout_secs=OUTPUTS_TIMEOUT,
418
416
  requested_at=time.time(),
419
417
  )
420
- metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
418
+ metadata = await self._get_metadata(self.input_plane_region, self.client)
421
419
  await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
422
420
  self.stub.AttemptAwait,
423
421
  await_request,
@@ -453,33 +451,6 @@ class _InputPlaneInvocation:
453
451
  await_response.output.result, await_response.output.data_format, control_plane_stub, self.client
454
452
  )
455
453
 
456
- async def run_generator(self):
457
- items_received = 0
458
- # populated when self.run_function() completes
459
- items_total: Union[int, None] = None
460
- async with aclosing(
461
- async_merge(
462
- _stream_function_call_data(
463
- self.client,
464
- self.stub,
465
- "",
466
- variant="data_out",
467
- attempt_token=self.attempt_token,
468
- ),
469
- callable_to_agen(self.run_function),
470
- )
471
- ) as streamer:
472
- async for item in streamer:
473
- if isinstance(item, api_pb2.GeneratorDone):
474
- items_total = item.items_total
475
- else:
476
- yield item
477
- items_received += 1
478
- # The comparison avoids infinite loops if a non-deterministic generator is retried
479
- # and produces less data in the second run than what was already sent.
480
- if items_total is not None and items_received >= items_total:
481
- break
482
-
483
454
  @staticmethod
484
455
  async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
485
456
  if not input_plane_region:
@@ -629,6 +600,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
629
600
  experimental_options: Optional[dict[str, str]] = None,
630
601
  _experimental_proxy_ip: Optional[str] = None,
631
602
  _experimental_custom_scaling_factor: Optional[float] = None,
603
+ _experimental_enable_gpu_snapshot: bool = False,
632
604
  ) -> "_Function":
633
605
  """mdmd:hidden"""
634
606
  # Needed to avoid circular imports
@@ -929,6 +901,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
929
901
  _experimental_concurrent_cancellations=True,
930
902
  _experimental_proxy_ip=_experimental_proxy_ip,
931
903
  _experimental_custom_scaling=_experimental_custom_scaling_factor is not None,
904
+ _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
932
905
  # --- These are deprecated in favor of autoscaler_settings
933
906
  warm_pool_size=min_containers or 0,
934
907
  concurrency_limit=max_containers or 0,
@@ -965,6 +938,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
965
938
  _experimental_group_size=function_definition._experimental_group_size,
966
939
  _experimental_buffer_containers=function_definition._experimental_buffer_containers,
967
940
  _experimental_custom_scaling=function_definition._experimental_custom_scaling,
941
+ _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
968
942
  _experimental_proxy_ip=function_definition._experimental_proxy_ip,
969
943
  snapshot_debug=function_definition.snapshot_debug,
970
944
  runtime_perf_record=function_definition.runtime_perf_record,
@@ -1513,35 +1487,20 @@ Use the `Function.get_web_url()` method instead.
1513
1487
  else:
1514
1488
  count_update_callback = None
1515
1489
 
1516
- if self._input_plane_url:
1517
- async with aclosing(
1518
- _map_invocation_inputplane(
1519
- self,
1520
- input_queue,
1521
- self.client,
1522
- order_outputs,
1523
- return_exceptions,
1524
- wrap_returned_exceptions,
1525
- count_update_callback,
1526
- )
1527
- ) as stream:
1528
- async for item in stream:
1529
- yield item
1530
- else:
1531
- async with aclosing(
1532
- _map_invocation(
1533
- self,
1534
- input_queue,
1535
- self.client,
1536
- order_outputs,
1537
- return_exceptions,
1538
- wrap_returned_exceptions,
1539
- count_update_callback,
1540
- api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1541
- )
1542
- ) as stream:
1543
- async for item in stream:
1544
- yield item
1490
+ async with aclosing(
1491
+ _map_invocation(
1492
+ self,
1493
+ input_queue,
1494
+ self.client,
1495
+ order_outputs,
1496
+ return_exceptions,
1497
+ wrap_returned_exceptions,
1498
+ count_update_callback,
1499
+ api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1500
+ )
1501
+ ) as stream:
1502
+ async for item in stream:
1503
+ yield item
1545
1504
 
1546
1505
  async def _call_function(self, args, kwargs) -> ReturnType:
1547
1506
  invocation: Union[_Invocation, _InputPlaneInvocation]
@@ -1585,24 +1544,13 @@ Use the `Function.get_web_url()` method instead.
1585
1544
  @live_method_gen
1586
1545
  @synchronizer.no_input_translation
1587
1546
  async def _call_generator(self, args, kwargs):
1588
- invocation: Union[_Invocation, _InputPlaneInvocation]
1589
- if self._input_plane_url:
1590
- invocation = await _InputPlaneInvocation.create(
1591
- self,
1592
- args,
1593
- kwargs,
1594
- client=self.client,
1595
- input_plane_url=self._input_plane_url,
1596
- input_plane_region=self._input_plane_region,
1597
- )
1598
- else:
1599
- invocation = await _Invocation.create(
1600
- self,
1601
- args,
1602
- kwargs,
1603
- client=self.client,
1604
- function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1605
- )
1547
+ invocation = await _Invocation.create(
1548
+ self,
1549
+ args,
1550
+ kwargs,
1551
+ client=self.client,
1552
+ function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1553
+ )
1606
1554
  async for res in invocation.run_generator():
1607
1555
  yield res
1608
1556
 
@@ -48,10 +48,6 @@ class _Object:
48
48
  _is_hydrated: bool
49
49
  _is_rehydrated: bool
50
50
 
51
- # Not all object subclasses have a meaningful "name" concept
52
- # So whether they expose this is a matter of having a name property
53
- _name: Optional[str]
54
-
55
51
  @classmethod
56
52
  def __init_subclass__(cls, type_prefix: Optional[str] = None):
57
53
  super().__init_subclass__()
@@ -72,7 +68,6 @@ class _Object:
72
68
  hydrate_lazily: bool = False,
73
69
  deps: Optional[Callable[..., Sequence["_Object"]]] = None,
74
70
  deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
75
- name: Optional[str] = None,
76
71
  ):
77
72
  self._local_uuid = str(uuid.uuid4())
78
73
  self._load = load
@@ -88,8 +83,6 @@ class _Object:
88
83
  self._is_hydrated = False
89
84
  self._is_rehydrated = False
90
85
 
91
- self._name = name
92
-
93
86
  self._initialize_from_empty()
94
87
 
95
88
  def _unhydrate(self):
@@ -170,11 +163,10 @@ class _Object:
170
163
  hydrate_lazily: bool = False,
171
164
  deps: Optional[Callable[..., Sequence["_Object"]]] = None,
172
165
  deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
173
- name: Optional[str] = None,
174
166
  ):
175
167
  # TODO(erikbern): flip the order of the two first arguments
176
168
  obj = _Object.__new__(cls)
177
- obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key, name)
169
+ obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
178
170
  return obj
179
171
 
180
172
  @staticmethod
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import asyncio
5
5
  import contextlib
6
6
  import functools
7
+ import io
7
8
  import platform
8
9
  import re
9
10
  import socket
@@ -31,7 +32,7 @@ from rich.progress import (
31
32
  from rich.spinner import Spinner
32
33
  from rich.text import Text
33
34
 
34
- from modal._utils.time_utils import timestamp_to_localized_str
35
+ from modal._utils.time_utils import timestamp_to_local
35
36
  from modal_proto import api_pb2
36
37
 
37
38
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
@@ -45,16 +46,6 @@ else:
45
46
  default_spinner = "dots"
46
47
 
47
48
 
48
- def make_console(*, stderr: bool = False, highlight: bool = True) -> Console:
49
- """Create a rich Console tuned for Modal CLI output."""
50
- return Console(
51
- stderr=stderr,
52
- highlight=highlight,
53
- # CLI does not work with auto-detected Jupyter HTML display_data.
54
- force_jupyter=False,
55
- )
56
-
57
-
58
49
  class FunctionQueuingColumn(ProgressColumn):
59
50
  """Renders time elapsed, including task.completed as additional elapsed time."""
60
51
 
@@ -72,6 +63,25 @@ class FunctionQueuingColumn(ProgressColumn):
72
63
  return Text(str(delta), style="progress.elapsed")
73
64
 
74
65
 
66
+ def download_progress_bar() -> Progress:
67
+ """
68
+ Returns a progress bar suitable for showing file download progress.
69
+ Requires passing a `path: str` data field for rendering.
70
+ """
71
+ return Progress(
72
+ TextColumn("[bold white]{task.fields[path]}", justify="right"),
73
+ BarColumn(bar_width=None),
74
+ "[progress.percentage]{task.percentage:>3.1f}%",
75
+ "•",
76
+ DownloadColumn(),
77
+ "•",
78
+ TransferSpeedColumn(),
79
+ "•",
80
+ TimeRemainingColumn(),
81
+ transient=True,
82
+ )
83
+
84
+
75
85
  class LineBufferedOutput:
76
86
  """Output stream that buffers lines and passes them to a callback."""
77
87
 
@@ -91,7 +101,7 @@ class LineBufferedOutput:
91
101
 
92
102
  if self._show_timestamps:
93
103
  for i in range(0, len(chunks) - 1, 2):
94
- chunks[i] = f"{timestamp_to_localized_str(log.timestamp)} {chunks[i]}"
104
+ chunks[i] = f"{timestamp_to_local(log.timestamp)} {chunks[i]}"
95
105
 
96
106
  completed_lines = "".join(chunks[:-1])
97
107
  remainder = chunks[-1]
@@ -137,11 +147,12 @@ class OutputManager:
137
147
  def __init__(
138
148
  self,
139
149
  *,
150
+ stdout: io.TextIOWrapper | None = None,
140
151
  status_spinner_text: str = "Running app...",
141
152
  show_timestamps: bool = False,
142
153
  ):
143
- self._stdout = sys.stdout
144
- self._console = make_console(highlight=False)
154
+ self._stdout = stdout or sys.stdout
155
+ self._console = Console(file=stdout, highlight=False)
145
156
  self._task_states = {}
146
157
  self._task_progress_items = {}
147
158
  self._current_render_group = None
@@ -0,0 +1,199 @@
1
+ # Copyright Modal Labs 2022
2
+ #
3
+ # This module provides a simple interface for creating GPU memory snapshots,
4
+ # provising a convenient interface to `cuda-checkpoint` [1]. This is intended
5
+ # to be used in conjunction with memory snapshots.
6
+ #
7
+ # [1] https://github.com/NVIDIA/cuda-checkpoint
8
+
9
+ import subprocess
10
+ import time
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+ from pathlib import Path
15
+
16
+ from modal.config import config, logger
17
+
18
+ CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
19
+
20
+
21
+ class CudaCheckpointState(Enum):
22
+ """State representation from the CUDA API: https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__TYPES.html#group__CUDA__TYPES_1gc96cdda177a2b8c296144567cbea4f23"""
23
+
24
+ RUNNING = "running"
25
+ LOCKED = "locked"
26
+ CHECKPOINTED = "checkpointed"
27
+ FAILED = "failed"
28
+
29
+
30
+ class CudaCheckpointException(Exception):
31
+ pass
32
+
33
+
34
+ @dataclass
35
+ class CudaCheckpointProcess:
36
+ """Contains a reference to a PID with active CUDA session. This also provides
37
+ methods for checkpointing and restoring GPU memory."""
38
+
39
+ pid: int
40
+ state: CudaCheckpointState
41
+
42
+ def toggle(self, target_state: CudaCheckpointState, timeout_secs: float = 5 * 60.0):
43
+ """Toggle CUDA checkpoint state for current process, moving GPU memory to the
44
+ CPU and back depending on the current process state when called."""
45
+ logger.debug(f"PID: {self.pid} Toggling CUDA checkpoint state to {target_state.value}")
46
+
47
+ start_time = time.monotonic()
48
+
49
+ while self._should_continue_toggle(target_state, start_time, timeout_secs):
50
+ self._execute_toggle_command()
51
+ time.sleep(0.1)
52
+
53
+ logger.debug(f"PID: {self.pid} Target state {target_state.value} reached")
54
+
55
+ def _should_continue_toggle(
56
+ self, target_state: CudaCheckpointState, start_time: float, timeout_secs: float
57
+ ) -> bool:
58
+ """Check if toggle operation should continue based on current state and timeout."""
59
+ self.refresh_state()
60
+
61
+ if self.state == target_state:
62
+ return False
63
+
64
+ if self.state == CudaCheckpointState.FAILED:
65
+ raise CudaCheckpointException(f"PID: {self.pid} CUDA process state is {self.state}")
66
+
67
+ elapsed = time.monotonic() - start_time
68
+ if elapsed >= timeout_secs:
69
+ raise CudaCheckpointException(
70
+ f"PID: {self.pid} Timeout after {elapsed:.2f}s waiting for state {target_state.value}. "
71
+ f"Current state: {self.state}"
72
+ )
73
+
74
+ return True
75
+
76
+ def _execute_toggle_command(self):
77
+ """Execute the cuda-checkpoint toggle command."""
78
+ try:
79
+ subprocess.run(
80
+ [CUDA_CHECKPOINT_PATH, "--toggle", "--pid", str(self.pid)],
81
+ check=True,
82
+ capture_output=True,
83
+ text=True,
84
+ )
85
+ logger.debug(f"PID: {self.pid} Successfully toggled CUDA checkpoint state")
86
+ except subprocess.CalledProcessError as e:
87
+ logger.debug(f"PID: {self.pid} Failed to toggle CUDA checkpoint state: {e.stderr}")
88
+ raise CudaCheckpointException(e.stderr)
89
+
90
+ def refresh_state(self) -> None:
91
+ """Refreshes the current CUDA checkpoint state for this process."""
92
+ try:
93
+ result = subprocess.run(
94
+ [CUDA_CHECKPOINT_PATH, "--get-state", "--pid", str(self.pid)],
95
+ check=True,
96
+ capture_output=True,
97
+ text=True,
98
+ timeout=5,
99
+ )
100
+
101
+ state_str = result.stdout.strip().lower()
102
+ self.state = CudaCheckpointState(state_str)
103
+
104
+ except subprocess.CalledProcessError as e:
105
+ logger.debug(f"PID: {self.pid} Failed to get CUDA checkpoint state: {e.stderr}")
106
+ raise CudaCheckpointException(e.stderr)
107
+
108
+
109
+ class CudaCheckpointSession:
110
+ """Manages the checkpointing state of processes with active CUDA sessions."""
111
+
112
+ def __init__(self):
113
+ self.cuda_processes = self._get_cuda_pids()
114
+ logger.debug(f"PIDs with CUDA sessions: {[c.pid for c in self.cuda_processes]}")
115
+
116
+ def _get_cuda_pids(self) -> list[CudaCheckpointProcess]:
117
+ """Iterates over all PIDs and identifies the ones that have running
118
+ CUDA sessions."""
119
+ cuda_pids: list[CudaCheckpointProcess] = []
120
+
121
+ # Get all active process IDs from /proc directory
122
+ proc_dir = Path("/proc")
123
+ if not proc_dir.exists():
124
+ raise CudaCheckpointException(
125
+ "OS does not have /proc path rendering it incompatible with GPU memory snapshots."
126
+ )
127
+
128
+ for entry in proc_dir.iterdir():
129
+ if not entry.name.isdigit():
130
+ continue
131
+
132
+ pid = int(entry.name)
133
+ try:
134
+ # Call cuda-checkpoint to check if this PID has a CUDA session
135
+ result = subprocess.run(
136
+ [CUDA_CHECKPOINT_PATH, "--get-state", "--pid", str(pid)],
137
+ capture_output=True,
138
+ text=True,
139
+ timeout=10,
140
+ )
141
+
142
+ # If the command succeeds (return code 0), this PID has a CUDA session
143
+ if result.returncode == 0:
144
+ state_str = result.stdout.strip().lower()
145
+ state = CudaCheckpointState(state_str)
146
+
147
+ cuda_checkpoint_process = CudaCheckpointProcess(pid=pid, state=state)
148
+ cuda_pids.append(cuda_checkpoint_process)
149
+
150
+ # Command failed, which is expected for PIDs without CUDA sessions
151
+ except subprocess.CalledProcessError:
152
+ continue
153
+
154
+ # Raise other exceptions
155
+ except subprocess.TimeoutExpired:
156
+ raise CudaCheckpointException(f"Failed to get CUDA state for PID {pid}")
157
+ except Exception as e:
158
+ raise CudaCheckpointException(e)
159
+
160
+ # Sort PIDs for ordered checkpointing
161
+ cuda_pids.sort(key=lambda x: x.pid)
162
+ return cuda_pids
163
+
164
+ def checkpoint(self) -> None:
165
+ # Validate all states first
166
+ for proc in self.cuda_processes:
167
+ if proc.state != CudaCheckpointState.RUNNING:
168
+ raise CudaCheckpointException(f"CUDA session not in {CudaCheckpointState.RUNNING} state.")
169
+
170
+ # Moving state from GPU to CPU can take several seconds per CUDA session.
171
+ # Make a parallel call per CUDA session.
172
+ start = time.perf_counter()
173
+
174
+ def checkpoint_impl(proc: CudaCheckpointProcess):
175
+ proc.toggle(CudaCheckpointState.CHECKPOINTED)
176
+
177
+ with ThreadPoolExecutor() as executor:
178
+ list(executor.map(checkpoint_impl, self.cuda_processes))
179
+
180
+ elapsed = time.perf_counter() - start
181
+ logger.debug(f"Checkpointing CUDA sessions took => {elapsed:.3f}s")
182
+
183
+ def restore(self) -> None:
184
+ # Validate all states first
185
+ for proc in self.cuda_processes:
186
+ if proc.state != CudaCheckpointState.CHECKPOINTED:
187
+ raise CudaCheckpointException(f"CUDA session not in {CudaCheckpointState.CHECKPOINTED} state.")
188
+
189
+ # See checkpoint() for rationale about parallelism.
190
+ start = time.perf_counter()
191
+
192
+ def restore_process(proc: CudaCheckpointProcess):
193
+ proc.toggle(CudaCheckpointState.RUNNING)
194
+
195
+ with ThreadPoolExecutor() as executor:
196
+ list(executor.map(restore_process, self.cuda_processes))
197
+
198
+ elapsed = time.perf_counter() - start
199
+ logger.debug(f"Restoring CUDA sessions took => {elapsed:.3f}s")
@@ -279,9 +279,7 @@ class TimestampPriorityQueue(Generic[T]):
279
279
 
280
280
  def __init__(self, maxsize: int = 0):
281
281
  self.condition = asyncio.Condition()
282
- self._queue: asyncio.PriorityQueue[tuple[float, int, Union[T, None]]] = asyncio.PriorityQueue(maxsize=maxsize)
283
- # Used to tiebreak items with the same timestamp that are not comparable. (eg. protos)
284
- self._counter = itertools.count()
282
+ self._queue: asyncio.PriorityQueue[tuple[float, Union[T, None]]] = asyncio.PriorityQueue(maxsize=maxsize)
285
283
 
286
284
  async def close(self):
287
285
  await self.put(self._MAX_PRIORITY, None)
@@ -290,7 +288,7 @@ class TimestampPriorityQueue(Generic[T]):
290
288
  """
291
289
  Add an item to the queue to be processed at a specific timestamp.
292
290
  """
293
- await self._queue.put((timestamp, next(self._counter), item))
291
+ await self._queue.put((timestamp, item))
294
292
  async with self.condition:
295
293
  self.condition.notify_all() # notify any waiting coroutines
296
294
 
@@ -303,7 +301,7 @@ class TimestampPriorityQueue(Generic[T]):
303
301
  while self.empty():
304
302
  await self.condition.wait()
305
303
  # peek at the next item
306
- timestamp, counter, item = await self._queue.get()
304
+ timestamp, item = await self._queue.get()
307
305
  now = time.time()
308
306
  if timestamp < now:
309
307
  return item
@@ -311,7 +309,7 @@ class TimestampPriorityQueue(Generic[T]):
311
309
  return None
312
310
  # not ready yet, calculate sleep time
313
311
  sleep_time = timestamp - now
314
- self._queue.put_nowait((timestamp, counter, item)) # put it back
312
+ self._queue.put_nowait((timestamp, item)) # put it back
315
313
  # wait until either the timeout or a new item is added
316
314
  try:
317
315
  await asyncio.wait_for(self.condition.wait(), timeout=sleep_time)
@@ -27,7 +27,7 @@ class _AuthTokenManager:
27
27
  self._expiry = 0.0
28
28
  self._lock: typing.Union[asyncio.Lock, None] = None
29
29
 
30
- async def get_token(self) -> str:
30
+ async def get_token(self):
31
31
  """
32
32
  When called, the AuthTokenManager can be in one of three states:
33
33
  1. Has a valid cached token. It is returned to the caller.