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/_functions.py
CHANGED
|
@@ -6,21 +6,21 @@ import textwrap
|
|
|
6
6
|
import time
|
|
7
7
|
import typing
|
|
8
8
|
import warnings
|
|
9
|
-
from collections.abc import AsyncGenerator, Sequence, Sized
|
|
9
|
+
from collections.abc import AsyncGenerator, Collection, Sequence, Sized
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import PurePosixPath
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
12
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
|
|
13
13
|
|
|
14
14
|
import typing_extensions
|
|
15
15
|
from google.protobuf.message import Message
|
|
16
16
|
from grpclib import GRPCError, Status
|
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
|
18
|
-
from synchronicity.exceptions import UserCodeException
|
|
19
18
|
|
|
20
19
|
from modal_proto import api_pb2
|
|
21
20
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
22
21
|
|
|
23
|
-
from .
|
|
22
|
+
from ._load_context import LoadContext
|
|
23
|
+
from ._object import _Object, live_method, live_method_gen
|
|
24
24
|
from ._pty import get_pty_info
|
|
25
25
|
from ._resolver import Resolver
|
|
26
26
|
from ._resources import convert_fn_config_to_resources_config
|
|
@@ -41,21 +41,20 @@ from ._utils.async_utils import (
|
|
|
41
41
|
synchronizer,
|
|
42
42
|
warn_if_generator_is_not_consumed,
|
|
43
43
|
)
|
|
44
|
-
from ._utils.
|
|
44
|
+
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
45
|
+
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
45
46
|
from ._utils.function_utils import (
|
|
46
47
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
47
48
|
OUTPUTS_TIMEOUT,
|
|
48
49
|
FunctionCreationStatus,
|
|
49
50
|
FunctionInfo,
|
|
50
|
-
IncludeSourceMode,
|
|
51
51
|
_create_input,
|
|
52
52
|
_process_result,
|
|
53
53
|
_stream_function_call_data,
|
|
54
54
|
get_function_type,
|
|
55
|
-
get_include_source_mode,
|
|
56
55
|
is_async,
|
|
57
56
|
)
|
|
58
|
-
from ._utils.grpc_utils import
|
|
57
|
+
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
59
58
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
60
59
|
from .call_graph import InputInfo, _reconstruct_call_graph
|
|
61
60
|
from .client import _Client
|
|
@@ -63,8 +62,6 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
|
63
62
|
from .config import config
|
|
64
63
|
from .exception import (
|
|
65
64
|
ExecutionError,
|
|
66
|
-
FunctionTimeoutError,
|
|
67
|
-
InternalFailure,
|
|
68
65
|
InvalidError,
|
|
69
66
|
NotFoundError,
|
|
70
67
|
OutputExpiredError,
|
|
@@ -75,12 +72,16 @@ from .mount import _get_client_mount, _Mount
|
|
|
75
72
|
from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
|
|
76
73
|
from .output import _get_output_manager
|
|
77
74
|
from .parallel_map import (
|
|
75
|
+
_experimental_spawn_map_async,
|
|
76
|
+
_experimental_spawn_map_sync,
|
|
78
77
|
_for_each_async,
|
|
79
78
|
_for_each_sync,
|
|
80
79
|
_map_async,
|
|
81
80
|
_map_invocation,
|
|
81
|
+
_map_invocation_inputplane,
|
|
82
82
|
_map_sync,
|
|
83
83
|
_spawn_map_async,
|
|
84
|
+
_spawn_map_invocation,
|
|
84
85
|
_spawn_map_sync,
|
|
85
86
|
_starmap_async,
|
|
86
87
|
_starmap_sync,
|
|
@@ -94,12 +95,14 @@ from .secret import _Secret
|
|
|
94
95
|
from .volume import _Volume
|
|
95
96
|
|
|
96
97
|
if TYPE_CHECKING:
|
|
97
|
-
import modal._partial_function
|
|
98
98
|
import modal.app
|
|
99
99
|
import modal.cls
|
|
100
|
-
import modal.partial_function
|
|
101
100
|
|
|
102
101
|
MAX_INTERNAL_FAILURE_COUNT = 8
|
|
102
|
+
TERMINAL_STATUSES = (
|
|
103
|
+
api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
|
|
104
|
+
api_pb2.GenericResult.GENERIC_STATUS_TERMINATED,
|
|
105
|
+
)
|
|
103
106
|
|
|
104
107
|
|
|
105
108
|
@dataclasses.dataclass
|
|
@@ -144,7 +147,13 @@ class _Invocation:
|
|
|
144
147
|
stub = client.stub
|
|
145
148
|
|
|
146
149
|
function_id = function.object_id
|
|
147
|
-
item = await _create_input(
|
|
150
|
+
item = await _create_input(
|
|
151
|
+
args,
|
|
152
|
+
kwargs,
|
|
153
|
+
stub,
|
|
154
|
+
function=function,
|
|
155
|
+
function_call_invocation_type=function_call_invocation_type,
|
|
156
|
+
)
|
|
148
157
|
|
|
149
158
|
request = api_pb2.FunctionMapRequest(
|
|
150
159
|
function_id=function_id,
|
|
@@ -156,21 +165,22 @@ class _Invocation:
|
|
|
156
165
|
|
|
157
166
|
if from_spawn_map:
|
|
158
167
|
request.from_spawn_map = True
|
|
159
|
-
response = await
|
|
160
|
-
client.stub.FunctionMap,
|
|
168
|
+
response = await client.stub.FunctionMap(
|
|
161
169
|
request,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
retry=Retry(
|
|
171
|
+
max_retries=None,
|
|
172
|
+
max_delay=30.0,
|
|
173
|
+
warning_message=RetryWarningMessage(
|
|
174
|
+
message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
|
|
175
|
+
"more function calls. This may be due to hitting rate limits or function backlog limits.",
|
|
176
|
+
warning_interval=10,
|
|
177
|
+
errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
|
|
178
|
+
),
|
|
179
|
+
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
169
180
|
),
|
|
170
|
-
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
171
181
|
)
|
|
172
182
|
else:
|
|
173
|
-
response = await
|
|
183
|
+
response = await client.stub.FunctionMap(request)
|
|
174
184
|
|
|
175
185
|
function_call_id = response.function_call_id
|
|
176
186
|
if response.pipelined_inputs:
|
|
@@ -190,10 +200,7 @@ class _Invocation:
|
|
|
190
200
|
request_put = api_pb2.FunctionPutInputsRequest(
|
|
191
201
|
function_id=function_id, inputs=[item], function_call_id=function_call_id
|
|
192
202
|
)
|
|
193
|
-
inputs_response: api_pb2.FunctionPutInputsResponse = await
|
|
194
|
-
client.stub.FunctionPutInputs,
|
|
195
|
-
request_put,
|
|
196
|
-
)
|
|
203
|
+
inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
|
|
197
204
|
processed_inputs = inputs_response.inputs
|
|
198
205
|
if not processed_inputs:
|
|
199
206
|
raise Exception("Could not create function call - the input queue seems to be full")
|
|
@@ -210,7 +217,11 @@ class _Invocation:
|
|
|
210
217
|
return _Invocation(stub, function_call_id, client, retry_context)
|
|
211
218
|
|
|
212
219
|
async def pop_function_call_outputs(
|
|
213
|
-
self,
|
|
220
|
+
self,
|
|
221
|
+
index: int = 0,
|
|
222
|
+
timeout: Optional[float] = None,
|
|
223
|
+
clear_on_success: bool = False,
|
|
224
|
+
input_jwts: Optional[list[str]] = None,
|
|
214
225
|
) -> api_pb2.FunctionGetOutputsResponse:
|
|
215
226
|
t0 = time.time()
|
|
216
227
|
if timeout is None:
|
|
@@ -228,11 +239,12 @@ class _Invocation:
|
|
|
228
239
|
clear_on_success=clear_on_success,
|
|
229
240
|
requested_at=time.time(),
|
|
230
241
|
input_jwts=input_jwts,
|
|
242
|
+
start_idx=index,
|
|
243
|
+
end_idx=index,
|
|
231
244
|
)
|
|
232
|
-
response: api_pb2.FunctionGetOutputsResponse = await
|
|
233
|
-
self.stub.FunctionGetOutputs,
|
|
245
|
+
response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
|
|
234
246
|
request,
|
|
235
|
-
attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
247
|
+
retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
|
|
236
248
|
)
|
|
237
249
|
|
|
238
250
|
if len(response.outputs) > 0:
|
|
@@ -252,21 +264,19 @@ class _Invocation:
|
|
|
252
264
|
|
|
253
265
|
item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
|
|
254
266
|
request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
|
|
255
|
-
await
|
|
256
|
-
self.stub.FunctionRetryInputs,
|
|
257
|
-
request,
|
|
258
|
-
)
|
|
267
|
+
await self.stub.FunctionRetryInputs(request)
|
|
259
268
|
|
|
260
|
-
async def _get_single_output(self, expected_jwt: Optional[str] = None) ->
|
|
269
|
+
async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
|
|
261
270
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
|
262
271
|
item: api_pb2.FunctionGetOutputsItem = (
|
|
263
272
|
await self.pop_function_call_outputs(
|
|
273
|
+
index=0,
|
|
264
274
|
timeout=None,
|
|
265
275
|
clear_on_success=True,
|
|
266
276
|
input_jwts=[expected_jwt] if expected_jwt else None,
|
|
267
277
|
)
|
|
268
278
|
).outputs[0]
|
|
269
|
-
return
|
|
279
|
+
return item
|
|
270
280
|
|
|
271
281
|
async def run_function(self) -> Any:
|
|
272
282
|
# Use retry logic only if retry policy is specified and
|
|
@@ -278,33 +288,38 @@ class _Invocation:
|
|
|
278
288
|
or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
|
279
289
|
or not ctx.sync_client_retries_enabled
|
|
280
290
|
):
|
|
281
|
-
|
|
291
|
+
item = await self._get_single_output()
|
|
292
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
|
282
293
|
|
|
283
294
|
# User errors including timeouts are managed by the user specified retry policy.
|
|
284
295
|
user_retry_manager = RetryManager(ctx.retry_policy)
|
|
285
296
|
|
|
286
297
|
while True:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
item = await self._get_single_output(ctx.input_jwt)
|
|
299
|
+
if item.result.status in TERMINAL_STATUSES:
|
|
300
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
|
301
|
+
|
|
302
|
+
if item.result.status != api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
|
|
303
|
+
# non-internal failures get a delay before retrying
|
|
290
304
|
delay_ms = user_retry_manager.get_delay_ms()
|
|
291
305
|
if delay_ms is None:
|
|
292
|
-
raise
|
|
306
|
+
# no more retries, this should raise an error when the non-success status is converted
|
|
307
|
+
# to an exception:
|
|
308
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
|
293
309
|
await asyncio.sleep(delay_ms / 1000)
|
|
294
|
-
|
|
295
|
-
# For system failures on the server, we retry immediately,
|
|
296
|
-
# and the failure does not count towards the retry policy.
|
|
297
|
-
pass
|
|
310
|
+
|
|
298
311
|
await self._retry_input()
|
|
299
312
|
|
|
300
|
-
async def poll_function(self, timeout: Optional[float] = None):
|
|
313
|
+
async def poll_function(self, timeout: Optional[float] = None, *, index: int = 0):
|
|
301
314
|
"""Waits up to timeout for a result from a function.
|
|
302
315
|
|
|
303
316
|
If timeout is `None`, waits indefinitely. This function is not
|
|
304
317
|
cancellation-safe.
|
|
305
318
|
"""
|
|
306
319
|
response: api_pb2.FunctionGetOutputsResponse = await self.pop_function_call_outputs(
|
|
307
|
-
|
|
320
|
+
index=index,
|
|
321
|
+
timeout=timeout,
|
|
322
|
+
clear_on_success=False,
|
|
308
323
|
)
|
|
309
324
|
if len(response.outputs) == 0 and response.num_unfinished_inputs == 0:
|
|
310
325
|
# if no unfinished inputs and no outputs, then function expired
|
|
@@ -322,7 +337,7 @@ class _Invocation:
|
|
|
322
337
|
items_total: Union[int, None] = None
|
|
323
338
|
async with aclosing(
|
|
324
339
|
async_merge(
|
|
325
|
-
_stream_function_call_data(self.client, self.function_call_id, variant="data_out"),
|
|
340
|
+
_stream_function_call_data(self.client, None, self.function_call_id, variant="data_out"),
|
|
326
341
|
callable_to_agen(self.run_function),
|
|
327
342
|
)
|
|
328
343
|
) as streamer:
|
|
@@ -337,11 +352,45 @@ class _Invocation:
|
|
|
337
352
|
if items_total is not None and items_received >= items_total:
|
|
338
353
|
break
|
|
339
354
|
|
|
355
|
+
async def enumerate(self, start_index: int, end_index: int):
|
|
356
|
+
"""Iterate over the results of the function call in the range [start_index, end_index)."""
|
|
357
|
+
limit = 49
|
|
358
|
+
current_index = start_index
|
|
359
|
+
while current_index < end_index:
|
|
360
|
+
# batch_end_indx is inclusive, so we subtract 1 to get the last index in the batch.
|
|
361
|
+
batch_end_index = min(current_index + limit, end_index) - 1
|
|
362
|
+
request = api_pb2.FunctionGetOutputsRequest(
|
|
363
|
+
function_call_id=self.function_call_id,
|
|
364
|
+
timeout=0,
|
|
365
|
+
last_entry_id="0-0",
|
|
366
|
+
clear_on_success=False,
|
|
367
|
+
requested_at=time.time(),
|
|
368
|
+
start_idx=current_index,
|
|
369
|
+
end_idx=batch_end_index,
|
|
370
|
+
)
|
|
371
|
+
response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
|
|
372
|
+
request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
outputs = list(response.outputs)
|
|
376
|
+
outputs.sort(key=lambda x: x.idx)
|
|
377
|
+
for output in outputs:
|
|
378
|
+
if output.idx != current_index:
|
|
379
|
+
break
|
|
380
|
+
result = await _process_result(output.result, output.data_format, self.stub, self.client)
|
|
381
|
+
yield output.idx, result
|
|
382
|
+
current_index += 1
|
|
383
|
+
|
|
384
|
+
# We're missing current_index, so we need to poll the function for the next result
|
|
385
|
+
if len(outputs) < (batch_end_index - current_index + 1):
|
|
386
|
+
result = await self.poll_function(index=current_index)
|
|
387
|
+
yield current_index, result
|
|
388
|
+
current_index += 1
|
|
389
|
+
|
|
340
390
|
|
|
341
391
|
class _InputPlaneInvocation:
|
|
342
392
|
"""Internal client representation of a single-input call to a Modal Function using the input
|
|
343
|
-
plane server API.
|
|
344
|
-
It is OK to make breaking changes to this class."""
|
|
393
|
+
plane server API."""
|
|
345
394
|
|
|
346
395
|
stub: ModalClientModal
|
|
347
396
|
|
|
@@ -352,12 +401,16 @@ class _InputPlaneInvocation:
|
|
|
352
401
|
client: _Client,
|
|
353
402
|
input_item: api_pb2.FunctionPutInputsItem,
|
|
354
403
|
function_id: str,
|
|
404
|
+
retry_policy: api_pb2.FunctionRetryPolicy,
|
|
405
|
+
input_plane_region: str,
|
|
355
406
|
):
|
|
356
407
|
self.stub = stub
|
|
357
408
|
self.client = client # Used by the deserializer.
|
|
358
409
|
self.attempt_token = attempt_token
|
|
359
410
|
self.input_item = input_item
|
|
360
411
|
self.function_id = function_id
|
|
412
|
+
self.retry_policy = retry_policy
|
|
413
|
+
self.input_plane_region = input_plane_region
|
|
361
414
|
|
|
362
415
|
@staticmethod
|
|
363
416
|
async def create(
|
|
@@ -367,25 +420,39 @@ class _InputPlaneInvocation:
|
|
|
367
420
|
*,
|
|
368
421
|
client: _Client,
|
|
369
422
|
input_plane_url: str,
|
|
423
|
+
input_plane_region: str,
|
|
370
424
|
) -> "_InputPlaneInvocation":
|
|
371
425
|
stub = await client.get_stub(input_plane_url)
|
|
372
426
|
|
|
373
427
|
function_id = function.object_id
|
|
374
|
-
|
|
428
|
+
control_plane_stub = client.stub
|
|
429
|
+
# Note: Blob upload is done on the control plane stub, not the input plane stub!
|
|
430
|
+
input_item = await _create_input(
|
|
431
|
+
args,
|
|
432
|
+
kwargs,
|
|
433
|
+
control_plane_stub,
|
|
434
|
+
function=function,
|
|
435
|
+
)
|
|
375
436
|
|
|
376
437
|
request = api_pb2.AttemptStartRequest(
|
|
377
438
|
function_id=function_id,
|
|
378
439
|
parent_input_id=current_input_id() or "",
|
|
379
440
|
input=input_item,
|
|
380
441
|
)
|
|
381
|
-
|
|
442
|
+
|
|
443
|
+
metadata = await client.get_input_plane_metadata(input_plane_region)
|
|
444
|
+
response = await stub.AttemptStart(request, metadata=metadata)
|
|
382
445
|
attempt_token = response.attempt_token
|
|
383
446
|
|
|
384
|
-
return _InputPlaneInvocation(
|
|
447
|
+
return _InputPlaneInvocation(
|
|
448
|
+
stub, attempt_token, client, input_item, function_id, response.retry_policy, input_plane_region
|
|
449
|
+
)
|
|
385
450
|
|
|
386
451
|
async def run_function(self) -> Any:
|
|
452
|
+
# User errors including timeouts are managed by the user-specified retry policy.
|
|
453
|
+
user_retry_manager = RetryManager(self.retry_policy)
|
|
454
|
+
|
|
387
455
|
# This will retry when the server returns GENERIC_STATUS_INTERNAL_FAILURE, i.e. lost inputs or worker preemption
|
|
388
|
-
# TODO(ryan): add logic to retry for user defined retry policy
|
|
389
456
|
internal_failure_count = 0
|
|
390
457
|
while True:
|
|
391
458
|
await_request = api_pb2.AttemptAwaitRequest(
|
|
@@ -393,33 +460,85 @@ class _InputPlaneInvocation:
|
|
|
393
460
|
timeout_secs=OUTPUTS_TIMEOUT,
|
|
394
461
|
requested_at=time.time(),
|
|
395
462
|
)
|
|
396
|
-
|
|
397
|
-
|
|
463
|
+
metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
|
|
464
|
+
await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
|
|
398
465
|
await_request,
|
|
399
|
-
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
466
|
+
retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
|
|
467
|
+
metadata=metadata,
|
|
400
468
|
)
|
|
401
469
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
470
|
+
# Keep awaiting until we get an output.
|
|
471
|
+
if not await_response.HasField("output"):
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
# If we have a final output, return.
|
|
475
|
+
if await_response.output.result.status in TERMINAL_STATUSES:
|
|
476
|
+
return await _process_result(
|
|
477
|
+
await_response.output.result, await_response.output.data_format, self.client.stub, self.client
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# We have a failure (internal or application), so see if there are any retries left, and if so, retry.
|
|
481
|
+
if await_response.output.result.status == api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
|
|
408
482
|
internal_failure_count += 1
|
|
409
|
-
# Limit the number of times we retry
|
|
410
|
-
if internal_failure_count
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
483
|
+
# Limit the number of times we retry internal failures.
|
|
484
|
+
if internal_failure_count < MAX_INTERNAL_FAILURE_COUNT:
|
|
485
|
+
# We immediately retry internal failures and the failure doesn't count towards the retry policy.
|
|
486
|
+
self.attempt_token = await self._retry_input(metadata)
|
|
487
|
+
continue
|
|
488
|
+
elif (delay_ms := user_retry_manager.get_delay_ms()) is not None:
|
|
489
|
+
# We still have user retries left, so sleep and retry.
|
|
490
|
+
await asyncio.sleep(delay_ms / 1000)
|
|
491
|
+
self.attempt_token = await self._retry_input(metadata)
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
# No more retries left.
|
|
495
|
+
return await _process_result(
|
|
496
|
+
await_response.output.result, await_response.output.data_format, self.client.stub, self.client
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
async def _retry_input(self, metadata: list[tuple[str, str]]) -> str:
|
|
500
|
+
retry_request = api_pb2.AttemptRetryRequest(
|
|
501
|
+
function_id=self.function_id,
|
|
502
|
+
parent_input_id=current_input_id() or "",
|
|
503
|
+
input=self.input_item,
|
|
504
|
+
attempt_token=self.attempt_token,
|
|
505
|
+
)
|
|
506
|
+
retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
|
|
507
|
+
return retry_response.attempt_token
|
|
508
|
+
|
|
509
|
+
async def run_generator(self):
|
|
510
|
+
items_received = 0
|
|
511
|
+
# populated when self.run_function() completes
|
|
512
|
+
items_total: Union[int, None] = None
|
|
513
|
+
async with aclosing(
|
|
514
|
+
async_merge(
|
|
515
|
+
_stream_function_call_data(
|
|
516
|
+
self.client,
|
|
517
|
+
self.stub,
|
|
518
|
+
function_call_id=None,
|
|
519
|
+
variant="data_out",
|
|
418
520
|
attempt_token=self.attempt_token,
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
521
|
+
),
|
|
522
|
+
callable_to_agen(self.run_function),
|
|
523
|
+
)
|
|
524
|
+
) as streamer:
|
|
525
|
+
async for item in streamer:
|
|
526
|
+
if isinstance(item, api_pb2.GeneratorDone):
|
|
527
|
+
items_total = item.items_total
|
|
528
|
+
else:
|
|
529
|
+
yield item
|
|
530
|
+
items_received += 1
|
|
531
|
+
# The comparison avoids infinite loops if a non-deterministic generator is retried
|
|
532
|
+
# and produces less data in the second run than what was already sent.
|
|
533
|
+
if items_total is not None and items_received >= items_total:
|
|
534
|
+
break
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
|
|
538
|
+
if not input_plane_region:
|
|
539
|
+
return []
|
|
540
|
+
token = await client._auth_token_manager.get_token()
|
|
541
|
+
return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
|
|
423
542
|
|
|
424
543
|
|
|
425
544
|
# Wrapper type for api_pb2.FunctionStats
|
|
@@ -461,7 +580,7 @@ class _FunctionSpec:
|
|
|
461
580
|
|
|
462
581
|
image: Optional[_Image]
|
|
463
582
|
mounts: Sequence[_Mount]
|
|
464
|
-
secrets:
|
|
583
|
+
secrets: Collection[_Secret]
|
|
465
584
|
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
|
|
466
585
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
|
467
586
|
# TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
|
@@ -474,6 +593,21 @@ class _FunctionSpec:
|
|
|
474
593
|
proxy: Optional[_Proxy]
|
|
475
594
|
|
|
476
595
|
|
|
596
|
+
def _get_supported_input_output_formats(is_web_endpoint: bool, is_generator: bool, restrict_output: bool):
|
|
597
|
+
if is_web_endpoint:
|
|
598
|
+
supported_input_formats = [api_pb2.DATA_FORMAT_ASGI]
|
|
599
|
+
supported_output_formats = [api_pb2.DATA_FORMAT_ASGI, api_pb2.DATA_FORMAT_GENERATOR_DONE]
|
|
600
|
+
else:
|
|
601
|
+
supported_input_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
|
|
602
|
+
if restrict_output:
|
|
603
|
+
supported_output_formats = [api_pb2.DATA_FORMAT_CBOR]
|
|
604
|
+
else:
|
|
605
|
+
supported_output_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
|
|
606
|
+
if is_generator:
|
|
607
|
+
supported_output_formats.append(api_pb2.DATA_FORMAT_GENERATOR_DONE)
|
|
608
|
+
return supported_input_formats, supported_output_formats
|
|
609
|
+
|
|
610
|
+
|
|
477
611
|
P = typing_extensions.ParamSpec("P")
|
|
478
612
|
ReturnType = typing.TypeVar("ReturnType", covariant=True)
|
|
479
613
|
OriginalReturnType = typing.TypeVar(
|
|
@@ -523,9 +657,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
523
657
|
@staticmethod
|
|
524
658
|
def from_local(
|
|
525
659
|
info: FunctionInfo,
|
|
526
|
-
app,
|
|
660
|
+
app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
|
|
527
661
|
image: _Image,
|
|
528
|
-
|
|
662
|
+
env: Optional[dict[str, Optional[str]]] = None,
|
|
663
|
+
secrets: Optional[Collection[_Secret]] = None,
|
|
529
664
|
schedule: Optional[Schedule] = None,
|
|
530
665
|
is_generator: bool = False,
|
|
531
666
|
gpu: Union[GPU_T, list[GPU_T]] = None,
|
|
@@ -537,7 +672,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
537
672
|
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
538
673
|
proxy: Optional[_Proxy] = None,
|
|
539
674
|
retries: Optional[Union[int, Retries]] = None,
|
|
540
|
-
timeout:
|
|
675
|
+
timeout: int = 300,
|
|
676
|
+
startup_timeout: Optional[int] = None,
|
|
541
677
|
min_containers: Optional[int] = None,
|
|
542
678
|
max_containers: Optional[int] = None,
|
|
543
679
|
buffer_containers: Optional[int] = None,
|
|
@@ -559,14 +695,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
559
695
|
rdma: Optional[bool] = None,
|
|
560
696
|
max_inputs: Optional[int] = None,
|
|
561
697
|
ephemeral_disk: Optional[int] = None,
|
|
562
|
-
|
|
563
|
-
include_source: Optional[bool] = None,
|
|
698
|
+
include_source: bool = True,
|
|
564
699
|
experimental_options: Optional[dict[str, str]] = None,
|
|
565
700
|
_experimental_proxy_ip: Optional[str] = None,
|
|
566
701
|
_experimental_custom_scaling_factor: Optional[float] = None,
|
|
567
|
-
|
|
702
|
+
restrict_output: bool = False,
|
|
568
703
|
) -> "_Function":
|
|
569
|
-
"""mdmd:hidden
|
|
704
|
+
"""mdmd:hidden
|
|
705
|
+
|
|
706
|
+
Note: This is not intended to be public API.
|
|
707
|
+
"""
|
|
570
708
|
# Needed to avoid circular imports
|
|
571
709
|
from ._partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
|
|
572
710
|
|
|
@@ -585,15 +723,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
585
723
|
assert not webhook_config
|
|
586
724
|
assert not schedule
|
|
587
725
|
|
|
588
|
-
|
|
589
|
-
if include_source_mode != IncludeSourceMode.INCLUDE_NOTHING:
|
|
590
|
-
entrypoint_mounts = info.get_entrypoint_mount()
|
|
591
|
-
else:
|
|
592
|
-
entrypoint_mounts = {}
|
|
593
|
-
|
|
726
|
+
entrypoint_mount = info.get_entrypoint_mount() if include_source else {}
|
|
594
727
|
all_mounts = [
|
|
595
728
|
_get_client_mount(),
|
|
596
|
-
*
|
|
729
|
+
*entrypoint_mount.values(),
|
|
597
730
|
]
|
|
598
731
|
|
|
599
732
|
retry_policy = _parse_retries(
|
|
@@ -606,6 +739,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
606
739
|
if is_generator:
|
|
607
740
|
raise InvalidError("Generator functions do not support retries.")
|
|
608
741
|
|
|
742
|
+
if timeout is None: # type: ignore[unreachable] # Help users who aren't using type checkers
|
|
743
|
+
raise InvalidError("The `timeout` parameter cannot be set to None: https://modal.com/docs/guide/timeouts")
|
|
744
|
+
|
|
745
|
+
secrets = secrets or []
|
|
746
|
+
if env:
|
|
747
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
748
|
+
|
|
609
749
|
function_spec = _FunctionSpec(
|
|
610
750
|
mounts=all_mounts,
|
|
611
751
|
secrets=secrets,
|
|
@@ -621,34 +761,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
621
761
|
proxy=proxy,
|
|
622
762
|
)
|
|
623
763
|
|
|
624
|
-
if info.user_cls and not is_auto_snapshot:
|
|
625
|
-
build_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.BUILD).items()
|
|
626
|
-
for k, pf in build_functions:
|
|
627
|
-
build_function = pf.raw_f
|
|
628
|
-
snapshot_info = FunctionInfo(build_function, user_cls=info.user_cls)
|
|
629
|
-
snapshot_function = _Function.from_local(
|
|
630
|
-
snapshot_info,
|
|
631
|
-
app=None,
|
|
632
|
-
image=image,
|
|
633
|
-
secrets=secrets,
|
|
634
|
-
gpu=gpu,
|
|
635
|
-
network_file_systems=network_file_systems,
|
|
636
|
-
volumes=volumes,
|
|
637
|
-
memory=memory,
|
|
638
|
-
timeout=pf.params.build_timeout,
|
|
639
|
-
cpu=cpu,
|
|
640
|
-
ephemeral_disk=ephemeral_disk,
|
|
641
|
-
is_builder_function=True,
|
|
642
|
-
is_auto_snapshot=True,
|
|
643
|
-
scheduler_placement=scheduler_placement,
|
|
644
|
-
include_source=include_source,
|
|
645
|
-
)
|
|
646
|
-
image = _Image._from_args(
|
|
647
|
-
base_images={"base": image},
|
|
648
|
-
build_function=snapshot_function,
|
|
649
|
-
force_build=image.force_build or bool(pf.params.force_build),
|
|
650
|
-
)
|
|
651
|
-
|
|
652
764
|
# Note that we also do these checks in FunctionCreate; could drop them here
|
|
653
765
|
if min_containers is not None and not isinstance(min_containers, int):
|
|
654
766
|
raise InvalidError(f"`min_containers` must be an int, not {type(min_containers).__name__}")
|
|
@@ -708,7 +820,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
708
820
|
validated_network_file_systems = validate_network_file_systems(network_file_systems)
|
|
709
821
|
|
|
710
822
|
# Validate image
|
|
711
|
-
if image is not None and not isinstance(image, _Image):
|
|
823
|
+
if image is not None and not isinstance(image, _Image): # type: ignore[unreachable]
|
|
712
824
|
raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
|
|
713
825
|
|
|
714
826
|
method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
|
|
@@ -721,17 +833,23 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
721
833
|
for method_name, partial_function in interface_methods.items():
|
|
722
834
|
function_type = get_function_type(partial_function.params.is_generator)
|
|
723
835
|
function_name = f"{info.user_cls.__name__}.{method_name}"
|
|
836
|
+
is_web_endpoint = partial_function._is_web_endpoint()
|
|
724
837
|
method_schema = get_callable_schema(
|
|
725
838
|
partial_function._get_raw_f(),
|
|
726
|
-
is_web_endpoint=
|
|
839
|
+
is_web_endpoint=is_web_endpoint,
|
|
727
840
|
ignore_first_argument=True,
|
|
728
841
|
)
|
|
842
|
+
method_input_formats, method_output_formats = _get_supported_input_output_formats(
|
|
843
|
+
is_web_endpoint, partial_function.params.is_generator or False, restrict_output
|
|
844
|
+
)
|
|
729
845
|
|
|
730
846
|
method_definition = api_pb2.MethodDefinition(
|
|
731
847
|
webhook_config=partial_function.params.webhook_config,
|
|
732
848
|
function_type=function_type,
|
|
733
849
|
function_name=function_name,
|
|
734
850
|
function_schema=method_schema,
|
|
851
|
+
supported_input_formats=method_input_formats,
|
|
852
|
+
supported_output_formats=method_output_formats,
|
|
735
853
|
)
|
|
736
854
|
method_definitions[method_name] = method_definition
|
|
737
855
|
|
|
@@ -755,29 +873,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
755
873
|
|
|
756
874
|
return deps
|
|
757
875
|
|
|
758
|
-
|
|
759
|
-
|
|
876
|
+
if info.is_service_class():
|
|
877
|
+
# classes don't have data formats themselves - input/output formats are set per method above
|
|
878
|
+
supported_input_formats = []
|
|
879
|
+
supported_output_formats = []
|
|
880
|
+
else:
|
|
881
|
+
is_web_endpoint = webhook_config is not None and webhook_config.type != api_pb2.WEBHOOK_TYPE_UNSPECIFIED
|
|
882
|
+
supported_input_formats, supported_output_formats = _get_supported_input_output_formats(
|
|
883
|
+
is_web_endpoint, is_generator, restrict_output
|
|
884
|
+
)
|
|
760
885
|
|
|
761
|
-
|
|
886
|
+
async def _preload(
|
|
887
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
888
|
+
):
|
|
889
|
+
assert load_context.app_id
|
|
762
890
|
req = api_pb2.FunctionPrecreateRequest(
|
|
763
|
-
app_id=
|
|
891
|
+
app_id=load_context.app_id,
|
|
764
892
|
function_name=info.function_name,
|
|
765
893
|
function_type=function_type,
|
|
766
894
|
existing_function_id=existing_object_id or "",
|
|
767
895
|
function_schema=get_callable_schema(info.raw_f, is_web_endpoint=bool(webhook_config))
|
|
768
896
|
if info.raw_f
|
|
769
897
|
else None,
|
|
898
|
+
supported_input_formats=supported_input_formats,
|
|
899
|
+
supported_output_formats=supported_output_formats,
|
|
770
900
|
)
|
|
771
901
|
if method_definitions:
|
|
772
902
|
for method_name, method_definition in method_definitions.items():
|
|
773
903
|
req.method_definitions[method_name].CopyFrom(method_definition)
|
|
774
904
|
elif webhook_config:
|
|
775
905
|
req.webhook_config.CopyFrom(webhook_config)
|
|
776
|
-
response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
|
|
777
|
-
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
|
778
906
|
|
|
779
|
-
|
|
780
|
-
|
|
907
|
+
response = await load_context.client.stub.FunctionPrecreate(req)
|
|
908
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
909
|
+
|
|
910
|
+
async def _load(
|
|
911
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
912
|
+
):
|
|
781
913
|
with FunctionCreationStatus(resolver, tag) as function_creation_status:
|
|
782
914
|
timeout_secs = timeout
|
|
783
915
|
|
|
@@ -827,6 +959,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
827
959
|
mount_path=path,
|
|
828
960
|
volume_id=volume.object_id,
|
|
829
961
|
allow_background_commits=True,
|
|
962
|
+
read_only=volume._read_only,
|
|
830
963
|
)
|
|
831
964
|
for path, volume in validated_volumes_no_cloud_buckets
|
|
832
965
|
]
|
|
@@ -843,6 +976,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
843
976
|
function_schema = (
|
|
844
977
|
get_callable_schema(info.raw_f, is_web_endpoint=bool(webhook_config)) if info.raw_f else None
|
|
845
978
|
)
|
|
979
|
+
|
|
846
980
|
# Create function remotely
|
|
847
981
|
function_definition = api_pb2.Function(
|
|
848
982
|
module_name=info.module_name or "",
|
|
@@ -863,6 +997,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
863
997
|
proxy_id=(proxy.object_id if proxy else None),
|
|
864
998
|
retry_policy=retry_policy,
|
|
865
999
|
timeout_secs=timeout_secs or 0,
|
|
1000
|
+
startup_timeout_secs=startup_timeout or timeout_secs,
|
|
866
1001
|
pty_info=pty_info,
|
|
867
1002
|
cloud_provider_str=cloud if cloud else "",
|
|
868
1003
|
runtime=config.get("function_runtime"),
|
|
@@ -896,7 +1031,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
896
1031
|
_experimental_concurrent_cancellations=True,
|
|
897
1032
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
898
1033
|
_experimental_custom_scaling=_experimental_custom_scaling_factor is not None,
|
|
899
|
-
_experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
|
|
900
1034
|
# --- These are deprecated in favor of autoscaler_settings
|
|
901
1035
|
warm_pool_size=min_containers or 0,
|
|
902
1036
|
concurrency_limit=max_containers or 0,
|
|
@@ -904,6 +1038,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
904
1038
|
task_idle_timeout_secs=scaledown_window or 0,
|
|
905
1039
|
# ---
|
|
906
1040
|
function_schema=function_schema,
|
|
1041
|
+
supported_input_formats=supported_input_formats,
|
|
1042
|
+
supported_output_formats=supported_output_formats,
|
|
907
1043
|
)
|
|
908
1044
|
|
|
909
1045
|
if isinstance(gpu, list):
|
|
@@ -917,6 +1053,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
917
1053
|
autoscaler_settings=function_definition.autoscaler_settings,
|
|
918
1054
|
worker_id=function_definition.worker_id,
|
|
919
1055
|
timeout_secs=function_definition.timeout_secs,
|
|
1056
|
+
startup_timeout_secs=function_definition.startup_timeout_secs,
|
|
920
1057
|
web_url=function_definition.web_url,
|
|
921
1058
|
web_url_info=function_definition.web_url_info,
|
|
922
1059
|
webhook_config=function_definition.webhook_config,
|
|
@@ -933,12 +1070,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
933
1070
|
_experimental_group_size=function_definition._experimental_group_size,
|
|
934
1071
|
_experimental_buffer_containers=function_definition._experimental_buffer_containers,
|
|
935
1072
|
_experimental_custom_scaling=function_definition._experimental_custom_scaling,
|
|
936
|
-
_experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
|
|
937
1073
|
_experimental_proxy_ip=function_definition._experimental_proxy_ip,
|
|
938
1074
|
snapshot_debug=function_definition.snapshot_debug,
|
|
939
1075
|
runtime_perf_record=function_definition.runtime_perf_record,
|
|
940
1076
|
function_schema=function_schema,
|
|
941
1077
|
untrusted=function_definition.untrusted,
|
|
1078
|
+
supported_input_formats=supported_input_formats,
|
|
1079
|
+
supported_output_formats=supported_output_formats,
|
|
942
1080
|
)
|
|
943
1081
|
|
|
944
1082
|
ranked_functions = []
|
|
@@ -967,18 +1105,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
967
1105
|
),
|
|
968
1106
|
)
|
|
969
1107
|
|
|
970
|
-
assert
|
|
1108
|
+
assert load_context.app_id
|
|
971
1109
|
assert (function_definition is None) != (function_data is None) # xor
|
|
972
1110
|
request = api_pb2.FunctionCreateRequest(
|
|
973
|
-
app_id=
|
|
1111
|
+
app_id=load_context.app_id,
|
|
974
1112
|
function=function_definition,
|
|
975
1113
|
function_data=function_data,
|
|
976
1114
|
existing_function_id=existing_object_id or "",
|
|
977
1115
|
)
|
|
978
1116
|
try:
|
|
979
|
-
response: api_pb2.FunctionCreateResponse = await
|
|
980
|
-
resolver.client.stub.FunctionCreate, request
|
|
981
|
-
)
|
|
1117
|
+
response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
|
|
982
1118
|
except GRPCError as exc:
|
|
983
1119
|
if exc.status == Status.INVALID_ARGUMENT:
|
|
984
1120
|
raise InvalidError(exc.message)
|
|
@@ -993,10 +1129,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
993
1129
|
serve_mounts = {m for m in all_mounts if m.is_local()}
|
|
994
1130
|
serve_mounts |= image._serve_mounts
|
|
995
1131
|
obj._serve_mounts = frozenset(serve_mounts)
|
|
996
|
-
self._hydrate(response.function_id,
|
|
1132
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
997
1133
|
|
|
998
1134
|
rep = f"Function({tag})"
|
|
999
|
-
|
|
1135
|
+
# Pass a *reference* to the App's LoadContext - this is important since the App is
|
|
1136
|
+
# the only way to infer a LoadContext for an `@app.function`, and the App doesn't
|
|
1137
|
+
# get its client until *after* the Function is created.
|
|
1138
|
+
load_context = app._root_load_context if app else LoadContext.empty()
|
|
1139
|
+
obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
|
|
1000
1140
|
|
|
1001
1141
|
obj._raw_f = info.raw_f
|
|
1002
1142
|
obj._info = info
|
|
@@ -1038,7 +1178,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1038
1178
|
|
|
1039
1179
|
parent = self
|
|
1040
1180
|
|
|
1041
|
-
async def _load(
|
|
1181
|
+
async def _load(
|
|
1182
|
+
param_bound_func: _Function,
|
|
1183
|
+
resolver: Resolver,
|
|
1184
|
+
load_context: LoadContext,
|
|
1185
|
+
existing_object_id: Optional[str],
|
|
1186
|
+
):
|
|
1042
1187
|
if not parent.is_hydrated:
|
|
1043
1188
|
# While the base Object.hydrate() method appears to be idempotent, it's not always safe
|
|
1044
1189
|
await parent.hydrate()
|
|
@@ -1071,7 +1216,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1071
1216
|
param_bound_func._hydrate_from_other(parent)
|
|
1072
1217
|
return
|
|
1073
1218
|
|
|
1074
|
-
environment_name = _get_environment_name(None, resolver)
|
|
1075
1219
|
assert parent is not None and parent.is_hydrated
|
|
1076
1220
|
|
|
1077
1221
|
if options:
|
|
@@ -1080,6 +1224,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1080
1224
|
mount_path=path,
|
|
1081
1225
|
volume_id=volume.object_id,
|
|
1082
1226
|
allow_background_commits=True,
|
|
1227
|
+
read_only=volume._read_only,
|
|
1083
1228
|
)
|
|
1084
1229
|
for path, volume in options.validated_volumes
|
|
1085
1230
|
]
|
|
@@ -1088,6 +1233,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1088
1233
|
replace_secret_ids=bool(options.secrets),
|
|
1089
1234
|
replace_volume_mounts=len(volume_mounts) > 0,
|
|
1090
1235
|
volume_mounts=volume_mounts,
|
|
1236
|
+
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(options.cloud_bucket_mounts),
|
|
1237
|
+
replace_cloud_bucket_mounts=bool(options.cloud_bucket_mounts),
|
|
1091
1238
|
resources=options.resources,
|
|
1092
1239
|
retry_policy=options.retry_policy,
|
|
1093
1240
|
concurrency_limit=options.max_containers,
|
|
@@ -1098,6 +1245,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1098
1245
|
target_concurrent_inputs=options.target_concurrent_inputs,
|
|
1099
1246
|
batch_max_size=options.batch_max_size,
|
|
1100
1247
|
batch_linger_ms=options.batch_wait_ms,
|
|
1248
|
+
scheduler_placement=options.scheduler_placement,
|
|
1249
|
+
cloud_provider_str=options.cloud,
|
|
1101
1250
|
)
|
|
1102
1251
|
else:
|
|
1103
1252
|
options_pb = None
|
|
@@ -1106,19 +1255,30 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1106
1255
|
function_id=parent.object_id,
|
|
1107
1256
|
serialized_params=serialized_params,
|
|
1108
1257
|
function_options=options_pb,
|
|
1109
|
-
environment_name=environment_name
|
|
1258
|
+
environment_name=load_context.environment_name
|
|
1110
1259
|
or "", # TODO: investigate shouldn't environment name always be specified here?
|
|
1111
1260
|
)
|
|
1112
1261
|
|
|
1113
|
-
response = await
|
|
1262
|
+
response = await parent._client.stub.FunctionBindParams(req)
|
|
1114
1263
|
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
|
1115
1264
|
|
|
1116
1265
|
def _deps():
|
|
1117
1266
|
if options:
|
|
1118
|
-
|
|
1267
|
+
all_deps = (
|
|
1268
|
+
[v for _, v in options.validated_volumes]
|
|
1269
|
+
+ list(options.secrets)
|
|
1270
|
+
+ [mount.secret for _, mount in options.cloud_bucket_mounts if mount.secret]
|
|
1271
|
+
)
|
|
1272
|
+
return [dep for dep in all_deps if not dep.is_hydrated]
|
|
1119
1273
|
return []
|
|
1120
1274
|
|
|
1121
|
-
fun: _Function = _Function._from_loader(
|
|
1275
|
+
fun: _Function = _Function._from_loader(
|
|
1276
|
+
_load,
|
|
1277
|
+
"Function(parametrized)",
|
|
1278
|
+
hydrate_lazily=True,
|
|
1279
|
+
deps=_deps,
|
|
1280
|
+
load_context_overrides=self._load_context_overrides,
|
|
1281
|
+
)
|
|
1122
1282
|
|
|
1123
1283
|
fun._info = self._info
|
|
1124
1284
|
fun._obj = obj
|
|
@@ -1169,7 +1329,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1169
1329
|
scaledown_window=scaledown_window,
|
|
1170
1330
|
)
|
|
1171
1331
|
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
|
|
1172
|
-
await
|
|
1332
|
+
await self.client.stub.FunctionUpdateSchedulingParams(request)
|
|
1173
1333
|
|
|
1174
1334
|
# One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
|
|
1175
1335
|
# and then we could return them here (would need some ad hoc dataclass, which I don't love)
|
|
@@ -1212,34 +1372,47 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1212
1372
|
await self.update_autoscaler(min_containers=warm_pool_size)
|
|
1213
1373
|
|
|
1214
1374
|
@classmethod
|
|
1215
|
-
def _from_name(
|
|
1375
|
+
def _from_name(
|
|
1376
|
+
cls,
|
|
1377
|
+
app_name: str,
|
|
1378
|
+
name: str,
|
|
1379
|
+
*,
|
|
1380
|
+
load_context_overrides: LoadContext,
|
|
1381
|
+
):
|
|
1216
1382
|
# internal function lookup implementation that allows lookup of class "service functions"
|
|
1217
1383
|
# in addition to non-class functions
|
|
1218
|
-
async def _load_remote(
|
|
1219
|
-
|
|
1384
|
+
async def _load_remote(
|
|
1385
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
1386
|
+
):
|
|
1220
1387
|
request = api_pb2.FunctionGetRequest(
|
|
1221
1388
|
app_name=app_name,
|
|
1222
1389
|
object_tag=name,
|
|
1223
|
-
|
|
1224
|
-
environment_name=_get_environment_name(environment_name, resolver) or "",
|
|
1390
|
+
environment_name=load_context.environment_name,
|
|
1225
1391
|
)
|
|
1226
1392
|
try:
|
|
1227
|
-
response = await
|
|
1228
|
-
except
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1393
|
+
response = await load_context.client.stub.FunctionGet(request)
|
|
1394
|
+
except NotFoundError as exc:
|
|
1395
|
+
# refine the error message
|
|
1396
|
+
env_context = (
|
|
1397
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
1398
|
+
)
|
|
1399
|
+
raise NotFoundError(
|
|
1400
|
+
f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
1401
|
+
) from None
|
|
1236
1402
|
|
|
1237
1403
|
print_server_warnings(response.server_warnings)
|
|
1238
1404
|
|
|
1239
|
-
self._hydrate(response.function_id,
|
|
1405
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1240
1406
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1407
|
+
environment_rep = (
|
|
1408
|
+
f", environment_name={load_context_overrides.environment_name!r}"
|
|
1409
|
+
if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
|
|
1410
|
+
else ""
|
|
1411
|
+
)
|
|
1412
|
+
rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
|
|
1413
|
+
return cls._from_loader(
|
|
1414
|
+
_load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1415
|
+
)
|
|
1243
1416
|
|
|
1244
1417
|
@classmethod
|
|
1245
1418
|
def from_name(
|
|
@@ -1247,14 +1420,15 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1247
1420
|
app_name: str,
|
|
1248
1421
|
name: str,
|
|
1249
1422
|
*,
|
|
1250
|
-
namespace=
|
|
1423
|
+
namespace=None, # mdmd:line-hidden
|
|
1251
1424
|
environment_name: Optional[str] = None,
|
|
1425
|
+
client: Optional[_Client] = None,
|
|
1252
1426
|
) -> "_Function":
|
|
1253
1427
|
"""Reference a Function from a deployed App by its name.
|
|
1254
1428
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1429
|
+
This is a lazy method that defers hydrating the local
|
|
1430
|
+
object with metadata from Modal servers until the first
|
|
1431
|
+
time it is actually used.
|
|
1258
1432
|
|
|
1259
1433
|
```python
|
|
1260
1434
|
f = modal.Function.from_name("other-app", "function")
|
|
@@ -1271,40 +1445,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1271
1445
|
f"instance.{method_name}.remote(...)\n",
|
|
1272
1446
|
)
|
|
1273
1447
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
async def lookup(
|
|
1278
|
-
app_name: str,
|
|
1279
|
-
name: str,
|
|
1280
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
1281
|
-
client: Optional[_Client] = None,
|
|
1282
|
-
environment_name: Optional[str] = None,
|
|
1283
|
-
) -> "_Function":
|
|
1284
|
-
"""mdmd:hidden
|
|
1285
|
-
Lookup a Function from a deployed App by its name.
|
|
1286
|
-
|
|
1287
|
-
DEPRECATED: This method is deprecated in favor of `modal.Function.from_name`.
|
|
1288
|
-
|
|
1289
|
-
In contrast to `modal.Function.from_name`, this is an eager method
|
|
1290
|
-
that will hydrate the local object with metadata from Modal servers.
|
|
1291
|
-
|
|
1292
|
-
```python notest
|
|
1293
|
-
f = modal.Function.lookup("other-app", "function")
|
|
1294
|
-
```
|
|
1295
|
-
"""
|
|
1296
|
-
deprecation_warning(
|
|
1297
|
-
(2025, 1, 27),
|
|
1298
|
-
"`modal.Function.lookup` is deprecated and will be removed in a future release."
|
|
1299
|
-
" It can be replaced with `modal.Function.from_name`."
|
|
1300
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
1448
|
+
warn_if_passing_namespace(namespace, "modal.Function.from_name")
|
|
1449
|
+
return cls._from_name(
|
|
1450
|
+
app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
|
|
1301
1451
|
)
|
|
1302
|
-
obj = _Function.from_name(app_name, name, namespace=namespace, environment_name=environment_name)
|
|
1303
|
-
if client is None:
|
|
1304
|
-
client = await _Client.from_env()
|
|
1305
|
-
resolver = Resolver(client=client)
|
|
1306
|
-
await resolver.load(obj)
|
|
1307
|
-
return obj
|
|
1308
1452
|
|
|
1309
1453
|
@property
|
|
1310
1454
|
def tag(self) -> str:
|
|
@@ -1360,6 +1504,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1360
1504
|
self._info = None
|
|
1361
1505
|
self._serve_mounts = frozenset()
|
|
1362
1506
|
self._metadata = None
|
|
1507
|
+
self._experimental_flash_urls = None
|
|
1363
1508
|
|
|
1364
1509
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
1365
1510
|
# Overridden concrete implementation of base class method
|
|
@@ -1377,6 +1522,17 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1377
1522
|
self._method_handle_metadata = dict(metadata.method_handle_metadata)
|
|
1378
1523
|
self._definition_id = metadata.definition_id
|
|
1379
1524
|
self._input_plane_url = metadata.input_plane_url
|
|
1525
|
+
self._input_plane_region = metadata.input_plane_region
|
|
1526
|
+
# The server may pass back a larger max object size for some input plane users. This applies to input plane
|
|
1527
|
+
# users only - anyone using the control plane will get the standard limit.
|
|
1528
|
+
# There are some cases like FunctionPrecreate where this value is not set at all. We expect that this field
|
|
1529
|
+
# will eventually be hydrated with the correct value, but just to be defensive, if the field is not set we use
|
|
1530
|
+
# MAX_OBJECT_SIZE_BYTES, otherwise it would get set to 0. Accidentally using 0 would cause us to blob upload
|
|
1531
|
+
# everything, so let's avoid that.
|
|
1532
|
+
self._max_object_size_bytes = (
|
|
1533
|
+
metadata.max_object_size_bytes if metadata.HasField("max_object_size_bytes") else MAX_OBJECT_SIZE_BYTES
|
|
1534
|
+
)
|
|
1535
|
+
self._experimental_flash_urls = metadata._experimental_flash_urls
|
|
1380
1536
|
|
|
1381
1537
|
def _get_metadata(self):
|
|
1382
1538
|
# Overridden concrete implementation of base class method
|
|
@@ -1392,6 +1548,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1392
1548
|
method_handle_metadata=self._method_handle_metadata,
|
|
1393
1549
|
function_schema=self._metadata.function_schema if self._metadata else None,
|
|
1394
1550
|
input_plane_url=self._input_plane_url,
|
|
1551
|
+
input_plane_region=self._input_plane_region,
|
|
1552
|
+
max_object_size_bytes=self._max_object_size_bytes,
|
|
1553
|
+
_experimental_flash_urls=self._experimental_flash_urls,
|
|
1554
|
+
supported_input_formats=self._metadata.supported_input_formats if self._metadata else [],
|
|
1555
|
+
supported_output_formats=self._metadata.supported_output_formats if self._metadata else [],
|
|
1395
1556
|
)
|
|
1396
1557
|
|
|
1397
1558
|
def _check_no_web_url(self, fn_name: str):
|
|
@@ -1422,6 +1583,11 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1422
1583
|
"""URL of a Function running as a web endpoint."""
|
|
1423
1584
|
return self._web_url
|
|
1424
1585
|
|
|
1586
|
+
@live_method
|
|
1587
|
+
async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
|
|
1588
|
+
"""URL of the flash service for the function."""
|
|
1589
|
+
return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
|
|
1590
|
+
|
|
1425
1591
|
@property
|
|
1426
1592
|
async def is_generator(self) -> bool:
|
|
1427
1593
|
"""mdmd:hidden"""
|
|
@@ -1438,7 +1604,11 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1438
1604
|
|
|
1439
1605
|
@live_method_gen
|
|
1440
1606
|
async def _map(
|
|
1441
|
-
self,
|
|
1607
|
+
self,
|
|
1608
|
+
input_queue: _SynchronizedQueue,
|
|
1609
|
+
order_outputs: bool,
|
|
1610
|
+
return_exceptions: bool,
|
|
1611
|
+
wrap_returned_exceptions: bool,
|
|
1442
1612
|
) -> AsyncGenerator[Any, None]:
|
|
1443
1613
|
"""mdmd:hidden
|
|
1444
1614
|
|
|
@@ -1459,19 +1629,51 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1459
1629
|
else:
|
|
1460
1630
|
count_update_callback = None
|
|
1461
1631
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1632
|
+
if self._input_plane_url:
|
|
1633
|
+
async with aclosing(
|
|
1634
|
+
_map_invocation_inputplane(
|
|
1635
|
+
self,
|
|
1636
|
+
input_queue,
|
|
1637
|
+
self.client,
|
|
1638
|
+
order_outputs,
|
|
1639
|
+
return_exceptions,
|
|
1640
|
+
wrap_returned_exceptions,
|
|
1641
|
+
count_update_callback,
|
|
1642
|
+
)
|
|
1643
|
+
) as stream:
|
|
1644
|
+
async for item in stream:
|
|
1645
|
+
yield item
|
|
1646
|
+
else:
|
|
1647
|
+
async with aclosing(
|
|
1648
|
+
_map_invocation(
|
|
1649
|
+
self,
|
|
1650
|
+
input_queue,
|
|
1651
|
+
self.client,
|
|
1652
|
+
order_outputs,
|
|
1653
|
+
return_exceptions,
|
|
1654
|
+
wrap_returned_exceptions,
|
|
1655
|
+
count_update_callback,
|
|
1656
|
+
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
|
1657
|
+
)
|
|
1658
|
+
) as stream:
|
|
1659
|
+
async for item in stream:
|
|
1660
|
+
yield item
|
|
1661
|
+
|
|
1662
|
+
@live_method
|
|
1663
|
+
async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
|
|
1664
|
+
self._check_no_web_url("spawn_map")
|
|
1665
|
+
if self._is_generator:
|
|
1666
|
+
raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
|
|
1667
|
+
|
|
1668
|
+
assert self._function_name
|
|
1669
|
+
function_call_id, num_inputs = await _spawn_map_invocation(
|
|
1670
|
+
self,
|
|
1671
|
+
input_queue,
|
|
1672
|
+
self.client,
|
|
1673
|
+
)
|
|
1674
|
+
fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
|
|
1675
|
+
fc._num_inputs = num_inputs # set the cached value of num_inputs
|
|
1676
|
+
return fc
|
|
1475
1677
|
|
|
1476
1678
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
1477
1679
|
invocation: Union[_Invocation, _InputPlaneInvocation]
|
|
@@ -1482,6 +1684,7 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1482
1684
|
kwargs,
|
|
1483
1685
|
client=self.client,
|
|
1484
1686
|
input_plane_url=self._input_plane_url,
|
|
1687
|
+
input_plane_region=self._input_plane_region,
|
|
1485
1688
|
)
|
|
1486
1689
|
else:
|
|
1487
1690
|
invocation = await _Invocation.create(
|
|
@@ -1514,13 +1717,24 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1514
1717
|
@live_method_gen
|
|
1515
1718
|
@synchronizer.no_input_translation
|
|
1516
1719
|
async def _call_generator(self, args, kwargs):
|
|
1517
|
-
invocation
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1720
|
+
invocation: Union[_Invocation, _InputPlaneInvocation]
|
|
1721
|
+
if self._input_plane_url:
|
|
1722
|
+
invocation = await _InputPlaneInvocation.create(
|
|
1723
|
+
self,
|
|
1724
|
+
args,
|
|
1725
|
+
kwargs,
|
|
1726
|
+
client=self.client,
|
|
1727
|
+
input_plane_url=self._input_plane_url,
|
|
1728
|
+
input_plane_region=self._input_plane_region,
|
|
1729
|
+
)
|
|
1730
|
+
else:
|
|
1731
|
+
invocation = await _Invocation.create(
|
|
1732
|
+
self,
|
|
1733
|
+
args,
|
|
1734
|
+
kwargs,
|
|
1735
|
+
client=self.client,
|
|
1736
|
+
function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
|
|
1737
|
+
)
|
|
1524
1738
|
async for res in invocation.run_generator():
|
|
1525
1739
|
yield res
|
|
1526
1740
|
|
|
@@ -1584,8 +1798,9 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1584
1798
|
# "user code" to run on the synchronicity thread, which seems bad
|
|
1585
1799
|
if not self._is_local():
|
|
1586
1800
|
msg = (
|
|
1587
|
-
"The definition for this
|
|
1588
|
-
"If this function was retrieved via `Function.
|
|
1801
|
+
"The definition for this Function is missing, so it is not possible to invoke it locally. "
|
|
1802
|
+
"If this function was retrieved via `Function.from_name`, "
|
|
1803
|
+
"you need to use one of the remote invocation methods instead."
|
|
1589
1804
|
)
|
|
1590
1805
|
raise ExecutionError(msg)
|
|
1591
1806
|
|
|
@@ -1662,8 +1877,9 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1662
1877
|
async def spawn(self, *args: P.args, **kwargs: P.kwargs) -> "_FunctionCall[ReturnType]":
|
|
1663
1878
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
1664
1879
|
|
|
1665
|
-
Returns a [`modal.FunctionCall`](/docs/reference/modal.FunctionCall) object
|
|
1666
|
-
waited for using
|
|
1880
|
+
Returns a [`modal.FunctionCall`](https://modal.com/docs/reference/modal.FunctionCall) object
|
|
1881
|
+
that can later be polled or waited for using
|
|
1882
|
+
[`.get(timeout=...)`](https://modal.com/docs/reference/modal.FunctionCall#get).
|
|
1667
1883
|
Conceptually similar to `multiprocessing.pool.apply_async`, or a Future/Promise in other contexts.
|
|
1668
1884
|
"""
|
|
1669
1885
|
self._check_no_web_url("spawn")
|
|
@@ -1685,10 +1901,9 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1685
1901
|
@live_method
|
|
1686
1902
|
async def get_current_stats(self) -> FunctionStats:
|
|
1687
1903
|
"""Return a `FunctionStats` object describing the current function's queue and runner counts."""
|
|
1688
|
-
resp = await
|
|
1689
|
-
self.client.stub.FunctionGetCurrentStats,
|
|
1904
|
+
resp = await self.client.stub.FunctionGetCurrentStats(
|
|
1690
1905
|
api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
|
|
1691
|
-
total_timeout=10.0,
|
|
1906
|
+
retry=Retry(total_timeout=10.0),
|
|
1692
1907
|
)
|
|
1693
1908
|
return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
|
|
1694
1909
|
|
|
@@ -1706,6 +1921,7 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1706
1921
|
starmap = MethodWithAio(_starmap_sync, _starmap_async, synchronizer)
|
|
1707
1922
|
for_each = MethodWithAio(_for_each_sync, _for_each_async, synchronizer)
|
|
1708
1923
|
spawn_map = MethodWithAio(_spawn_map_sync, _spawn_map_async, synchronizer)
|
|
1924
|
+
experimental_spawn_map = MethodWithAio(_experimental_spawn_map_sync, _experimental_spawn_map_async, synchronizer)
|
|
1709
1925
|
|
|
1710
1926
|
|
|
1711
1927
|
class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
@@ -1720,12 +1936,25 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1720
1936
|
"""
|
|
1721
1937
|
|
|
1722
1938
|
_is_generator: bool = False
|
|
1939
|
+
_num_inputs: Optional[int] = None
|
|
1723
1940
|
|
|
1724
1941
|
def _invocation(self):
|
|
1725
1942
|
return _Invocation(self.client.stub, self.object_id, self.client)
|
|
1726
1943
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1944
|
+
@live_method
|
|
1945
|
+
async def num_inputs(self) -> int:
|
|
1946
|
+
"""Get the number of inputs in the function call."""
|
|
1947
|
+
if self._num_inputs is None:
|
|
1948
|
+
request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
|
|
1949
|
+
resp = await self.client.stub.FunctionCallFromId(request)
|
|
1950
|
+
self._num_inputs = resp.num_inputs # cached
|
|
1951
|
+
return self._num_inputs
|
|
1952
|
+
|
|
1953
|
+
@live_method
|
|
1954
|
+
async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
|
|
1955
|
+
"""Get the result of the index-th input of the function call.
|
|
1956
|
+
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
1957
|
+
A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
|
|
1729
1958
|
|
|
1730
1959
|
This function waits indefinitely by default. It takes an optional
|
|
1731
1960
|
`timeout` argument that specifies the maximum number of seconds to wait,
|
|
@@ -1733,27 +1962,59 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1733
1962
|
|
|
1734
1963
|
The returned coroutine is not cancellation-safe.
|
|
1735
1964
|
"""
|
|
1736
|
-
return await self._invocation().poll_function(timeout=timeout)
|
|
1965
|
+
return await self._invocation().poll_function(timeout=timeout, index=index)
|
|
1966
|
+
|
|
1967
|
+
@live_method_gen
|
|
1968
|
+
async def iter(self, *, start: int = 0, end: Optional[int] = None) -> AsyncIterator[ReturnType]:
|
|
1969
|
+
"""Iterate in-order over the results of the function call.
|
|
1970
|
+
|
|
1971
|
+
Optionally, specify a range [start, end) to iterate over.
|
|
1972
|
+
|
|
1973
|
+
Example:
|
|
1974
|
+
```python
|
|
1975
|
+
@app.function()
|
|
1976
|
+
def my_func(a):
|
|
1977
|
+
return a ** 2
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
@app.local_entrypoint()
|
|
1981
|
+
def main():
|
|
1982
|
+
fc = my_func.spawn_map([1, 2, 3, 4])
|
|
1983
|
+
assert list(fc.iter()) == [1, 4, 9, 16]
|
|
1984
|
+
assert list(fc.iter(start=1, end=3)) == [4, 9]
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
If `end` is not provided, it will iterate over all results.
|
|
1988
|
+
"""
|
|
1989
|
+
num_inputs = await self.num_inputs()
|
|
1990
|
+
if end is None:
|
|
1991
|
+
end = num_inputs
|
|
1992
|
+
if start < 0 or end > num_inputs:
|
|
1993
|
+
raise ValueError(f"Invalid index range: {start} to {end} for {num_inputs} inputs")
|
|
1994
|
+
async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
|
|
1995
|
+
yield item
|
|
1737
1996
|
|
|
1997
|
+
@live_method
|
|
1738
1998
|
async def get_call_graph(self) -> list[InputInfo]:
|
|
1739
1999
|
"""Returns a structure representing the call graph from a given root
|
|
1740
2000
|
call ID, along with the status of execution for each node.
|
|
1741
2001
|
|
|
1742
|
-
See [`modal.call_graph`](/docs/reference/modal.call_graph) reference page
|
|
2002
|
+
See [`modal.call_graph`](https://modal.com/docs/reference/modal.call_graph) reference page
|
|
1743
2003
|
for documentation on the structure of the returned `InputInfo` items.
|
|
1744
2004
|
"""
|
|
1745
2005
|
assert self._client and self._client.stub
|
|
1746
2006
|
request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
|
|
1747
|
-
response = await
|
|
2007
|
+
response = await self._client.stub.FunctionGetCallGraph(request)
|
|
1748
2008
|
return _reconstruct_call_graph(response)
|
|
1749
2009
|
|
|
2010
|
+
@live_method
|
|
1750
2011
|
async def cancel(
|
|
1751
2012
|
self,
|
|
1752
2013
|
# if true, containers running the inputs are forcibly terminated
|
|
1753
2014
|
terminate_containers: bool = False,
|
|
1754
2015
|
):
|
|
1755
2016
|
"""Cancels the function call, which will stop its execution and mark its inputs as
|
|
1756
|
-
[`TERMINATED`](/docs/reference/modal.call_graph#modalcall_graphinputstatus).
|
|
2017
|
+
[`TERMINATED`](https://modal.com/docs/reference/modal.call_graph#modalcall_graphinputstatus).
|
|
1757
2018
|
|
|
1758
2019
|
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
1759
2020
|
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
@@ -1762,7 +2023,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1762
2023
|
function_call_id=self.object_id, terminate_containers=terminate_containers
|
|
1763
2024
|
)
|
|
1764
2025
|
assert self._client and self._client.stub
|
|
1765
|
-
await
|
|
2026
|
+
await self._client.stub.FunctionCallCancel(request)
|
|
1766
2027
|
|
|
1767
2028
|
@staticmethod
|
|
1768
2029
|
async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
|
|
@@ -1784,11 +2045,18 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1784
2045
|
if you no longer have access to the original object returned from `Function.spawn`.
|
|
1785
2046
|
|
|
1786
2047
|
"""
|
|
1787
|
-
if client is None:
|
|
1788
|
-
client = await _Client.from_env()
|
|
1789
2048
|
|
|
1790
|
-
|
|
1791
|
-
|
|
2049
|
+
async def _load(
|
|
2050
|
+
self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
2051
|
+
):
|
|
2052
|
+
# this loader doesn't do anything in practice, but it will get the client from the load_context
|
|
2053
|
+
self._hydrate(function_call_id, load_context.client, None)
|
|
2054
|
+
|
|
2055
|
+
rep = f"FunctionCall.from_id({function_call_id!r})"
|
|
2056
|
+
|
|
2057
|
+
return _FunctionCall._from_loader(
|
|
2058
|
+
_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
|
|
2059
|
+
)
|
|
1792
2060
|
|
|
1793
2061
|
@staticmethod
|
|
1794
2062
|
async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
|