modal 1.1.1.dev16__py3-none-any.whl → 1.1.1.dev18__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/_functions.py +34 -16
- modal/_utils/auth_token_manager.py +1 -1
- modal/_utils/function_utils.py +1 -0
- modal/client.py +8 -1
- modal/client.pyi +9 -2
- modal/parallel_map.py +289 -0
- modal/parallel_map.pyi +21 -0
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/METADATA +1 -1
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/RECORD +17 -17
- modal_proto/api.proto +8 -0
- modal_proto/api_pb2.py +180 -172
- modal_proto/api_pb2.pyi +23 -1
- modal_version/__init__.py +1 -1
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/WHEEL +0 -0
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/entry_points.txt +0 -0
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.1.dev16.dist-info → modal-1.1.1.dev18.dist-info}/top_level.txt +0 -0
modal/_functions.py
CHANGED
|
@@ -75,6 +75,7 @@ from .parallel_map import (
|
|
|
75
75
|
_for_each_sync,
|
|
76
76
|
_map_async,
|
|
77
77
|
_map_invocation,
|
|
78
|
+
_map_invocation_inputplane,
|
|
78
79
|
_map_sync,
|
|
79
80
|
_spawn_map_async,
|
|
80
81
|
_spawn_map_sync,
|
|
@@ -399,7 +400,8 @@ class _InputPlaneInvocation:
|
|
|
399
400
|
parent_input_id=current_input_id() or "",
|
|
400
401
|
input=input_item,
|
|
401
402
|
)
|
|
402
|
-
|
|
403
|
+
|
|
404
|
+
metadata = await client.get_input_plane_metadata(input_plane_region)
|
|
403
405
|
response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
|
|
404
406
|
attempt_token = response.attempt_token
|
|
405
407
|
|
|
@@ -415,7 +417,7 @@ class _InputPlaneInvocation:
|
|
|
415
417
|
timeout_secs=OUTPUTS_TIMEOUT,
|
|
416
418
|
requested_at=time.time(),
|
|
417
419
|
)
|
|
418
|
-
metadata = await self.
|
|
420
|
+
metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
|
|
419
421
|
await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
|
|
420
422
|
self.stub.AttemptAwait,
|
|
421
423
|
await_request,
|
|
@@ -1514,20 +1516,36 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1514
1516
|
else:
|
|
1515
1517
|
count_update_callback = None
|
|
1516
1518
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1519
|
+
# TODO(ben-okeefe): Feature gating for input plane map until feature is enabled.
|
|
1520
|
+
if self._input_plane_url and False:
|
|
1521
|
+
async with aclosing(
|
|
1522
|
+
_map_invocation_inputplane(
|
|
1523
|
+
self,
|
|
1524
|
+
input_queue,
|
|
1525
|
+
self.client,
|
|
1526
|
+
order_outputs,
|
|
1527
|
+
return_exceptions,
|
|
1528
|
+
wrap_returned_exceptions,
|
|
1529
|
+
count_update_callback,
|
|
1530
|
+
)
|
|
1531
|
+
) as stream:
|
|
1532
|
+
async for item in stream:
|
|
1533
|
+
yield item
|
|
1534
|
+
else:
|
|
1535
|
+
async with aclosing(
|
|
1536
|
+
_map_invocation(
|
|
1537
|
+
self,
|
|
1538
|
+
input_queue,
|
|
1539
|
+
self.client,
|
|
1540
|
+
order_outputs,
|
|
1541
|
+
return_exceptions,
|
|
1542
|
+
wrap_returned_exceptions,
|
|
1543
|
+
count_update_callback,
|
|
1544
|
+
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
|
1545
|
+
)
|
|
1546
|
+
) as stream:
|
|
1547
|
+
async for item in stream:
|
|
1548
|
+
yield item
|
|
1531
1549
|
|
|
1532
1550
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
1533
1551
|
invocation: Union[_Invocation, _InputPlaneInvocation]
|
|
@@ -27,7 +27,7 @@ class _AuthTokenManager:
|
|
|
27
27
|
self._expiry = 0.0
|
|
28
28
|
self._lock: typing.Union[asyncio.Lock, None] = None
|
|
29
29
|
|
|
30
|
-
async def get_token(self):
|
|
30
|
+
async def get_token(self) -> str:
|
|
31
31
|
"""
|
|
32
32
|
When called, the AuthTokenManager can be in one of three states:
|
|
33
33
|
1. Has a valid cached token. It is returned to the caller.
|
modal/_utils/function_utils.py
CHANGED
modal/client.py
CHANGED
|
@@ -268,6 +268,14 @@ class _Client:
|
|
|
268
268
|
# Just used from tests.
|
|
269
269
|
cls._client_from_env = client
|
|
270
270
|
|
|
271
|
+
async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]:
|
|
272
|
+
assert self._auth_token_manager, "Client must have an instance of auth token manager."
|
|
273
|
+
token = await self._auth_token_manager.get_token()
|
|
274
|
+
return [
|
|
275
|
+
("x-modal-input-plane-region", input_plane_region),
|
|
276
|
+
("x-modal-auth-token", token),
|
|
277
|
+
]
|
|
278
|
+
|
|
271
279
|
async def _call_safely(self, coro, readable_method: str):
|
|
272
280
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
|
273
281
|
|
|
@@ -456,4 +464,3 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
|
456
464
|
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
457
465
|
async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
|
|
458
466
|
yield response
|
|
459
|
-
|
modal/client.pyi
CHANGED
|
@@ -33,7 +33,7 @@ class _Client:
|
|
|
33
33
|
server_url: str,
|
|
34
34
|
client_type: int,
|
|
35
35
|
credentials: typing.Optional[tuple[str, str]],
|
|
36
|
-
version: str = "1.1.1.
|
|
36
|
+
version: str = "1.1.1.dev18",
|
|
37
37
|
):
|
|
38
38
|
"""mdmd:hidden
|
|
39
39
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -112,6 +112,7 @@ class _Client:
|
|
|
112
112
|
"""mdmd:hidden"""
|
|
113
113
|
...
|
|
114
114
|
|
|
115
|
+
async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
115
116
|
async def _call_safely(self, coro, readable_method: str):
|
|
116
117
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
|
117
118
|
|
|
@@ -163,7 +164,7 @@ class Client:
|
|
|
163
164
|
server_url: str,
|
|
164
165
|
client_type: int,
|
|
165
166
|
credentials: typing.Optional[tuple[str, str]],
|
|
166
|
-
version: str = "1.1.1.
|
|
167
|
+
version: str = "1.1.1.dev18",
|
|
167
168
|
):
|
|
168
169
|
"""mdmd:hidden
|
|
169
170
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -275,6 +276,12 @@ class Client:
|
|
|
275
276
|
"""mdmd:hidden"""
|
|
276
277
|
...
|
|
277
278
|
|
|
279
|
+
class __get_input_plane_metadata_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
280
|
+
def __call__(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
281
|
+
async def aio(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
282
|
+
|
|
283
|
+
get_input_plane_metadata: __get_input_plane_metadata_spec[typing_extensions.Self]
|
|
284
|
+
|
|
278
285
|
class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
279
286
|
def __call__(self, /, coro, readable_method: str):
|
|
280
287
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
modal/parallel_map.py
CHANGED
|
@@ -424,6 +424,295 @@ async def _map_invocation(
|
|
|
424
424
|
await log_debug_stats_task
|
|
425
425
|
|
|
426
426
|
|
|
427
|
+
async def _map_invocation_inputplane(
|
|
428
|
+
function: "modal.functions._Function",
|
|
429
|
+
raw_input_queue: _SynchronizedQueue,
|
|
430
|
+
client: "modal.client._Client",
|
|
431
|
+
order_outputs: bool,
|
|
432
|
+
return_exceptions: bool,
|
|
433
|
+
wrap_returned_exceptions: bool,
|
|
434
|
+
count_update_callback: Optional[Callable[[int, int], None]],
|
|
435
|
+
) -> typing.AsyncGenerator[Any, None]:
|
|
436
|
+
"""Input-plane implementation of a function map invocation.
|
|
437
|
+
|
|
438
|
+
This is analogous to `_map_invocation`, but instead of the control-plane
|
|
439
|
+
`FunctionMap` / `FunctionPutInputs` / `FunctionGetOutputs` RPCs it speaks
|
|
440
|
+
the input-plane protocol consisting of `MapStartOrContinue` and `MapAwait`.
|
|
441
|
+
|
|
442
|
+
The implementation purposefully ignores retry handling for now - a stub is
|
|
443
|
+
left in place so that a future change can add support for the retry path
|
|
444
|
+
without re-structuring the surrounding code.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
assert function._input_plane_url, "_map_invocation_inputplane should only be used for input-plane backed functions"
|
|
448
|
+
|
|
449
|
+
input_plane_stub = await client.get_stub(function._input_plane_url)
|
|
450
|
+
|
|
451
|
+
assert client.stub, "Client must be hydrated with a stub for _map_invocation_inputplane"
|
|
452
|
+
|
|
453
|
+
# ------------------------------------------------------------
|
|
454
|
+
# Invocation-wide state
|
|
455
|
+
# ------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
have_all_inputs = False
|
|
458
|
+
map_done_event = asyncio.Event()
|
|
459
|
+
|
|
460
|
+
inputs_created = 0
|
|
461
|
+
outputs_completed = 0
|
|
462
|
+
|
|
463
|
+
# The input-plane server returns this after the first request.
|
|
464
|
+
function_call_id: str | None = None
|
|
465
|
+
function_call_id_received = asyncio.Event()
|
|
466
|
+
|
|
467
|
+
# Map of idx -> attempt_token returned by the server. This will be needed
|
|
468
|
+
# for a future client-side retry implementation.
|
|
469
|
+
attempt_tokens: dict[int, str] = {}
|
|
470
|
+
|
|
471
|
+
# Single priority queue that holds *both* fresh inputs (timestamp == now)
|
|
472
|
+
# and future retries (timestamp > now).
|
|
473
|
+
queue: TimestampPriorityQueue[api_pb2.MapStartOrContinueItem] = TimestampPriorityQueue()
|
|
474
|
+
|
|
475
|
+
# Maximum number of inputs that may be in-flight (the server sends this in
|
|
476
|
+
# the first response – fall back to the default if we never receive it for
|
|
477
|
+
# any reason).
|
|
478
|
+
max_inputs_outstanding = MAX_INPUTS_OUTSTANDING_DEFAULT
|
|
479
|
+
|
|
480
|
+
# ------------------------------------------------------------
|
|
481
|
+
# Helper functions
|
|
482
|
+
# ------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
def update_counters(created_delta: int = 0, completed_delta: int = 0, set_have_all_inputs: bool | None = None):
|
|
485
|
+
nonlocal inputs_created, outputs_completed, have_all_inputs
|
|
486
|
+
|
|
487
|
+
if created_delta:
|
|
488
|
+
inputs_created += created_delta
|
|
489
|
+
if completed_delta:
|
|
490
|
+
outputs_completed += completed_delta
|
|
491
|
+
if set_have_all_inputs is not None:
|
|
492
|
+
have_all_inputs = set_have_all_inputs
|
|
493
|
+
|
|
494
|
+
if count_update_callback is not None:
|
|
495
|
+
count_update_callback(outputs_completed, inputs_created)
|
|
496
|
+
|
|
497
|
+
if have_all_inputs and outputs_completed >= inputs_created:
|
|
498
|
+
map_done_event.set()
|
|
499
|
+
|
|
500
|
+
async def create_input(argskwargs):
|
|
501
|
+
idx = inputs_created + 1 # 1-indexed map call idx
|
|
502
|
+
update_counters(created_delta=1)
|
|
503
|
+
(args, kwargs) = argskwargs
|
|
504
|
+
put_item: api_pb2.FunctionPutInputsItem = await _create_input(
|
|
505
|
+
args,
|
|
506
|
+
kwargs,
|
|
507
|
+
client.stub,
|
|
508
|
+
max_object_size_bytes=function._max_object_size_bytes,
|
|
509
|
+
idx=idx,
|
|
510
|
+
method_name=function._use_method_name,
|
|
511
|
+
)
|
|
512
|
+
return api_pb2.MapStartOrContinueItem(input=put_item)
|
|
513
|
+
|
|
514
|
+
# ------------------------------------------------------------
|
|
515
|
+
# Coroutine: drain user input iterator, upload blobs, enqueue for sending
|
|
516
|
+
# ------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
async def input_iter():
|
|
519
|
+
while True:
|
|
520
|
+
raw_input = await raw_input_queue.get()
|
|
521
|
+
if raw_input is None: # end of input sentinel
|
|
522
|
+
break
|
|
523
|
+
yield raw_input # args, kwargs
|
|
524
|
+
|
|
525
|
+
async def drain_input_generator():
|
|
526
|
+
async with aclosing(
|
|
527
|
+
async_map_ordered(input_iter(), create_input, concurrency=BLOB_MAX_PARALLELISM)
|
|
528
|
+
) as streamer:
|
|
529
|
+
async for q_item in streamer:
|
|
530
|
+
await queue.put(time.time(), q_item)
|
|
531
|
+
|
|
532
|
+
# All inputs have been read.
|
|
533
|
+
await queue.close()
|
|
534
|
+
update_counters(set_have_all_inputs=True)
|
|
535
|
+
yield
|
|
536
|
+
|
|
537
|
+
# ------------------------------------------------------------
|
|
538
|
+
# Coroutine: send queued items to the input-plane server
|
|
539
|
+
# ------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
async def pump_inputs():
|
|
542
|
+
nonlocal function_call_id, max_inputs_outstanding
|
|
543
|
+
|
|
544
|
+
async for batch in queue_batch_iterator(queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
|
|
545
|
+
# Convert the queued items into the proto format expected by the RPC.
|
|
546
|
+
request_items: list[api_pb2.MapStartOrContinueItem] = [
|
|
547
|
+
api_pb2.MapStartOrContinueItem(input=qi.input, attempt_token=qi.attempt_token) for qi in batch
|
|
548
|
+
]
|
|
549
|
+
# Build request
|
|
550
|
+
request = api_pb2.MapStartOrContinueRequest(
|
|
551
|
+
function_id=function.object_id,
|
|
552
|
+
function_call_id=function_call_id,
|
|
553
|
+
parent_input_id=current_input_id() or "",
|
|
554
|
+
items=request_items,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
|
558
|
+
|
|
559
|
+
response: api_pb2.MapStartOrContinueResponse = await retry_transient_errors(
|
|
560
|
+
input_plane_stub.MapStartOrContinue, request, metadata=metadata
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# TODO(ben-okeefe): Understand if an input could be lost at this step and not registered
|
|
564
|
+
|
|
565
|
+
if function_call_id is None:
|
|
566
|
+
function_call_id = response.function_call_id
|
|
567
|
+
function_call_id_received.set()
|
|
568
|
+
max_inputs_outstanding = response.max_inputs_outstanding or MAX_INPUTS_OUTSTANDING_DEFAULT
|
|
569
|
+
|
|
570
|
+
# Record attempt tokens for future retries; also release semaphore slots now that the
|
|
571
|
+
# inputs are officially registered on the server.
|
|
572
|
+
for idx, attempt_token in enumerate(response.attempt_tokens):
|
|
573
|
+
# Client expects the server to return the attempt tokens in the same order as the inputs we sent.
|
|
574
|
+
attempt_tokens[request_items[idx].input.idx] = attempt_token
|
|
575
|
+
|
|
576
|
+
yield
|
|
577
|
+
|
|
578
|
+
# ------------------------------------------------------------
|
|
579
|
+
# Coroutine: **stub** – retry handling will be added in the future
|
|
580
|
+
# ------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
async def retry_inputs():
|
|
583
|
+
"""Temporary stub for retrying inputs. Retry handling will be added in the future."""
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
while not map_done_event.is_set():
|
|
587
|
+
await asyncio.sleep(1)
|
|
588
|
+
if False:
|
|
589
|
+
yield
|
|
590
|
+
except asyncio.CancelledError:
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
# ------------------------------------------------------------
|
|
594
|
+
# Coroutine: stream outputs via MapAwait
|
|
595
|
+
# ------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
async def get_all_outputs():
|
|
598
|
+
"""Continuously fetch outputs until the map is complete."""
|
|
599
|
+
last_entry_id = ""
|
|
600
|
+
while not map_done_event.is_set():
|
|
601
|
+
if function_call_id is None:
|
|
602
|
+
await function_call_id_received.wait()
|
|
603
|
+
continue
|
|
604
|
+
|
|
605
|
+
request = api_pb2.MapAwaitRequest(
|
|
606
|
+
function_call_id=function_call_id,
|
|
607
|
+
last_entry_id=last_entry_id,
|
|
608
|
+
requested_at=time.time(),
|
|
609
|
+
timeout=OUTPUTS_TIMEOUT,
|
|
610
|
+
)
|
|
611
|
+
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
|
612
|
+
response: api_pb2.MapAwaitResponse = await retry_transient_errors(
|
|
613
|
+
input_plane_stub.MapAwait,
|
|
614
|
+
request,
|
|
615
|
+
max_retries=20,
|
|
616
|
+
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
617
|
+
metadata=metadata,
|
|
618
|
+
)
|
|
619
|
+
last_entry_id = response.last_entry_id
|
|
620
|
+
|
|
621
|
+
for output_item in response.outputs:
|
|
622
|
+
yield output_item
|
|
623
|
+
|
|
624
|
+
update_counters(completed_delta=1)
|
|
625
|
+
|
|
626
|
+
# The loop condition will exit when map_done_event is set from update_counters.
|
|
627
|
+
|
|
628
|
+
async def get_all_outputs_and_clean_up():
|
|
629
|
+
try:
|
|
630
|
+
async with aclosing(get_all_outputs()) as stream:
|
|
631
|
+
async for item in stream:
|
|
632
|
+
yield item
|
|
633
|
+
finally:
|
|
634
|
+
# We could signal server we are done with outputs so it can clean up.
|
|
635
|
+
pass
|
|
636
|
+
|
|
637
|
+
# ------------------------------------------------------------
|
|
638
|
+
# Coroutine: convert FunctionGetOutputsItem → actual result value
|
|
639
|
+
# ------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
async def fetch_output(item: api_pb2.FunctionGetOutputsItem) -> tuple[int, Any]:
|
|
642
|
+
try:
|
|
643
|
+
output_val = await _process_result(item.result, item.data_format, input_plane_stub, client)
|
|
644
|
+
except Exception as exc:
|
|
645
|
+
if return_exceptions:
|
|
646
|
+
output_val = exc
|
|
647
|
+
else:
|
|
648
|
+
raise exc
|
|
649
|
+
|
|
650
|
+
return (item.idx, output_val)
|
|
651
|
+
|
|
652
|
+
async def poll_outputs():
|
|
653
|
+
# map to store out-of-order outputs received
|
|
654
|
+
received_outputs = {}
|
|
655
|
+
output_idx = 1 # 1-indexed map call idx
|
|
656
|
+
|
|
657
|
+
async with aclosing(
|
|
658
|
+
async_map_ordered(get_all_outputs_and_clean_up(), fetch_output, concurrency=BLOB_MAX_PARALLELISM)
|
|
659
|
+
) as streamer:
|
|
660
|
+
async for idx, output in streamer:
|
|
661
|
+
if not order_outputs:
|
|
662
|
+
yield _OutputValue(output)
|
|
663
|
+
else:
|
|
664
|
+
# hold on to outputs for function maps, so we can reorder them correctly.
|
|
665
|
+
received_outputs[idx] = output
|
|
666
|
+
|
|
667
|
+
while True:
|
|
668
|
+
if output_idx not in received_outputs:
|
|
669
|
+
# we haven't received the output for the current index yet.
|
|
670
|
+
# stop returning outputs to the caller and instead wait for
|
|
671
|
+
# the next output to arrive from the server.
|
|
672
|
+
break
|
|
673
|
+
|
|
674
|
+
output = received_outputs.pop(output_idx)
|
|
675
|
+
yield _OutputValue(output)
|
|
676
|
+
output_idx += 1
|
|
677
|
+
|
|
678
|
+
assert len(received_outputs) == 0
|
|
679
|
+
|
|
680
|
+
# ------------------------------------------------------------
|
|
681
|
+
# Debug-logging helper
|
|
682
|
+
# ------------------------------------------------------------
|
|
683
|
+
async def log_debug_stats():
|
|
684
|
+
def log_stats():
|
|
685
|
+
logger.debug(
|
|
686
|
+
"Map-IP stats: have_all_inputs=%s inputs_created=%d outputs_completed=%d queue_size=%d",
|
|
687
|
+
have_all_inputs,
|
|
688
|
+
inputs_created,
|
|
689
|
+
outputs_completed,
|
|
690
|
+
queue.qsize(),
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
while True:
|
|
694
|
+
log_stats()
|
|
695
|
+
try:
|
|
696
|
+
await asyncio.sleep(10)
|
|
697
|
+
except asyncio.CancelledError:
|
|
698
|
+
# Log final stats before exiting
|
|
699
|
+
log_stats()
|
|
700
|
+
break
|
|
701
|
+
|
|
702
|
+
# ------------------------------------------------------------
|
|
703
|
+
# Run the four coroutines concurrently and yield results as they arrive
|
|
704
|
+
# ------------------------------------------------------------
|
|
705
|
+
|
|
706
|
+
log_task = asyncio.create_task(log_debug_stats())
|
|
707
|
+
|
|
708
|
+
async with aclosing(async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())) as merged:
|
|
709
|
+
async for maybe_output in merged:
|
|
710
|
+
if maybe_output is not None: # ignore None sentinels
|
|
711
|
+
yield maybe_output.value
|
|
712
|
+
|
|
713
|
+
log_task.cancel()
|
|
714
|
+
|
|
715
|
+
|
|
427
716
|
async def _map_helper(
|
|
428
717
|
self: "modal.functions.Function",
|
|
429
718
|
async_input_gen: typing.AsyncGenerator[Any, None],
|
modal/parallel_map.pyi
CHANGED
|
@@ -70,6 +70,27 @@ def _map_invocation(
|
|
|
70
70
|
count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
|
|
71
71
|
function_call_invocation_type: int,
|
|
72
72
|
): ...
|
|
73
|
+
def _map_invocation_inputplane(
|
|
74
|
+
function: modal._functions._Function,
|
|
75
|
+
raw_input_queue: _SynchronizedQueue,
|
|
76
|
+
client: modal.client._Client,
|
|
77
|
+
order_outputs: bool,
|
|
78
|
+
return_exceptions: bool,
|
|
79
|
+
wrap_returned_exceptions: bool,
|
|
80
|
+
count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
|
|
81
|
+
) -> typing.AsyncGenerator[typing.Any, None]:
|
|
82
|
+
"""Input-plane implementation of a function map invocation.
|
|
83
|
+
|
|
84
|
+
This is analogous to `_map_invocation`, but instead of the control-plane
|
|
85
|
+
`FunctionMap` / `FunctionPutInputs` / `FunctionGetOutputs` RPCs it speaks
|
|
86
|
+
the input-plane protocol consisting of `MapStartOrContinue` and `MapAwait`.
|
|
87
|
+
|
|
88
|
+
The implementation purposefully ignores retry handling for now - a stub is
|
|
89
|
+
left in place so that a future change can add support for the retry path
|
|
90
|
+
without re-structuring the surrounding code.
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
73
94
|
def _map_helper(
|
|
74
95
|
self: modal.functions.Function,
|
|
75
96
|
async_input_gen: typing.AsyncGenerator[typing.Any, None],
|
|
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=sTJcc9EbDuCKSwg3tL6ZckFw9WWdlkXW8mId1IvJCNc,2846
|
|
|
3
3
|
modal/_clustered_functions.py,sha256=zmrKbptRbqp4euS3LWncKaLXb8Kjj4YreusOzpEpRMk,2856
|
|
4
4
|
modal/_clustered_functions.pyi,sha256=_wtFjWocGf1WgI-qYBpbJPArNkg2H9JV7BVaGgMesEQ,1103
|
|
5
5
|
modal/_container_entrypoint.py,sha256=1qBMNY_E9ICC_sRCtillMxmKPsmxJl1J0_qOAG8rH-0,28288
|
|
6
|
-
modal/_functions.py,sha256=
|
|
6
|
+
modal/_functions.py,sha256=c1EeAcHcYP76wHbLomV0mFe73AoL-TUSjJNSDu6CELI,83065
|
|
7
7
|
modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
|
|
8
8
|
modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
|
|
9
9
|
modal/_object.py,sha256=vzRWhAcFJDM8ZmiUSDgSj9gJhLZJA-wjLVvTl984N4A,11295
|
|
@@ -21,8 +21,8 @@ modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
|
|
|
21
21
|
modal/app.py,sha256=BBR2NmGzZbFGfhKAmtzllD0o4TbVDBbOEs0O2ysSdQo,48277
|
|
22
22
|
modal/app.pyi,sha256=h6JtBA6a7wobdZAuS3QuXrWCUZqfyKPuGV3XdjCqT3k,43753
|
|
23
23
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
|
24
|
-
modal/client.py,sha256=
|
|
25
|
-
modal/client.pyi,sha256=
|
|
24
|
+
modal/client.py,sha256=kyAIVB3Ay-XKJizQ_1ufUFB__EagV0MLmHJpyYyJ7J0,18636
|
|
25
|
+
modal/client.pyi,sha256=9JRiPD1SpvKkNx4zbQafP7MoEr_696pP20yqmLs6j00,15831
|
|
26
26
|
modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
|
|
27
27
|
modal/cloud_bucket_mount.pyi,sha256=-qSfYAQvIoO_l2wsCCGTG5ZUwQieNKXdAO00yP1-LYU,7394
|
|
28
28
|
modal/cls.py,sha256=7A0xGnugQzm8dOfnKMjLjtqekRlRtQ0jPFRYgq6xdUM,40018
|
|
@@ -52,8 +52,8 @@ modal/network_file_system.pyi,sha256=Td_IobHr84iLo_9LZKQ4tNdUB60yjX8QWBaFiUvhfi8
|
|
|
52
52
|
modal/object.py,sha256=bTeskuY8JFrESjU4_UL_nTwYlBQdOLmVaOX3X6EMxsg,164
|
|
53
53
|
modal/object.pyi,sha256=751TV6BntarPsErf0HDQPsvePjWFf0JZK8ZAiRpM1yg,6627
|
|
54
54
|
modal/output.py,sha256=q4T9uHduunj4NwY-YSwkHGgjZlCXMuJbfQ5UFaAGRAc,1968
|
|
55
|
-
modal/parallel_map.py,sha256
|
|
56
|
-
modal/parallel_map.pyi,sha256
|
|
55
|
+
modal/parallel_map.py,sha256=-9nS9s1jbx1Iqh_5HQRK4xTdhnXF4AGIXwT4UGJ8R78,52666
|
|
56
|
+
modal/parallel_map.pyi,sha256=fCugFsGup4Cflesb10_uR-nt5_eguuvhvtvavus_F98,11186
|
|
57
57
|
modal/partial_function.py,sha256=aIdlGfTjjgqY6Fpr-biCjvRU9W542_S5N2xkNN_rYGM,1127
|
|
58
58
|
modal/partial_function.pyi,sha256=lqqOzZ9-QvHTDWKQ_oAYYOvsXgTOBKhO9u-RI98JbUk,13986
|
|
59
59
|
modal/proxy.py,sha256=NQJJMGo-D2IfmeU0vb10WWaE4oTLcuf9jTeEJvactOg,1446
|
|
@@ -92,12 +92,12 @@ modal/_runtime/user_code_imports.py,sha256=78wJyleqY2RVibqcpbDQyfWVBVT9BjyHPeoV9
|
|
|
92
92
|
modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
|
|
93
93
|
modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
|
|
94
94
|
modal/_utils/async_utils.py,sha256=ot8NiPGZ5bRJhY5ilZyjNgx24VI-1BIpCu054oLHDf0,29556
|
|
95
|
-
modal/_utils/auth_token_manager.py,sha256=
|
|
95
|
+
modal/_utils/auth_token_manager.py,sha256=i-kfLgDd4BMAw6wouO5aKfNGHo27VAZoVOsbEWqDr2I,5252
|
|
96
96
|
modal/_utils/blob_utils.py,sha256=bySVr9M7hlFzZo-u4ikovxMdcdEE8yfGOs94Zex2k4o,20913
|
|
97
97
|
modal/_utils/bytes_io_segment_payload.py,sha256=vaXPq8b52-x6G2hwE7SrjS58pg_aRm7gV3bn3yjmTzQ,4261
|
|
98
98
|
modal/_utils/deprecation.py,sha256=-Bgg7jZdcJU8lROy18YyVnQYbM8hue-hVmwJqlWAGH0,5504
|
|
99
99
|
modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
|
|
100
|
-
modal/_utils/function_utils.py,sha256=
|
|
100
|
+
modal/_utils/function_utils.py,sha256=uEs3hkVQr46m2_LthJqKaI02LLMgHeiD8CsoNWxEpnk,27417
|
|
101
101
|
modal/_utils/git_utils.py,sha256=qtUU6JAttF55ZxYq51y55OR58B0tDPZsZWK5dJe6W5g,3182
|
|
102
102
|
modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
|
|
103
103
|
modal/_utils/grpc_utils.py,sha256=HBZdMcBHCk6uozILYTjGnR0mV8fg7WOdJldoyZ-ZhSg,10137
|
|
@@ -151,7 +151,7 @@ modal/requirements/2025.06.txt,sha256=KxDaVTOwatHvboDo4lorlgJ7-n-MfAwbPwxJ0zcJqr
|
|
|
151
151
|
modal/requirements/PREVIEW.txt,sha256=KxDaVTOwatHvboDo4lorlgJ7-n-MfAwbPwxJ0zcJqrs,312
|
|
152
152
|
modal/requirements/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
|
|
153
153
|
modal/requirements/base-images.json,sha256=JYSDAgHTl-WrV_TZW5icY-IJEnbe2eQ4CZ_KN6EOZKU,1304
|
|
154
|
-
modal-1.1.1.
|
|
154
|
+
modal-1.1.1.dev18.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
155
155
|
modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
|
|
156
156
|
modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
|
|
157
157
|
modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
|
|
@@ -159,10 +159,10 @@ modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,2
|
|
|
159
159
|
modal_docs/mdmd/mdmd.py,sha256=eW5MzrEl7mSclDo4Uv64sQ1-4IyLggldbgUJdBVLDdI,6449
|
|
160
160
|
modal_docs/mdmd/signatures.py,sha256=XJaZrK7Mdepk5fdX51A8uENiLFNil85Ud0d4MH8H5f0,3218
|
|
161
161
|
modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
|
|
162
|
-
modal_proto/api.proto,sha256=
|
|
162
|
+
modal_proto/api.proto,sha256=5j2a64upHl9Xf0OcyoiH-6t1eyQTdDpS7nNocYVYqVk,102442
|
|
163
163
|
modal_proto/api_grpc.py,sha256=AL8Z1zlvrsqrxXYEv_mKroJArPV7_8eQ2bMvvswDlTQ,123880
|
|
164
|
-
modal_proto/api_pb2.py,sha256=
|
|
165
|
-
modal_proto/api_pb2.pyi,sha256=
|
|
164
|
+
modal_proto/api_pb2.py,sha256=73p7vlYoCyB_5SIHQ5vZr4jy8LAdayp-ba5DLiTJdIU,358885
|
|
165
|
+
modal_proto/api_pb2.pyi,sha256=7nghkZyfXJvaN7b9byqNwHxD6n1jtvjJAVN93CPRVp0,493332
|
|
166
166
|
modal_proto/api_pb2_grpc.py,sha256=QmhsoHLD9BBXVl3ZpIb-0_sZzpQ1Q9OLBU7ONHHojG4,267607
|
|
167
167
|
modal_proto/api_pb2_grpc.pyi,sha256=Wy6NAO-o06jbQHxvZiMAi08QQT8VHUJcSAmlRRL0p1M,62762
|
|
168
168
|
modal_proto/modal_api_grpc.py,sha256=A622btdK6V3CFaZfXK63ocCUlulKYJsgM-rcEPEFv24,18703
|
|
@@ -174,10 +174,10 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
|
|
|
174
174
|
modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
|
175
175
|
modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
|
|
176
176
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
177
|
-
modal_version/__init__.py,sha256=
|
|
177
|
+
modal_version/__init__.py,sha256=3xrphd0fMXrGt-Xu74onNsNLzncvzfMuebAoJjDqW90,121
|
|
178
178
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
|
179
|
-
modal-1.1.1.
|
|
180
|
-
modal-1.1.1.
|
|
181
|
-
modal-1.1.1.
|
|
182
|
-
modal-1.1.1.
|
|
183
|
-
modal-1.1.1.
|
|
179
|
+
modal-1.1.1.dev18.dist-info/METADATA,sha256=pC2CJg1xV_-J6-Ji4ZutQ77ROscU0LzEVTs_6DTxI6c,2462
|
|
180
|
+
modal-1.1.1.dev18.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
181
|
+
modal-1.1.1.dev18.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
|
182
|
+
modal-1.1.1.dev18.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
|
|
183
|
+
modal-1.1.1.dev18.dist-info/RECORD,,
|
modal_proto/api.proto
CHANGED
|
@@ -240,6 +240,13 @@ enum SystemErrorCode {
|
|
|
240
240
|
SYSTEM_ERROR_CODE_NOSPC = 28; // ENOSPC: No space left on device
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
enum TaskSnapshotBehavior {
|
|
244
|
+
TASK_SNAPSHOT_BEHAVIOR_UNSPECIFIED = 0;
|
|
245
|
+
TASK_SNAPSHOT_BEHAVIOR_SNAPSHOT = 1;
|
|
246
|
+
TASK_SNAPSHOT_BEHAVIOR_RESTORE = 2;
|
|
247
|
+
TASK_SNAPSHOT_BEHAVIOR_NONE = 3;
|
|
248
|
+
}
|
|
249
|
+
|
|
243
250
|
enum TaskState {
|
|
244
251
|
TASK_STATE_UNSPECIFIED = 0;
|
|
245
252
|
TASK_STATE_CREATED = 6;
|
|
@@ -2961,6 +2968,7 @@ message TaskInfo {
|
|
|
2961
2968
|
double enqueued_at = 5;
|
|
2962
2969
|
string gpu_type = 6;
|
|
2963
2970
|
string sandbox_id = 7;
|
|
2971
|
+
TaskSnapshotBehavior snapshot_behavior = 8;
|
|
2964
2972
|
}
|
|
2965
2973
|
|
|
2966
2974
|
message TaskListRequest {
|