Skip to content

Commit e08c617

Browse files
committed
Merge remote-tracking branch 'origin/main' into freider/cbor2-function-calling
2 parents f8d3e29 + bfd92a2 commit e08c617

19 files changed

+831
-106
lines changed

CHANGELOG.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,14 @@ This changelog documents user-facing updates (features, enhancements, fixes, and
66

77
<!-- NEW CONTENT GENERATED BELOW. PLEASE PRESERVE THIS COMMENT. -->
88

9-
#### 1.1.4.dev11 (2025-08-22)
10-
11-
Forbid the use of `encrypted_ports`, `h2_ports`, and `unencrypted_ports` in Sandbox creation when `block_network` is `True`.
12-
13-
14-
#### 1.1.4.dev7 (2025-08-22)
15-
16-
The type returned by `modal.experimental.get_cluster_info()` now also includes the cluster ID - shared across the set of tasks that spin up in tandem when using the `@clustered` decorator.
17-
18-
19-
#### 1.1.4.dev5 (2025-08-21)
20-
21-
- Added an `idle_timeout` param to `Sandbox.create()` which, when provided, will have the sandbox terminate after `idle_timeout` seconds of idleness.
9+
### 1.1.4 (2025-09-03)
2210

11+
- Added a `startup_timeout` parameter to the `@app.function()` and `@app.cls()` decorators. When used, this configures the timeout applied to each container's startup period separately from the input `timeout`. For backwards compatibility, `timeout` still applies to the startup phase when `startup_timeout` is unset.
12+
- Added an optional `idle_timeout` parameter to `modal.Sandbox.create()`. When provided, Sandboxes will terminate after `idle_timeout` seconds of idleness.
13+
- The dataclass returned by `modal.experimental.get_cluster_info()` now includes a `cluster_id` field to identify the clustered set of containers.
14+
- When `block_network=True` is set in `modal.Sandbox.create()`, we now raise an error if any of `encrypted_ports`, `h2_ports`, or `unencrypted_ports` are also set.
15+
- Functions decorated with `@modal.asgi_app()` now return an HTTP 408 (request timeout) error code instead of a 502 (gateway timeout) in rare cases when an input fails to arrive at the container, e.g. due to cancellation.
16+
- `modal.Sandbox.create()` now warns when an invalid `name=` is passed, applying the same rules as other Modal object names: names must be alphanumeric and not longer than 64 characters. This will become an error in the future.
2317

2418
### 1.1.3 (2025-08-19)
2519

@@ -94,7 +88,7 @@ This release also includes a number of other new features and bug fixes:
9488
- Added a `build_args` parameter to `modal.Image.from_dockerfile` for passing arguments through to `ARG` instructions in the Dockerfile.
9589
- It's now possible to use `@modal.experimental.clustered` and `i6pn` networking with `modal.Cls`.
9690
- Fixed a bug where `Cls.with_options` would fail when provided with a `modal.Secret` object that was already hydrated.
97-
- Fixed a bug where the timeout specified in `modal.Sandbox.exec()` was not respected by `modal.Sandbox.wait()` or `modal.Sandbox.poll()`.
91+
- Fixed a bug where the timeout specified in `modal.Sandbox.exec()` was not respected by `ContainerProcess.wait()` or `ContainerProcess.poll()`.
9892
- Fixed retry handling when using `modal run --detach` directly against a remote Function.
9993

10094
Finally, this release introduces a small number of deprecations and potentially-breaking changes:

modal/_functions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ def from_local(
674674
proxy: Optional[_Proxy] = None,
675675
retries: Optional[Union[int, Retries]] = None,
676676
timeout: int = 300,
677+
startup_timeout: Optional[int] = None,
677678
min_containers: Optional[int] = None,
678679
max_containers: Optional[int] = None,
679680
buffer_containers: Optional[int] = None,
@@ -966,6 +967,7 @@ async def _load(self: _Function, resolver: Resolver, existing_object_id: Optiona
966967
proxy_id=(proxy.object_id if proxy else None),
967968
retry_policy=retry_policy,
968969
timeout_secs=timeout_secs or 0,
970+
startup_timeout_secs=startup_timeout or timeout_secs,
969971
pty_info=pty_info,
970972
cloud_provider_str=cloud if cloud else "",
971973
runtime=config.get("function_runtime"),
@@ -1019,6 +1021,7 @@ async def _load(self: _Function, resolver: Resolver, existing_object_id: Optiona
10191021
autoscaler_settings=function_definition.autoscaler_settings,
10201022
worker_id=function_definition.worker_id,
10211023
timeout_secs=function_definition.timeout_secs,
1024+
startup_timeout_secs=function_definition.startup_timeout_secs,
10221025
web_url=function_definition.web_url,
10231026
web_url_info=function_definition.web_url_info,
10241027
webhook_config=function_definition.webhook_config,
@@ -1471,6 +1474,7 @@ def _initialize_from_empty(self):
14711474
self._info = None
14721475
self._serve_mounts = frozenset()
14731476
self._metadata = None
1477+
self._experimental_flash_urls = None
14741478

14751479
def _hydrate_metadata(self, metadata: Optional[Message]):
14761480
# Overridden concrete implementation of base class method
@@ -1498,6 +1502,7 @@ def _hydrate_metadata(self, metadata: Optional[Message]):
14981502
self._max_object_size_bytes = (
14991503
metadata.max_object_size_bytes if metadata.HasField("max_object_size_bytes") else MAX_OBJECT_SIZE_BYTES
15001504
)
1505+
self._experimental_flash_urls = metadata._experimental_flash_urls
15011506

15021507
def _get_metadata(self):
15031508
# Overridden concrete implementation of base class method
@@ -1515,6 +1520,7 @@ def _get_metadata(self):
15151520
input_plane_url=self._input_plane_url,
15161521
input_plane_region=self._input_plane_region,
15171522
max_object_size_bytes=self._max_object_size_bytes,
1523+
_experimental_flash_urls=self._experimental_flash_urls,
15181524
)
15191525

15201526
def _check_no_web_url(self, fn_name: str):
@@ -1545,6 +1551,11 @@ async def get_web_url(self) -> Optional[str]:
15451551
"""URL of a Function running as a web endpoint."""
15461552
return self._web_url
15471553

1554+
@live_method
1555+
async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
1556+
"""URL of the flash service for the function."""
1557+
return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
1558+
15481559
@property
15491560
async def is_generator(self) -> bool:
15501561
"""mdmd:hidden"""

modal/_runtime/asgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ async def disconnect_app():
120120

121121
async def handle_first_input_timeout():
122122
if scope["type"] == "http":
123-
await messages_from_app.put({"type": "http.response.start", "status": 502})
123+
await messages_from_app.put({"type": "http.response.start", "status": 408})
124124
await messages_from_app.put(
125125
{
126126
"type": "http.response.body",

modal/app.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,8 @@ def function(
641641
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
642642
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
643643
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
644-
timeout: int = 300, # Maximum execution time in seconds.
644+
timeout: int = 300, # Maximum execution time for inputs and startup time in seconds.
645+
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
645646
name: Optional[str] = None, # Sets the Modal name of the function within the app
646647
is_generator: Optional[
647648
bool
@@ -816,6 +817,7 @@ def f(self, x):
816817
batch_max_size=batch_max_size,
817818
batch_wait_ms=batch_wait_ms,
818819
timeout=timeout,
820+
startup_timeout=startup_timeout or timeout,
819821
cloud=cloud,
820822
webhook_config=webhook_config,
821823
enable_memory_snapshot=enable_memory_snapshot,
@@ -869,7 +871,8 @@ def cls(
869871
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
870872
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
871873
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
872-
timeout: int = 300, # Maximum execution time in seconds; applies independently to startup and each input.
874+
timeout: int = 300, # Maximum execution time for inputs and startup time in seconds.
875+
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
873876
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
874877
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
875878
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
@@ -1002,6 +1005,7 @@ def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
10021005
batch_max_size=batch_max_size,
10031006
batch_wait_ms=batch_wait_ms,
10041007
timeout=timeout,
1008+
startup_timeout=startup_timeout or timeout,
10051009
cloud=cloud,
10061010
enable_memory_snapshot=enable_memory_snapshot,
10071011
block_network=block_network,

modal/cls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from modal_proto import api_pb2
1313

1414
from ._functions import _Function, _parse_retries
15-
from ._object import _Object
15+
from ._object import _Object, live_method
1616
from ._partial_function import (
1717
_find_callables_for_obj,
1818
_find_partial_methods_for_user_cls,
@@ -510,6 +510,11 @@ def _get_method_names(self) -> Collection[str]:
510510
# returns method names for a *local* class only for now (used by cli)
511511
return self._method_partials.keys()
512512

513+
@live_method
514+
async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
515+
"""URL of the flash service for the class."""
516+
return await self._get_class_service_function()._experimental_get_flash_urls()
517+
513518
def _hydrate_metadata(self, metadata: Message):
514519
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
515520
class_service_function = self._get_class_service_function()

modal/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
* `traceback` (in the .toml file) / `MODAL_TRACEBACK` (as an env var).
6767
Defaults to False. Enables printing full tracebacks on unexpected CLI
6868
errors, which can be useful for debugging client issues.
69-
* `log_pattern` (in the .toml file) / MODAL_LOG_PATTERN` (as an env var).
70-
Defaults to "[modal-client] %(asctime)s %(message)s"
69+
* `log_pattern` (in the .toml file) / `MODAL_LOG_PATTERN` (as an env var).
70+
Defaults to `"[modal-client] %(asctime)s %(message)s"`
7171
The log formatting pattern that will be used by the modal client itself.
7272
See https://docs.python.org/3/library/logging.html#logrecord-attributes for available
7373
log attributes.

modal/container_process.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ._utils.grpc_utils import retry_transient_errors
1111
from ._utils.shell_utils import stream_from_stdin, write_to_fd
1212
from .client import _Client
13+
from .config import logger
1314
from .exception import InteractiveTimeoutError, InvalidError
1415
from .io_streams import _StreamReader, _StreamWriter
1516
from .stream_type import StreamType
@@ -136,6 +137,7 @@ async def wait(self) -> int:
136137
self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
137138
except (asyncio.TimeoutError, TimeoutError):
138139
self._returncode = -1
140+
logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
139141
return self._returncode
140142

141143
async def attach(self):

modal/experimental/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,19 +311,17 @@ async def notebook_base_image(*, python_version: Optional[str] = None, force_bui
311311

312312
commands: list[str] = [
313313
"apt-get update",
314-
"apt-get install -y libpq-dev pkg-config cmake git curl wget unzip zip libsqlite3-dev openssh-server vim",
314+
"apt-get install -y "
315+
+ "libpq-dev pkg-config cmake git curl wget unzip zip libsqlite3-dev openssh-server vim ffmpeg",
315316
_install_cuda_command(),
316317
# Install uv since it's faster than pip for installing packages.
317318
"pip install uv",
318319
# https://github.com/astral-sh/uv/issues/11480
319-
"pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126",
320+
"pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu129",
320321
f"uv pip install --system {shlex.join(sorted(environment_packages))}",
321322
f"uv pip install --system {shlex.join(sorted(kernelshim_packages))}",
322323
]
323324

324-
# TODO: Also install the CUDA Toolkit, so `nvcc` is available.
325-
# https://github.com/charlesfrye/cuda-modal/blob/7fef8db12402986cf42d9c8cca8c63d1da6d7700/cuda/use_cuda.py#L158-L188
326-
327325
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
328326
return DockerfileSpec(
329327
commands=[

0 commit comments

Comments
 (0)