modAL 1.1.1__tar.gz → 1.1.1.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.1.1 → modal-1.1.1.dev1}/PKG-INFO +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/__main__.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_clustered_functions.py +0 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_clustered_functions.pyi +2 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_functions.py +26 -78
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_object.py +1 -9
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_output.py +25 -14
- modal-1.1.1.dev1/modal/_runtime/gpu_memory_snapshot.py +199 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/async_utils.py +4 -6
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/auth_token_manager.py +1 -1
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/blob_utils.py +21 -16
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/function_utils.py +4 -16
- modal-1.1.1.dev1/modal/_utils/time_utils.py +15 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/app.py +4 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/app.pyi +4 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/_traceback.py +2 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/app.py +4 -4
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/cluster.py +4 -4
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/config.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/container.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/dict.py +4 -4
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/entry_point.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/import_refs.py +3 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/network_file_system.py +9 -8
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/profile.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/queues.py +5 -5
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/secret.py +5 -5
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/utils.py +4 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/volume.py +9 -8
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/client.py +1 -8
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/client.pyi +10 -9
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/container_process.py +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/dict.py +3 -47
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/dict.pyi +0 -55
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/exception.py +0 -4
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/experimental/__init__.py +1 -1
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/experimental/flash.py +2 -18
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/experimental/flash.pyi +0 -19
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/functions.pyi +1 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/image.py +10 -26
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/image.pyi +4 -12
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/mount.py +1 -1
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/object.pyi +0 -4
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/parallel_map.py +4 -432
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/parallel_map.pyi +0 -28
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/queue.py +3 -46
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/queue.pyi +0 -53
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/sandbox.py +25 -105
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/sandbox.pyi +18 -108
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/secret.py +5 -48
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/secret.pyi +0 -55
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/token_flow.py +3 -3
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/volume.py +18 -49
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/volume.pyi +8 -50
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/SOURCES.txt +8 -8
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/requires.txt +1 -1
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api.proto +14 -140
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api_grpc.py +0 -80
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api_pb2.py +756 -927
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api_pb2.pyi +34 -488
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api_pb2_grpc.py +0 -166
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/api_pb2_grpc.pyi +0 -52
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/modal_api_grpc.py +0 -5
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_version/__init__.py +1 -1
- {modal-1.1.1 → modal-1.1.1.dev1}/pyproject.toml +2 -2
- modal-1.1.1/modal/_runtime/gpu_memory_snapshot.py +0 -303
- modal-1.1.1/modal/_utils/time_utils.py +0 -19
- {modal-1.1.1 → modal-1.1.1.dev1}/LICENSE +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/README.md +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_container_entrypoint.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_ipython.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_location.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_partial_function.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_pty.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_resolver.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_resources.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/asgi.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_serialization.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_traceback.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_tunnel.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_tunnel.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_type_manager.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/app_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/deprecation.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/git_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/http_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/logger.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/name_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/package_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_vendor/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_vendor/tblib.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/_watcher.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/call_graph.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/_download.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/environment.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/launch.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/run.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cli/token.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cls.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/cls.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/config.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/container_process.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/environments.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/environments.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/experimental/ipython.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/file_io.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/file_io.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/functions.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/gpu.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/io_streams.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/io_streams.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/mount.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/network_file_system.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/network_file_system.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/object.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/output.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/partial_function.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/partial_function.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/proxy.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/proxy.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/py.typed +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/2023.12.312.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/2023.12.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/2024.04.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/2024.10.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/2025.06.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/PREVIEW.txt +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/README.md +0 -0
- {modal-1.1.1/modal/builder → modal-1.1.1.dev1/modal/requirements}/base-images.json +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/retries.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/runner.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/runner.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/running_app.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/schedule.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/scheduler_placement.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/serving.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/serving.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/snapshot.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/snapshot.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/stream_type.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal/token_flow.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/__init__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/modal_options_grpc.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options.proto +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options_grpc.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options_pb2.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options_pb2.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options_pb2_grpc.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/options_pb2_grpc.pyi +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_proto/py.typed +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/modal_version/__main__.py +0 -0
- {modal-1.1.1 → modal-1.1.1.dev1}/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.dev1
|
|
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 =
|
|
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]
|
|
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]
|
|
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.
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
|
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
|
|
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"{
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|
|
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.
|