modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__py3-none-any.whl
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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/__init__.py
CHANGED
|
@@ -25,7 +25,6 @@ try:
|
|
|
25
25
|
from .partial_function import (
|
|
26
26
|
asgi_app,
|
|
27
27
|
batched,
|
|
28
|
-
build,
|
|
29
28
|
concurrent,
|
|
30
29
|
enter,
|
|
31
30
|
exit,
|
|
@@ -80,7 +79,6 @@ __all__ = [
|
|
|
80
79
|
"Volume",
|
|
81
80
|
"asgi_app",
|
|
82
81
|
"batched",
|
|
83
|
-
"build",
|
|
84
82
|
"concurrent",
|
|
85
83
|
"current_function_call_id",
|
|
86
84
|
"current_input_id",
|
modal/__main__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
+
from ._output import make_console
|
|
4
5
|
from ._traceback import reduce_traceback_to_user_code
|
|
5
6
|
from .cli._traceback import highlight_modal_warnings, setup_rich_traceback
|
|
6
7
|
from .cli.entry_point import entrypoint_cli
|
|
@@ -35,9 +36,7 @@ def main():
|
|
|
35
36
|
raise
|
|
36
37
|
|
|
37
38
|
from grpclib import GRPCError, Status
|
|
38
|
-
from rich.console import Console
|
|
39
39
|
from rich.panel import Panel
|
|
40
|
-
from rich.text import Text
|
|
41
40
|
|
|
42
41
|
if isinstance(exc, GRPCError):
|
|
43
42
|
status_map = {
|
|
@@ -68,8 +67,8 @@ def main():
|
|
|
68
67
|
if notes := getattr(exc, "__notes__", []):
|
|
69
68
|
content = f"{content}\n\nNote: {' '.join(notes)}"
|
|
70
69
|
|
|
71
|
-
console =
|
|
72
|
-
panel = Panel(
|
|
70
|
+
console = make_console(stderr=True)
|
|
71
|
+
panel = Panel(content, title=title, title_align="left", border_style="red")
|
|
73
72
|
console.print(panel, highlight=False)
|
|
74
73
|
sys.exit(1)
|
|
75
74
|
|
modal/_billing.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from modal_proto import api_pb2
|
|
7
|
+
|
|
8
|
+
from .client import _Client
|
|
9
|
+
from .exception import InvalidError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkspaceBillingReportItem(TypedDict):
|
|
13
|
+
object_id: str
|
|
14
|
+
description: str
|
|
15
|
+
environment_name: str
|
|
16
|
+
interval_start: datetime
|
|
17
|
+
cost: Decimal
|
|
18
|
+
tags: dict[str, str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _workspace_billing_report(
|
|
22
|
+
*,
|
|
23
|
+
start: datetime, # Start of the report, inclusive
|
|
24
|
+
end: Optional[datetime] = None, # End of the report, exclusive
|
|
25
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
26
|
+
tag_names: Optional[list[str]] = None, # Optional additional metadata to include
|
|
27
|
+
client: Optional[_Client] = None,
|
|
28
|
+
) -> list[dict[str, Any]]:
|
|
29
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
30
|
+
|
|
31
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
32
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
33
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
34
|
+
tags) for identifying that object.
|
|
35
|
+
|
|
36
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
37
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
38
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
39
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
40
|
+
partial `end` intervals will be excluded.
|
|
41
|
+
|
|
42
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
43
|
+
and `tag_names` (i.e., keys) are specified in the request. Note that tags will be attributed
|
|
44
|
+
to the entire interval even if they were added or removed at some point within it.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
if client is None:
|
|
48
|
+
client = await _Client.from_env()
|
|
49
|
+
|
|
50
|
+
tag_names = tag_names or []
|
|
51
|
+
|
|
52
|
+
if end is None:
|
|
53
|
+
end = datetime.now(timezone.utc)
|
|
54
|
+
|
|
55
|
+
for dt in (start, end):
|
|
56
|
+
if dt.tzinfo is None:
|
|
57
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
58
|
+
elif dt.tzinfo != timezone.utc:
|
|
59
|
+
raise InvalidError("Timezone-aware start/end limits must be in UTC.")
|
|
60
|
+
|
|
61
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
62
|
+
resolution=resolution,
|
|
63
|
+
tag_names=tag_names,
|
|
64
|
+
)
|
|
65
|
+
request.start_timestamp.FromDatetime(start)
|
|
66
|
+
request.end_timestamp.FromDatetime(end)
|
|
67
|
+
|
|
68
|
+
rows = []
|
|
69
|
+
async for pb_item in client.stub.WorkspaceBillingReport.unary_stream(request):
|
|
70
|
+
item = {
|
|
71
|
+
"object_id": pb_item.object_id,
|
|
72
|
+
"description": pb_item.description,
|
|
73
|
+
"environment_name": pb_item.environment_name,
|
|
74
|
+
"interval_start": pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
|
|
75
|
+
"cost": Decimal(pb_item.cost),
|
|
76
|
+
"tags": dict(pb_item.tags),
|
|
77
|
+
}
|
|
78
|
+
rows.append(item)
|
|
79
|
+
|
|
80
|
+
return rows
|
modal/_clustered_functions.py
CHANGED
|
@@ -5,7 +5,6 @@ from dataclasses import dataclass
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
from modal._utils.async_utils import synchronize_api
|
|
8
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
9
8
|
from modal.client import _Client
|
|
10
9
|
from modal.exception import InvalidError
|
|
11
10
|
from modal_proto import api_pb2
|
|
@@ -14,7 +13,9 @@ from modal_proto import api_pb2
|
|
|
14
13
|
@dataclass
|
|
15
14
|
class ClusterInfo:
|
|
16
15
|
rank: int
|
|
16
|
+
cluster_id: str
|
|
17
17
|
container_ips: list[str]
|
|
18
|
+
container_ipv4_ips: list[str]
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
cluster_info: Optional[ClusterInfo] = None
|
|
@@ -59,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
59
60
|
os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
|
|
60
61
|
|
|
61
62
|
if world_size > 1:
|
|
62
|
-
resp
|
|
63
|
-
client.stub.TaskClusterHello,
|
|
63
|
+
resp = await client.stub.TaskClusterHello(
|
|
64
64
|
api_pb2.TaskClusterHelloRequest(
|
|
65
65
|
task_id=task_id,
|
|
66
66
|
container_ip=container_ip,
|
|
@@ -68,12 +68,16 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
68
68
|
)
|
|
69
69
|
cluster_info = ClusterInfo(
|
|
70
70
|
rank=resp.cluster_rank,
|
|
71
|
+
cluster_id=resp.cluster_id,
|
|
71
72
|
container_ips=resp.container_ips,
|
|
73
|
+
container_ipv4_ips=resp.container_ipv4_ips,
|
|
72
74
|
)
|
|
73
75
|
else:
|
|
74
76
|
cluster_info = ClusterInfo(
|
|
75
77
|
rank=0,
|
|
78
|
+
cluster_id="", # No cluster ID for single-node # TODO(irfansharif): Is this right?
|
|
76
79
|
container_ips=[container_ip],
|
|
80
|
+
container_ipv4_ips=[], # No IPv4 IPs for single-node
|
|
77
81
|
)
|
|
78
82
|
|
|
79
83
|
|
modal/_clustered_functions.pyi
CHANGED
|
@@ -3,12 +3,24 @@ import typing
|
|
|
3
3
|
import typing_extensions
|
|
4
4
|
|
|
5
5
|
class ClusterInfo:
|
|
6
|
+
"""ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str])"""
|
|
7
|
+
|
|
6
8
|
rank: int
|
|
9
|
+
cluster_id: str
|
|
7
10
|
container_ips: list[str]
|
|
11
|
+
container_ipv4_ips: list[str]
|
|
12
|
+
|
|
13
|
+
def __init__(self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str]) -> None:
|
|
14
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def __repr__(self):
|
|
18
|
+
"""Return repr(self)."""
|
|
19
|
+
...
|
|
8
20
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
def __eq__(self, other):
|
|
22
|
+
"""Return self==value."""
|
|
23
|
+
...
|
|
12
24
|
|
|
13
25
|
def get_cluster_info() -> ClusterInfo: ...
|
|
14
26
|
async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
|
modal/_container_entrypoint.py
CHANGED
|
@@ -15,7 +15,6 @@ if telemetry_socket:
|
|
|
15
15
|
instrument_imports(telemetry_socket)
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
|
-
import concurrent.futures
|
|
19
18
|
import inspect
|
|
20
19
|
import queue
|
|
21
20
|
import signal
|
|
@@ -33,14 +32,14 @@ from modal._partial_function import (
|
|
|
33
32
|
_PartialFunctionFlags,
|
|
34
33
|
)
|
|
35
34
|
from modal._serialization import deserialize, deserialize_params
|
|
36
|
-
from modal._utils.async_utils import TaskContext, synchronizer
|
|
35
|
+
from modal._utils.async_utils import TaskContext, aclosing, synchronizer
|
|
37
36
|
from modal._utils.function_utils import (
|
|
38
37
|
callable_has_non_self_params,
|
|
39
38
|
)
|
|
40
39
|
from modal.app import App, _App
|
|
41
40
|
from modal.client import Client, _Client
|
|
42
41
|
from modal.config import logger
|
|
43
|
-
from modal.exception import ExecutionError, InputCancellation
|
|
42
|
+
from modal.exception import ExecutionError, InputCancellation
|
|
44
43
|
from modal.running_app import RunningApp, running_app_from_layout
|
|
45
44
|
from modal_proto import api_pb2
|
|
46
45
|
|
|
@@ -49,7 +48,6 @@ from ._runtime.container_io_manager import (
|
|
|
49
48
|
ContainerIOManager,
|
|
50
49
|
IOContext,
|
|
51
50
|
UserException,
|
|
52
|
-
_ContainerIOManager,
|
|
53
51
|
)
|
|
54
52
|
|
|
55
53
|
if TYPE_CHECKING:
|
|
@@ -186,94 +184,75 @@ def call_function(
|
|
|
186
184
|
batch_wait_ms: int,
|
|
187
185
|
):
|
|
188
186
|
async def run_input_async(io_context: IOContext) -> None:
|
|
187
|
+
reset_context = execution_context._set_current_context_ids(
|
|
188
|
+
io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
|
|
189
|
+
)
|
|
189
190
|
started_at = time.time()
|
|
190
|
-
input_ids, function_call_ids = io_context.input_ids, io_context.function_call_ids
|
|
191
|
-
reset_context = execution_context._set_current_context_ids(input_ids, function_call_ids)
|
|
192
191
|
async with container_io_manager.handle_input_exception.aio(io_context, started_at):
|
|
193
|
-
res = io_context.call_finalized_function()
|
|
194
192
|
# TODO(erikbern): any exception below shouldn't be considered a user exception
|
|
195
193
|
if io_context.finalized_function.is_generator:
|
|
196
|
-
if not inspect.isasyncgen(res):
|
|
197
|
-
raise InvalidError(f"Async generator function returned value of type {type(res)}")
|
|
198
|
-
|
|
199
194
|
# Send up to this many outputs at a time.
|
|
195
|
+
current_function_call_id = execution_context.current_function_call_id()
|
|
196
|
+
assert current_function_call_id is not None # Set above.
|
|
197
|
+
current_attempt_token = execution_context.current_attempt_token()
|
|
198
|
+
assert current_attempt_token is not None # Set above, but can be empty string.
|
|
200
199
|
generator_queue: asyncio.Queue[Any] = await container_io_manager._queue_create.aio(1024)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await generator_output_task # Wait to finish sending generator outputs.
|
|
216
|
-
message = api_pb2.GeneratorDone(items_total=item_count)
|
|
217
|
-
await container_io_manager.push_outputs.aio(
|
|
218
|
-
io_context,
|
|
219
|
-
started_at,
|
|
220
|
-
message,
|
|
221
|
-
api_pb2.DATA_FORMAT_GENERATOR_DONE,
|
|
200
|
+
async with container_io_manager.generator_output_sender(
|
|
201
|
+
current_function_call_id,
|
|
202
|
+
current_attempt_token,
|
|
203
|
+
io_context._generator_output_format(),
|
|
204
|
+
generator_queue,
|
|
205
|
+
):
|
|
206
|
+
item_count = 0
|
|
207
|
+
async with aclosing(io_context.call_generator_async()) as gen:
|
|
208
|
+
async for value in gen:
|
|
209
|
+
await container_io_manager._queue_put.aio(generator_queue, value)
|
|
210
|
+
item_count += 1
|
|
211
|
+
|
|
212
|
+
await container_io_manager._send_outputs.aio(
|
|
213
|
+
started_at, io_context.output_items_generator_done(started_at, item_count)
|
|
222
214
|
)
|
|
223
215
|
else:
|
|
224
|
-
|
|
225
|
-
raise InvalidError(
|
|
226
|
-
f"Async (non-generator) function returned value of type {type(res)}"
|
|
227
|
-
" You might need to use @app.function(..., is_generator=True)."
|
|
228
|
-
)
|
|
229
|
-
value = await res
|
|
216
|
+
value = await io_context.call_function_async()
|
|
230
217
|
await container_io_manager.push_outputs.aio(
|
|
231
218
|
io_context,
|
|
232
219
|
started_at,
|
|
233
220
|
value,
|
|
234
|
-
io_context.finalized_function.data_format,
|
|
235
221
|
)
|
|
236
222
|
reset_context()
|
|
237
223
|
|
|
238
224
|
def run_input_sync(io_context: IOContext) -> None:
|
|
239
225
|
started_at = time.time()
|
|
240
|
-
|
|
241
|
-
|
|
226
|
+
reset_context = execution_context._set_current_context_ids(
|
|
227
|
+
io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
|
|
228
|
+
)
|
|
242
229
|
with container_io_manager.handle_input_exception(io_context, started_at):
|
|
243
|
-
res = io_context.call_finalized_function()
|
|
244
|
-
|
|
245
230
|
# TODO(erikbern): any exception below shouldn't be considered a user exception
|
|
246
231
|
if io_context.finalized_function.is_generator:
|
|
247
|
-
|
|
248
|
-
raise InvalidError(f"Generator function returned value of type {type(res)}")
|
|
249
|
-
|
|
232
|
+
gen = io_context.call_generator_sync()
|
|
250
233
|
# Send up to this many outputs at a time.
|
|
234
|
+
current_function_call_id = execution_context.current_function_call_id()
|
|
235
|
+
assert current_function_call_id is not None # Set above.
|
|
236
|
+
current_attempt_token = execution_context.current_attempt_token()
|
|
237
|
+
assert current_attempt_token is not None # Set above, but can be empty string.
|
|
251
238
|
generator_queue: asyncio.Queue[Any] = container_io_manager._queue_create(1024)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
239
|
+
with container_io_manager.generator_output_sender(
|
|
240
|
+
current_function_call_id,
|
|
241
|
+
current_attempt_token,
|
|
242
|
+
io_context._generator_output_format(),
|
|
255
243
|
generator_queue,
|
|
256
|
-
|
|
244
|
+
):
|
|
245
|
+
item_count = 0
|
|
246
|
+
for value in gen:
|
|
247
|
+
container_io_manager._queue_put(generator_queue, value)
|
|
248
|
+
item_count += 1
|
|
249
|
+
|
|
250
|
+
container_io_manager._send_outputs(
|
|
251
|
+
started_at, io_context.output_items_generator_done(started_at, item_count)
|
|
257
252
|
)
|
|
258
|
-
|
|
259
|
-
item_count = 0
|
|
260
|
-
for value in res:
|
|
261
|
-
container_io_manager._queue_put(generator_queue, value)
|
|
262
|
-
item_count += 1
|
|
263
|
-
|
|
264
|
-
container_io_manager._queue_put(generator_queue, _ContainerIOManager._GENERATOR_STOP_SENTINEL)
|
|
265
|
-
generator_output_task.result() # Wait to finish sending generator outputs.
|
|
266
|
-
message = api_pb2.GeneratorDone(items_total=item_count)
|
|
267
|
-
container_io_manager.push_outputs(io_context, started_at, message, api_pb2.DATA_FORMAT_GENERATOR_DONE)
|
|
268
253
|
else:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
f"Sync (non-generator) function return value of type {type(res)}."
|
|
272
|
-
" You might need to use @app.function(..., is_generator=True)."
|
|
273
|
-
)
|
|
274
|
-
container_io_manager.push_outputs(
|
|
275
|
-
io_context, started_at, res, io_context.finalized_function.data_format
|
|
276
|
-
)
|
|
254
|
+
values = io_context.call_function_sync()
|
|
255
|
+
container_io_manager.push_outputs(io_context, started_at, values)
|
|
277
256
|
reset_context()
|
|
278
257
|
|
|
279
258
|
if container_io_manager.input_concurrency_enabled:
|
|
@@ -436,9 +415,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
436
415
|
param_kwargs,
|
|
437
416
|
)
|
|
438
417
|
else:
|
|
418
|
+
assert ser_usr_cls is None
|
|
439
419
|
service = import_single_function_service(
|
|
440
420
|
function_def,
|
|
441
|
-
ser_usr_cls,
|
|
442
421
|
ser_fun,
|
|
443
422
|
)
|
|
444
423
|
|
|
@@ -471,7 +450,10 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
471
450
|
f"Function has {len(service.service_deps)} dependencies"
|
|
472
451
|
f" but container got {len(dep_object_ids)} object ids.\n"
|
|
473
452
|
f"Code deps: {service.service_deps}\n"
|
|
474
|
-
f"Object ids: {dep_object_ids}"
|
|
453
|
+
f"Object ids: {dep_object_ids}\n"
|
|
454
|
+
"\n"
|
|
455
|
+
"This can happen if you are defining Modal objects under a conditional statement "
|
|
456
|
+
"that evaluates differently in the local and remote environments."
|
|
475
457
|
)
|
|
476
458
|
for object_id, obj in zip(dep_object_ids, service.service_deps):
|
|
477
459
|
metadata: Message = container_app.object_handle_metadata[object_id]
|