modal 1.2.1.dev8__py3-none-any.whl → 1.2.2.dev19__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.
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +4 -1
- modal/_functions.py +33 -49
- modal/_grpc_client.py +148 -0
- modal/_output.py +3 -4
- modal/_partial_function.py +22 -2
- modal/_runtime/container_io_manager.py +21 -22
- modal/_utils/async_utils.py +12 -3
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +3 -4
- modal/_utils/function_utils.py +4 -0
- modal/_utils/grpc_utils.py +80 -51
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/task_command_router_client.py +536 -0
- modal/app.py +7 -5
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/entry_point.py +1 -0
- modal/cli/launch.py +1 -2
- modal/cli/network_file_system.py +1 -4
- modal/cli/queues.py +1 -2
- modal/cli/secret.py +1 -2
- modal/client.py +5 -115
- modal/client.pyi +2 -91
- modal/cls.py +1 -2
- modal/config.py +3 -1
- modal/container_process.py +287 -11
- modal/container_process.pyi +95 -32
- modal/dict.py +12 -12
- modal/environments.py +1 -2
- modal/exception.py +4 -0
- modal/experimental/__init__.py +2 -3
- modal/experimental/flash.py +27 -57
- modal/experimental/flash.pyi +6 -20
- modal/file_io.py +13 -27
- modal/functions.pyi +6 -6
- modal/image.py +24 -3
- modal/image.pyi +4 -0
- modal/io_streams.py +433 -127
- modal/io_streams.pyi +236 -171
- modal/mount.py +4 -4
- modal/network_file_system.py +5 -6
- modal/parallel_map.py +29 -31
- modal/parallel_map.pyi +3 -9
- modal/partial_function.pyi +4 -1
- modal/queue.py +17 -18
- modal/runner.py +12 -11
- modal/sandbox.py +148 -42
- modal/sandbox.pyi +139 -0
- modal/secret.py +4 -5
- modal/snapshot.py +1 -4
- modal/token_flow.py +1 -1
- modal/volume.py +22 -22
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/METADATA +1 -1
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/RECORD +70 -68
- modal_proto/api.proto +2 -24
- modal_proto/api_grpc.py +0 -32
- modal_proto/api_pb2.py +838 -878
- modal_proto/api_pb2.pyi +8 -70
- modal_proto/api_pb2_grpc.py +0 -67
- modal_proto/api_pb2_grpc.pyi +0 -22
- modal_proto/modal_api_grpc.py +175 -177
- modal_proto/sandbox_router.proto +0 -4
- modal_proto/sandbox_router_pb2.pyi +0 -4
- modal_version/__init__.py +1 -1
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/WHEEL +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/entry_points.txt +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/top_level.txt +0 -0
modal/experimental/__init__.py
CHANGED
|
@@ -13,7 +13,6 @@ from .._object import _get_environment_name
|
|
|
13
13
|
from .._partial_function import _clustered
|
|
14
14
|
from .._runtime.container_io_manager import _ContainerIOManager
|
|
15
15
|
from .._utils.async_utils import synchronize_api, synchronizer
|
|
16
|
-
from .._utils.grpc_utils import retry_transient_errors
|
|
17
16
|
from ..app import _App
|
|
18
17
|
from ..client import _Client
|
|
19
18
|
from ..cls import _Cls
|
|
@@ -116,7 +115,7 @@ async def get_app_objects(
|
|
|
116
115
|
|
|
117
116
|
app = await _App.lookup(app_name, environment_name=environment_name, client=client)
|
|
118
117
|
req = api_pb2.AppGetLayoutRequest(app_id=app.app_id)
|
|
119
|
-
app_layout_resp = await
|
|
118
|
+
app_layout_resp = await client.stub.AppGetLayout(req)
|
|
120
119
|
|
|
121
120
|
app_objects: dict[str, Union[_Function, _Cls]] = {}
|
|
122
121
|
|
|
@@ -361,4 +360,4 @@ async def image_delete(
|
|
|
361
360
|
client = await _Client.from_env()
|
|
362
361
|
|
|
363
362
|
req = api_pb2.ImageDeleteRequest(image_id=image_id)
|
|
364
|
-
await
|
|
363
|
+
await client.stub.ImageDelete(req)
|
modal/experimental/flash.py
CHANGED
|
@@ -16,7 +16,6 @@ from modal_proto import api_pb2
|
|
|
16
16
|
|
|
17
17
|
from .._tunnel import _forward as _forward_tunnel
|
|
18
18
|
from .._utils.async_utils import synchronize_api, synchronizer
|
|
19
|
-
from .._utils.grpc_utils import retry_transient_errors
|
|
20
19
|
from ..client import _Client
|
|
21
20
|
from ..config import logger
|
|
22
21
|
from ..exception import InvalidError
|
|
@@ -126,10 +125,8 @@ class _FlashManager:
|
|
|
126
125
|
f"due to error: {port_check_error}, num_failures: {self.num_failures}"
|
|
127
126
|
)
|
|
128
127
|
self.num_failures += 1
|
|
129
|
-
await
|
|
130
|
-
|
|
131
|
-
api_pb2.FlashContainerDeregisterRequest(),
|
|
132
|
-
)
|
|
128
|
+
await self.client.stub.FlashContainerDeregister(api_pb2.FlashContainerDeregisterRequest())
|
|
129
|
+
|
|
133
130
|
except asyncio.CancelledError:
|
|
134
131
|
logger.warning("[Modal Flash] Shutting down...")
|
|
135
132
|
break
|
|
@@ -148,8 +145,7 @@ class _FlashManager:
|
|
|
148
145
|
|
|
149
146
|
async def stop(self):
|
|
150
147
|
self.heartbeat_task.cancel()
|
|
151
|
-
await
|
|
152
|
-
self.client.stub.FlashContainerDeregister,
|
|
148
|
+
await self.client.stub.FlashContainerDeregister(
|
|
153
149
|
api_pb2.FlashContainerDeregisterRequest(),
|
|
154
150
|
)
|
|
155
151
|
|
|
@@ -321,7 +317,7 @@ class _FlashPrometheusAutoscaler:
|
|
|
321
317
|
|
|
322
318
|
async def _compute_target_containers(self, current_replicas: int) -> int:
|
|
323
319
|
"""
|
|
324
|
-
Gets
|
|
320
|
+
Gets metrics from container to autoscale up or down.
|
|
325
321
|
"""
|
|
326
322
|
containers = await self._get_all_containers()
|
|
327
323
|
if len(containers) > current_replicas:
|
|
@@ -334,7 +330,7 @@ class _FlashPrometheusAutoscaler:
|
|
|
334
330
|
if current_replicas == 0:
|
|
335
331
|
return 1
|
|
336
332
|
|
|
337
|
-
# Get metrics based on autoscaler type
|
|
333
|
+
# Get metrics based on autoscaler type
|
|
338
334
|
sum_metric, n_containers_with_metrics = await self._get_scaling_info(containers)
|
|
339
335
|
|
|
340
336
|
desired_replicas = self._calculate_desired_replicas(
|
|
@@ -406,39 +402,26 @@ class _FlashPrometheusAutoscaler:
|
|
|
406
402
|
return desired_replicas
|
|
407
403
|
|
|
408
404
|
async def _get_scaling_info(self, containers) -> tuple[float, int]:
|
|
409
|
-
"""Get metrics using
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
sum_metric = sum(container_metrics_list)
|
|
421
|
-
n_containers_with_metrics = len(container_metrics_list)
|
|
422
|
-
else:
|
|
423
|
-
sum_metric = 0
|
|
424
|
-
n_containers_with_metrics = 0
|
|
425
|
-
|
|
426
|
-
container_metrics_list = await asyncio.gather(
|
|
427
|
-
*[
|
|
428
|
-
self._get_metrics(f"https://{container.host}:{container.port}/{self.metrics_endpoint}")
|
|
429
|
-
for container in containers
|
|
430
|
-
]
|
|
431
|
-
)
|
|
405
|
+
"""Get metrics using container exposed metrics endpoints."""
|
|
406
|
+
sum_metric = 0
|
|
407
|
+
n_containers_with_metrics = 0
|
|
408
|
+
|
|
409
|
+
container_metrics_list = await asyncio.gather(
|
|
410
|
+
*[
|
|
411
|
+
self._get_metrics(f"https://{container.host}:{container.port}/{self.metrics_endpoint}")
|
|
412
|
+
for container in containers
|
|
413
|
+
]
|
|
414
|
+
)
|
|
432
415
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
416
|
+
for container_metrics in container_metrics_list:
|
|
417
|
+
if (
|
|
418
|
+
container_metrics is None
|
|
419
|
+
or self.target_metric not in container_metrics
|
|
420
|
+
or len(container_metrics[self.target_metric]) == 0
|
|
421
|
+
):
|
|
422
|
+
continue
|
|
423
|
+
sum_metric += container_metrics[self.target_metric][0].value
|
|
424
|
+
n_containers_with_metrics += 1
|
|
442
425
|
|
|
443
426
|
return sum_metric, n_containers_with_metrics
|
|
444
427
|
|
|
@@ -474,23 +457,14 @@ class _FlashPrometheusAutoscaler:
|
|
|
474
457
|
|
|
475
458
|
return metrics
|
|
476
459
|
|
|
477
|
-
async def _get_container_metrics(self, container_id: str) -> Optional[api_pb2.TaskGetAutoscalingMetricsResponse]:
|
|
478
|
-
req = api_pb2.TaskGetAutoscalingMetricsRequest(task_id=container_id)
|
|
479
|
-
try:
|
|
480
|
-
resp = await retry_transient_errors(self.client.stub.TaskGetAutoscalingMetrics, req)
|
|
481
|
-
return resp
|
|
482
|
-
except Exception as e:
|
|
483
|
-
logger.warning(f"[Modal Flash] Error getting metrics for container {container_id}: {e}")
|
|
484
|
-
return None
|
|
485
|
-
|
|
486
460
|
async def _get_all_containers(self):
|
|
487
461
|
req = api_pb2.FlashContainerListRequest(function_id=self.fn.object_id)
|
|
488
|
-
resp = await
|
|
462
|
+
resp = await self.client.stub.FlashContainerList(req)
|
|
489
463
|
return resp.containers
|
|
490
464
|
|
|
491
465
|
async def _set_target_slots(self, target_slots: int):
|
|
492
466
|
req = api_pb2.FlashSetTargetSlotsMetricsRequest(function_id=self.fn.object_id, target_slots=target_slots)
|
|
493
|
-
await
|
|
467
|
+
await self.client.stub.FlashSetTargetSlotsMetrics(req)
|
|
494
468
|
return
|
|
495
469
|
|
|
496
470
|
def _make_scaling_decision(
|
|
@@ -572,14 +546,10 @@ async def flash_prometheus_autoscaler(
|
|
|
572
546
|
app_name: str,
|
|
573
547
|
cls_name: str,
|
|
574
548
|
# Endpoint to fetch metrics from. Must be in Prometheus format. Example: "/metrics"
|
|
575
|
-
# If metrics_endpoint is "internal", we will use containers' internal metrics to autoscale instead.
|
|
576
549
|
metrics_endpoint: str,
|
|
577
550
|
# Target metric to autoscale on. Example: "vllm:num_requests_running"
|
|
578
|
-
# If metrics_endpoint is "internal", target_metrics options are: [cpu_usage_percent, memory_usage_percent]
|
|
579
551
|
target_metric: str,
|
|
580
552
|
# Target metric value. Example: 25
|
|
581
|
-
# If metrics_endpoint is "internal", target_metric_value is a percentage value between 0.1 and 1.0 (inclusive),
|
|
582
|
-
# indicating container's usage of that metric.
|
|
583
553
|
target_metric_value: float,
|
|
584
554
|
min_containers: Optional[int] = None,
|
|
585
555
|
max_containers: Optional[int] = None,
|
|
@@ -645,5 +615,5 @@ async def flash_get_containers(app_name: str, cls_name: str) -> list[dict[str, A
|
|
|
645
615
|
assert fn is not None
|
|
646
616
|
await fn.hydrate(client=client)
|
|
647
617
|
req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
|
|
648
|
-
resp = await
|
|
618
|
+
resp = await client.stub.FlashContainerList(req)
|
|
649
619
|
return resp.containers
|
modal/experimental/flash.pyi
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import modal.client
|
|
2
|
-
import modal_proto.api_pb2
|
|
3
2
|
import subprocess
|
|
4
3
|
import typing
|
|
5
4
|
import typing_extensions
|
|
@@ -139,7 +138,7 @@ class _FlashPrometheusAutoscaler:
|
|
|
139
138
|
async def start(self): ...
|
|
140
139
|
async def _run_autoscaler_loop(self): ...
|
|
141
140
|
async def _compute_target_containers(self, current_replicas: int) -> int:
|
|
142
|
-
"""Gets
|
|
141
|
+
"""Gets metrics from container to autoscale up or down."""
|
|
143
142
|
...
|
|
144
143
|
|
|
145
144
|
def _calculate_desired_replicas(
|
|
@@ -154,13 +153,10 @@ class _FlashPrometheusAutoscaler:
|
|
|
154
153
|
...
|
|
155
154
|
|
|
156
155
|
async def _get_scaling_info(self, containers) -> tuple[float, int]:
|
|
157
|
-
"""Get metrics using
|
|
156
|
+
"""Get metrics using container exposed metrics endpoints."""
|
|
158
157
|
...
|
|
159
158
|
|
|
160
159
|
async def _get_metrics(self, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
|
|
161
|
-
async def _get_container_metrics(
|
|
162
|
-
self, container_id: str
|
|
163
|
-
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
|
164
160
|
async def _get_all_containers(self): ...
|
|
165
161
|
async def _set_target_slots(self, target_slots: int): ...
|
|
166
162
|
def _make_scaling_decision(
|
|
@@ -226,11 +222,11 @@ class FlashPrometheusAutoscaler:
|
|
|
226
222
|
|
|
227
223
|
class ___compute_target_containers_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
228
224
|
def __call__(self, /, current_replicas: int) -> int:
|
|
229
|
-
"""Gets
|
|
225
|
+
"""Gets metrics from container to autoscale up or down."""
|
|
230
226
|
...
|
|
231
227
|
|
|
232
228
|
async def aio(self, /, current_replicas: int) -> int:
|
|
233
|
-
"""Gets
|
|
229
|
+
"""Gets metrics from container to autoscale up or down."""
|
|
234
230
|
...
|
|
235
231
|
|
|
236
232
|
_compute_target_containers: ___compute_target_containers_spec[typing_extensions.Self]
|
|
@@ -248,11 +244,11 @@ class FlashPrometheusAutoscaler:
|
|
|
248
244
|
|
|
249
245
|
class ___get_scaling_info_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
250
246
|
def __call__(self, /, containers) -> tuple[float, int]:
|
|
251
|
-
"""Get metrics using
|
|
247
|
+
"""Get metrics using container exposed metrics endpoints."""
|
|
252
248
|
...
|
|
253
249
|
|
|
254
250
|
async def aio(self, /, containers) -> tuple[float, int]:
|
|
255
|
-
"""Get metrics using
|
|
251
|
+
"""Get metrics using container exposed metrics endpoints."""
|
|
256
252
|
...
|
|
257
253
|
|
|
258
254
|
_get_scaling_info: ___get_scaling_info_spec[typing_extensions.Self]
|
|
@@ -263,16 +259,6 @@ class FlashPrometheusAutoscaler:
|
|
|
263
259
|
|
|
264
260
|
_get_metrics: ___get_metrics_spec[typing_extensions.Self]
|
|
265
261
|
|
|
266
|
-
class ___get_container_metrics_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
267
|
-
def __call__(
|
|
268
|
-
self, /, container_id: str
|
|
269
|
-
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
|
270
|
-
async def aio(
|
|
271
|
-
self, /, container_id: str
|
|
272
|
-
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
|
273
|
-
|
|
274
|
-
_get_container_metrics: ___get_container_metrics_spec[typing_extensions.Self]
|
|
275
|
-
|
|
276
262
|
class ___get_all_containers_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
277
263
|
def __call__(self, /): ...
|
|
278
264
|
async def aio(self, /): ...
|
modal/file_io.py
CHANGED
|
@@ -13,7 +13,6 @@ import json
|
|
|
13
13
|
from grpclib.exceptions import GRPCError, StreamTerminatedError
|
|
14
14
|
|
|
15
15
|
from modal._utils.async_utils import TaskContext
|
|
16
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
17
16
|
from modal.exception import ClientClosed
|
|
18
17
|
from modal_proto import api_pb2
|
|
19
18
|
|
|
@@ -57,8 +56,7 @@ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optio
|
|
|
57
56
|
if start is not None and end is not None:
|
|
58
57
|
if start >= end:
|
|
59
58
|
raise ValueError("start must be less than end")
|
|
60
|
-
resp = await
|
|
61
|
-
file._client.stub.ContainerFilesystemExec,
|
|
59
|
+
resp = await file._client.stub.ContainerFilesystemExec(
|
|
62
60
|
api_pb2.ContainerFilesystemExecRequest(
|
|
63
61
|
file_delete_bytes_request=api_pb2.ContainerFileDeleteBytesRequest(
|
|
64
62
|
file_descriptor=file._file_descriptor,
|
|
@@ -85,8 +83,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
|
|
|
85
83
|
raise InvalidError("start must be less than end")
|
|
86
84
|
if len(data) > WRITE_CHUNK_SIZE:
|
|
87
85
|
raise InvalidError("Write request payload exceeds 16 MiB limit")
|
|
88
|
-
resp = await
|
|
89
|
-
file._client.stub.ContainerFilesystemExec,
|
|
86
|
+
resp = await file._client.stub.ContainerFilesystemExec(
|
|
90
87
|
api_pb2.ContainerFilesystemExecRequest(
|
|
91
88
|
file_write_replace_bytes_request=api_pb2.ContainerFileWriteReplaceBytesRequest(
|
|
92
89
|
file_descriptor=file._file_descriptor,
|
|
@@ -261,8 +258,7 @@ class _FileIO(Generic[T]):
|
|
|
261
258
|
raise TypeError("Expected str when in text mode")
|
|
262
259
|
|
|
263
260
|
async def _open_file(self, path: str, mode: str) -> None:
|
|
264
|
-
resp = await
|
|
265
|
-
self._client.stub.ContainerFilesystemExec,
|
|
261
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
266
262
|
api_pb2.ContainerFilesystemExecRequest(
|
|
267
263
|
file_open_request=api_pb2.ContainerFileOpenRequest(path=path, mode=mode),
|
|
268
264
|
task_id=self._task_id,
|
|
@@ -285,8 +281,7 @@ class _FileIO(Generic[T]):
|
|
|
285
281
|
return self
|
|
286
282
|
|
|
287
283
|
async def _make_read_request(self, n: Optional[int]) -> bytes:
|
|
288
|
-
resp = await
|
|
289
|
-
self._client.stub.ContainerFilesystemExec,
|
|
284
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
290
285
|
api_pb2.ContainerFilesystemExecRequest(
|
|
291
286
|
file_read_request=api_pb2.ContainerFileReadRequest(file_descriptor=self._file_descriptor, n=n),
|
|
292
287
|
task_id=self._task_id,
|
|
@@ -309,8 +304,7 @@ class _FileIO(Generic[T]):
|
|
|
309
304
|
"""Read a single line from the current position."""
|
|
310
305
|
self._check_closed()
|
|
311
306
|
self._check_readable()
|
|
312
|
-
resp = await
|
|
313
|
-
self._client.stub.ContainerFilesystemExec,
|
|
307
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
314
308
|
api_pb2.ContainerFilesystemExecRequest(
|
|
315
309
|
file_read_line_request=api_pb2.ContainerFileReadLineRequest(file_descriptor=self._file_descriptor),
|
|
316
310
|
task_id=self._task_id,
|
|
@@ -351,8 +345,7 @@ class _FileIO(Generic[T]):
|
|
|
351
345
|
raise ValueError("Write request payload exceeds 1 GiB limit")
|
|
352
346
|
for i in range(0, len(data), WRITE_CHUNK_SIZE):
|
|
353
347
|
chunk = data[i : i + WRITE_CHUNK_SIZE]
|
|
354
|
-
resp = await
|
|
355
|
-
self._client.stub.ContainerFilesystemExec,
|
|
348
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
356
349
|
api_pb2.ContainerFilesystemExecRequest(
|
|
357
350
|
file_write_request=api_pb2.ContainerFileWriteRequest(
|
|
358
351
|
file_descriptor=self._file_descriptor,
|
|
@@ -367,8 +360,7 @@ class _FileIO(Generic[T]):
|
|
|
367
360
|
"""Flush the buffer to disk."""
|
|
368
361
|
self._check_closed()
|
|
369
362
|
self._check_writable()
|
|
370
|
-
resp = await
|
|
371
|
-
self._client.stub.ContainerFilesystemExec,
|
|
363
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
372
364
|
api_pb2.ContainerFilesystemExecRequest(
|
|
373
365
|
file_flush_request=api_pb2.ContainerFileFlushRequest(file_descriptor=self._file_descriptor),
|
|
374
366
|
task_id=self._task_id,
|
|
@@ -393,8 +385,7 @@ class _FileIO(Generic[T]):
|
|
|
393
385
|
(relative to the current position) and 2 (relative to the file's end).
|
|
394
386
|
"""
|
|
395
387
|
self._check_closed()
|
|
396
|
-
resp = await
|
|
397
|
-
self._client.stub.ContainerFilesystemExec,
|
|
388
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
398
389
|
api_pb2.ContainerFilesystemExecRequest(
|
|
399
390
|
file_seek_request=api_pb2.ContainerFileSeekRequest(
|
|
400
391
|
file_descriptor=self._file_descriptor,
|
|
@@ -410,8 +401,7 @@ class _FileIO(Generic[T]):
|
|
|
410
401
|
async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
|
|
411
402
|
"""List the contents of the provided directory."""
|
|
412
403
|
self = _FileIO(client, task_id)
|
|
413
|
-
resp = await
|
|
414
|
-
self._client.stub.ContainerFilesystemExec,
|
|
404
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
415
405
|
api_pb2.ContainerFilesystemExecRequest(
|
|
416
406
|
file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
|
|
417
407
|
task_id=task_id,
|
|
@@ -427,8 +417,7 @@ class _FileIO(Generic[T]):
|
|
|
427
417
|
async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
|
|
428
418
|
"""Create a new directory."""
|
|
429
419
|
self = _FileIO(client, task_id)
|
|
430
|
-
resp = await
|
|
431
|
-
self._client.stub.ContainerFilesystemExec,
|
|
420
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
432
421
|
api_pb2.ContainerFilesystemExecRequest(
|
|
433
422
|
file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
|
|
434
423
|
task_id=self._task_id,
|
|
@@ -440,8 +429,7 @@ class _FileIO(Generic[T]):
|
|
|
440
429
|
async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
|
|
441
430
|
"""Remove a file or directory in the Sandbox."""
|
|
442
431
|
self = _FileIO(client, task_id)
|
|
443
|
-
resp = await
|
|
444
|
-
self._client.stub.ContainerFilesystemExec,
|
|
432
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
445
433
|
api_pb2.ContainerFilesystemExecRequest(
|
|
446
434
|
file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
|
|
447
435
|
task_id=self._task_id,
|
|
@@ -460,8 +448,7 @@ class _FileIO(Generic[T]):
|
|
|
460
448
|
timeout: Optional[int] = None,
|
|
461
449
|
) -> AsyncIterator[FileWatchEvent]:
|
|
462
450
|
self = _FileIO(client, task_id)
|
|
463
|
-
resp = await
|
|
464
|
-
self._client.stub.ContainerFilesystemExec,
|
|
451
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
465
452
|
api_pb2.ContainerFilesystemExecRequest(
|
|
466
453
|
file_watch_request=api_pb2.ContainerFileWatchRequest(
|
|
467
454
|
path=path,
|
|
@@ -503,8 +490,7 @@ class _FileIO(Generic[T]):
|
|
|
503
490
|
|
|
504
491
|
async def _close(self) -> None:
|
|
505
492
|
# Buffer is flushed by the runner on close
|
|
506
|
-
resp = await
|
|
507
|
-
self._client.stub.ContainerFilesystemExec,
|
|
493
|
+
resp = await self._client.stub.ContainerFilesystemExec(
|
|
508
494
|
api_pb2.ContainerFilesystemExecRequest(
|
|
509
495
|
file_close_request=api_pb2.ContainerFileCloseRequest(file_descriptor=self._file_descriptor),
|
|
510
496
|
task_id=self._task_id,
|
modal/functions.pyi
CHANGED
|
@@ -401,7 +401,7 @@ class Function(
|
|
|
401
401
|
|
|
402
402
|
_call_generator: ___call_generator_spec[typing_extensions.Self]
|
|
403
403
|
|
|
404
|
-
class __remote_spec(typing_extensions.Protocol[
|
|
404
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
405
405
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
|
|
406
406
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
407
407
|
...
|
|
@@ -410,7 +410,7 @@ class Function(
|
|
|
410
410
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
411
411
|
...
|
|
412
412
|
|
|
413
|
-
remote: __remote_spec[modal._functions.
|
|
413
|
+
remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
|
|
414
414
|
|
|
415
415
|
class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
416
416
|
def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
|
|
@@ -437,7 +437,7 @@ class Function(
|
|
|
437
437
|
"""
|
|
438
438
|
...
|
|
439
439
|
|
|
440
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
|
440
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
441
441
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
442
442
|
"""[Experimental] Calls the function with the given arguments, without waiting for the results.
|
|
443
443
|
|
|
@@ -461,7 +461,7 @@ class Function(
|
|
|
461
461
|
...
|
|
462
462
|
|
|
463
463
|
_experimental_spawn: ___experimental_spawn_spec[
|
|
464
|
-
modal._functions.
|
|
464
|
+
modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
|
|
465
465
|
]
|
|
466
466
|
|
|
467
467
|
class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
|
|
@@ -470,7 +470,7 @@ class Function(
|
|
|
470
470
|
|
|
471
471
|
_spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
|
|
472
472
|
|
|
473
|
-
class __spawn_spec(typing_extensions.Protocol[
|
|
473
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
|
|
474
474
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
475
475
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
476
476
|
|
|
@@ -491,7 +491,7 @@ class Function(
|
|
|
491
491
|
"""
|
|
492
492
|
...
|
|
493
493
|
|
|
494
|
-
spawn: __spawn_spec[modal._functions.
|
|
494
|
+
spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
|
|
495
495
|
|
|
496
496
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
|
|
497
497
|
"""Return the inner Python object wrapped by this Modal Function."""
|
modal/image.py
CHANGED
|
@@ -38,7 +38,8 @@ from ._utils.docker_utils import (
|
|
|
38
38
|
find_dockerignore_file,
|
|
39
39
|
)
|
|
40
40
|
from ._utils.function_utils import FunctionInfo
|
|
41
|
-
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
41
|
+
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
42
|
+
from ._utils.mount_utils import validate_only_modal_volumes
|
|
42
43
|
from .client import _Client
|
|
43
44
|
from .cloud_bucket_mount import _CloudBucketMount
|
|
44
45
|
from .config import config, logger, user_config_path
|
|
@@ -487,6 +488,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
487
488
|
context_mount_function: Optional[Callable[[], Optional[_Mount]]] = None,
|
|
488
489
|
force_build: bool = False,
|
|
489
490
|
build_args: dict[str, str] = {},
|
|
491
|
+
validated_volumes: Optional[Sequence[tuple[str, _Volume]]] = None,
|
|
490
492
|
# For internal use only.
|
|
491
493
|
_namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
492
494
|
_do_assert_no_mount_layers: bool = True,
|
|
@@ -494,6 +496,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
494
496
|
if base_images is None:
|
|
495
497
|
base_images = {}
|
|
496
498
|
|
|
499
|
+
if validated_volumes is None:
|
|
500
|
+
validated_volumes = []
|
|
501
|
+
|
|
497
502
|
if secrets is None:
|
|
498
503
|
secrets = []
|
|
499
504
|
if gpu_config is None:
|
|
@@ -514,6 +519,8 @@ class _Image(_Object, type_prefix="im"):
|
|
|
514
519
|
deps += (build_function,)
|
|
515
520
|
if image_registry_config and image_registry_config.secret:
|
|
516
521
|
deps += (image_registry_config.secret,)
|
|
522
|
+
for _, vol in validated_volumes:
|
|
523
|
+
deps += (vol,)
|
|
517
524
|
return deps
|
|
518
525
|
|
|
519
526
|
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
|
@@ -592,6 +599,17 @@ class _Image(_Object, type_prefix="im"):
|
|
|
592
599
|
build_function_id = ""
|
|
593
600
|
_build_function = None
|
|
594
601
|
|
|
602
|
+
# Relies on dicts being ordered (true as of Python 3.6).
|
|
603
|
+
volume_mounts = [
|
|
604
|
+
api_pb2.VolumeMount(
|
|
605
|
+
mount_path=path,
|
|
606
|
+
volume_id=volume.object_id,
|
|
607
|
+
allow_background_commits=True,
|
|
608
|
+
read_only=volume._read_only,
|
|
609
|
+
)
|
|
610
|
+
for path, volume in validated_volumes
|
|
611
|
+
]
|
|
612
|
+
|
|
595
613
|
image_definition = api_pb2.Image(
|
|
596
614
|
base_images=base_images_pb2s,
|
|
597
615
|
dockerfile_commands=dockerfile.commands,
|
|
@@ -604,6 +622,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
604
622
|
runtime_debug=config.get("function_runtime_debug"),
|
|
605
623
|
build_function=_build_function,
|
|
606
624
|
build_args=build_args,
|
|
625
|
+
volume_mounts=volume_mounts,
|
|
607
626
|
)
|
|
608
627
|
|
|
609
628
|
req = api_pb2.ImageGetOrCreateRequest(
|
|
@@ -619,7 +638,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
619
638
|
allow_global_deployment=os.environ.get("MODAL_IMAGE_ALLOW_GLOBAL_DEPLOYMENT") == "1",
|
|
620
639
|
ignore_cache=config.get("ignore_cache"),
|
|
621
640
|
)
|
|
622
|
-
resp = await
|
|
641
|
+
resp = await resolver.client.stub.ImageGetOrCreate(req)
|
|
623
642
|
image_id = resp.image_id
|
|
624
643
|
result: api_pb2.GenericResult
|
|
625
644
|
metadata: Optional[api_pb2.ImageMetadata] = None
|
|
@@ -848,7 +867,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
848
867
|
client = await _Client.from_env()
|
|
849
868
|
|
|
850
869
|
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
|
851
|
-
resp = await
|
|
870
|
+
resp = await client.stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
|
|
852
871
|
self._hydrate(resp.image_id, resolver.client, resp.metadata)
|
|
853
872
|
|
|
854
873
|
rep = f"Image.from_id({image_id!r})"
|
|
@@ -1690,6 +1709,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1690
1709
|
*commands: Union[str, list[str]],
|
|
1691
1710
|
env: Optional[dict[str, Optional[str]]] = None,
|
|
1692
1711
|
secrets: Optional[Collection[_Secret]] = None,
|
|
1712
|
+
volumes: Optional[dict[Union[str, PurePosixPath], _Volume]] = None,
|
|
1693
1713
|
gpu: GPU_T = None,
|
|
1694
1714
|
force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
|
|
1695
1715
|
) -> "_Image":
|
|
@@ -1712,6 +1732,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1712
1732
|
secrets=secrets,
|
|
1713
1733
|
gpu_config=parse_gpu_config(gpu),
|
|
1714
1734
|
force_build=self.force_build or force_build,
|
|
1735
|
+
validated_volumes=validate_only_modal_volumes(volumes, "Image.run_commands"),
|
|
1715
1736
|
)
|
|
1716
1737
|
|
|
1717
1738
|
@staticmethod
|
modal/image.pyi
CHANGED
|
@@ -176,6 +176,7 @@ class _Image(modal._object._Object):
|
|
|
176
176
|
] = None,
|
|
177
177
|
force_build: bool = False,
|
|
178
178
|
build_args: dict[str, str] = {},
|
|
179
|
+
validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume._Volume]]] = None,
|
|
179
180
|
_namespace: int = 1,
|
|
180
181
|
_do_assert_no_mount_layers: bool = True,
|
|
181
182
|
): ...
|
|
@@ -668,6 +669,7 @@ class _Image(modal._object._Object):
|
|
|
668
669
|
*commands: typing.Union[str, list[str]],
|
|
669
670
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
670
671
|
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
672
|
+
volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]] = None,
|
|
671
673
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
672
674
|
force_build: bool = False,
|
|
673
675
|
) -> _Image:
|
|
@@ -1091,6 +1093,7 @@ class Image(modal.object.Object):
|
|
|
1091
1093
|
] = None,
|
|
1092
1094
|
force_build: bool = False,
|
|
1093
1095
|
build_args: dict[str, str] = {},
|
|
1096
|
+
validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume.Volume]]] = None,
|
|
1094
1097
|
_namespace: int = 1,
|
|
1095
1098
|
_do_assert_no_mount_layers: bool = True,
|
|
1096
1099
|
): ...
|
|
@@ -1648,6 +1651,7 @@ class Image(modal.object.Object):
|
|
|
1648
1651
|
*commands: typing.Union[str, list[str]],
|
|
1649
1652
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1650
1653
|
secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
|
|
1654
|
+
volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume]] = None,
|
|
1651
1655
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
1652
1656
|
force_build: bool = False,
|
|
1653
1657
|
) -> Image:
|