Skip to content

Commit

Permalink
Merge branch 'main' into deven/update_minimum_cpu_core_request
Browse files Browse the repository at this point in the history
  • Loading branch information
devennavani committed Aug 2, 2024
2 parents 7010e86 + b3c3b7e commit 3a44d32
Show file tree
Hide file tree
Showing 29 changed files with 262 additions and 127 deletions.
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ We appreciate your patience while we speedily work towards a stable release of t

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

### 0.64.2 (2024-08-02)

- Volumes can now be mounted to an ad hoc modal shell session:

```
modal shell --volume my-vol-name
```
When the shell starts, the volume will be mounted at `/mnt/my-vol-name`. This may be helpful for shell-based exploration or manipulation of volume contents.

Note that the option can be used multiple times to mount additional models:
```
modal shell --volume models --volume data
```



### 0.64.0 (2024-07-29)

- App deployment events are now atomic, reducing the risk that a failed deploy will leave the App in a bad state.



### 0.63.87 (2024-07-24)

* The `_experimental_boost` argument can now be removed. Boost is now enabled on all modal Functions.



### 0.63.77 (2024-07-18)

* Setting `_allow_background_volume_commits` is no longer necessary and has been deprecated. Remove this argument in your decorators.
Expand Down Expand Up @@ -90,7 +118,7 @@ Starting in this version, all `@methods` and web endpoints will be part of the s



### v0.62.230 (2024-06-18)
### 0.62.230 (2024-06-18)

- It is now an error to create or lookup Modal objects (`Volume`, `Dict`, `Secret`, etc.) with an invalid name. Object names must be shorter than 64 characters and may contain only alphanumeric characters, dashes, periods, and underscores. The name check had inadvertently been removed for a brief time following an internal refactor and then reintroduced as a warning. It is once more a hard error. Please get in touch if this is blocking access to your data.

Expand Down
1 change: 0 additions & 1 deletion modal/_container_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,6 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
if function_def.is_checkpointing_function:
container_io_manager.memory_snapshot()


# Install hooks for interactive functions.
if function_def.pty_info.pty_type != api_pb2.PTYInfo.PTY_TYPE_UNSPECIFIED:

Expand Down
16 changes: 5 additions & 11 deletions modal/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,12 @@ def step_progress(text: str = "") -> Spinner:
return Spinner(default_spinner, text, style="blue")


def step_progress_update(spinner: Spinner, message: str):
spinner.update(text=message)
def step_completed(message: str) -> RenderableType:
return f"[green]✓[/green] {message}"


def step_completed(message: str, is_substep: bool = False) -> RenderableType:
"""Returns the element to be rendered when a step is completed."""

STEP_COMPLETED = "[green]✓[/green]"
SUBSTEP_COMPLETED = "🔨"

symbol = SUBSTEP_COMPLETED if is_substep else STEP_COMPLETED
return f"{symbol} {message}"
def substep_completed(message: str) -> RenderableType:
return f"🔨 {message}"


def download_progress_bar() -> Progress:
Expand Down Expand Up @@ -313,7 +307,7 @@ def tasks_at_state(state):
message = f"[blue]{message}[/blue] [grey70]View app at [underline]{self._app_page_url}[/underline][/grey70]"

# Set the new message
step_progress_update(self._status_spinner, message)
self._status_spinner.update(text=message)

def update_snapshot_progress(self, image_id: str, task_progress: api_pb2.TaskProgress) -> None:
# TODO(erikbern): move this to sit on the resolver object, mostly
Expand Down
27 changes: 11 additions & 16 deletions modal/_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ def __init__(self, progress: "Optional[Tree]"):
self._step_node = progress.add(self._spinner)

def message(self, message):
from ._output import step_progress_update

if self._spinner is not None:
step_progress_update(self._spinner, message)
self._spinner.update(text=message)

def finish(self, message):
from ._output import step_completed, step_progress_update
from ._output import substep_completed

if self._step_node is not None:
step_progress_update(self._spinner, message)
self._step_node.label = step_completed(message, is_substep=True)
self._spinner.update(text=message)
self._step_node.label = substep_completed(message)


class Resolver:
Expand All @@ -52,7 +50,7 @@ class Resolver:

def __init__(
self,
client=None,
client: _Client,
*,
environment_name: Optional[str] = None,
app_id: Optional[str] = None,
Expand Down Expand Up @@ -127,15 +125,12 @@ async def loader():
raise

# Check that the id of functions and classes didn't change
# TODO(erikbern): revisit this once stub assignments have been disallowed
if not obj._is_another_app and (obj.object_id.startswith("fu-") or obj.object_id.startswith("cs-")):
# Persisted refs are ignored because their life cycle is managed independently.
# The same tag on an app can be pointed at different objects.
if existing_object_id is not None and obj.object_id != existing_object_id:
raise Exception(
f"Tried creating an object using existing id {existing_object_id}"
f" but it has id {obj.object_id}"
)
# Persisted refs are ignored because their life cycle is managed independently.
if not obj._is_another_app and existing_object_id is not None and obj.object_id != existing_object_id:
raise Exception(
f"Tried creating an object using existing id {existing_object_id}"
f" but it has id {obj.object_id}"
)

return obj

Expand Down
22 changes: 22 additions & 0 deletions modal/_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ def create_module(self, spec):
spec.loader = self
return module

def get_data(self, path: str) -> bytes:
"""
Implementation is required to support pkgutil.get_data.
> If the package cannot be located or loaded, or it uses a loader which does
> not support get_data, then None is returned.
ref: https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
"""
return self.loader.get_data(path)

def get_resource_reader(self, fullname: str):
"""
Support reading a binary artifact that is shipped within a package.
> Loaders that wish to support resource reading are expected to provide a method called
> get_resource_reader(fullname) which returns an object implementing this ABC’s interface.
ref: docs.python.org/3.10/library/importlib.html?highlight=traversableresources#importlib.abc.ResourceReader
"""
return self.loader.get_resource_reader(fullname)


class ImportInterceptor(importlib.abc.MetaPathFinder):
loading: typing.Dict[str, typing.Tuple[str, float]]
Expand Down
4 changes: 2 additions & 2 deletions modal/_tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def tcp_tunnel():
```
**SSH example:**
This assumes you have a rsa keypair in `~/.ssh/id_rsa{.pub}, this is a bare-bones example
This assumes you have a rsa keypair in `~/.ssh/id_rsa{.pub}`, this is a bare-bones example
letting you SSH into a Modal container.
```python
Expand All @@ -152,7 +152,7 @@ def some_function():
with modal.forward(port=22, unencrypted=True) as tunnel:
hostname, port = tunnel.tcp_socket
connection_cmd = f'ssh -p {port} root@{hostname}'
print(f"ssh into container using:\n{connection_cmd}")
print(f"ssh into container using: {connection_cmd}")
time.sleep(3600) # keep alive for 1 hour or until killed
```
Expand Down
2 changes: 1 addition & 1 deletion modal/_utils/blob_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def use_md5(url: str) -> bool:
https://github.com/spulec/moto/issues/816
"""
host = urlparse(url).netloc.split(":")[0]
if host.endswith(".amazonaws.com"):
if host.endswith(".amazonaws.com") or host.endswith(".r2.cloudflarestorage.com"):
return True
elif host in ["127.0.0.1", "localhost", "172.21.0.1"]:
return False
Expand Down
18 changes: 14 additions & 4 deletions modal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def registered_web_endpoints(self) -> List[str]:

def local_entrypoint(
self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
) -> Callable[[Callable[..., Any]], None]:
) -> Callable[[Callable[..., Any]], _LocalEntrypoint]:
"""Decorate a function to be used as a CLI entrypoint for a Modal App.
These functions can be used to define code that runs locally to set up the app,
Expand Down Expand Up @@ -488,7 +488,7 @@ def main(foo: int, bar: str):
if name is not None and not isinstance(name, str):
raise InvalidError("Invalid value for `name`: Must be string.")

def wrapped(raw_f: Callable[..., Any]) -> None:
def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
info = FunctionInfo(raw_f)
tag = name if name is not None else raw_f.__qualname__
if tag in self._local_entrypoints:
Expand Down Expand Up @@ -549,7 +549,7 @@ def function(
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
# Parameters below here are experimental. Use with caution!
_allow_background_volume_commits: None = None,
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
_experimental_boost: None = None, # Deprecated: lower latency function execution is now default.
_experimental_scheduler_placement: Optional[
SchedulerPlacement
] = None, # Experimental controls over fine-grained scheduling (alpha).
Expand All @@ -567,6 +567,11 @@ def function(
(2024, 5, 1), "interactive=True has been deprecated. Set MODAL_INTERACTIVE_FUNCTIONS=1 instead."
)

if _experimental_boost is not None:
deprecation_warning(
(2024, 7, 23), "`_experimental_boost` is now always-on. This argument is no longer needed."
)

if image is None:
image = self._get_default_image()

Expand Down Expand Up @@ -724,7 +729,7 @@ def cls(
interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
# Parameters below here are experimental. Use with caution!
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
_experimental_boost: None = None, # Deprecated: lower latency function execution is now default.
_experimental_scheduler_placement: Optional[
SchedulerPlacement
] = None, # Experimental controls over fine-grained scheduling (alpha).
Expand All @@ -738,6 +743,11 @@ def cls(
(2024, 5, 1), "interactive=True has been deprecated. Set MODAL_INTERACTIVE_FUNCTIONS=1 instead."
)

if _experimental_boost is not None:
deprecation_warning(
(2024, 7, 23), "`_experimental_boost` is now always-on. This argument is no longer needed."
)

if image is None:
image = self._get_default_image()

Expand Down
21 changes: 17 additions & 4 deletions modal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ..image import Image
from ..runner import deploy_app, interactive_shell, run_app
from ..serving import serve_app
from ..volume import Volume
from .import_refs import import_app, import_function
from .utils import ENV_OPTION, ENV_OPTION_HELP, stream_app_logs

Expand Down Expand Up @@ -278,10 +279,10 @@ def run(ctx, detach, quiet, interactive, env):

def deploy(
app_ref: str = typer.Argument(..., help="Path to a Python file with an app."),
name: str = typer.Option(None, help="Name of the deployment."),
name: str = typer.Option("", help="Name of the deployment."),
env: str = ENV_OPTION,
stream_logs: bool = typer.Option(False, help="Stream logs from the app upon deployment."),
tag: str = typer.Option(None, help="Tag the deployment with a version."),
tag: str = typer.Option("", help="Tag the deployment with a version."),
):
# this ensures that `modal.lookup()` without environment specification uses the same env as specified
env = ensure_env(env)
Expand All @@ -292,7 +293,7 @@ def deploy(
name = app.name

with enable_output():
res = deploy_app(app, name=name, environment_name=env, tag=tag)
res = deploy_app(app, name=name, environment_name=env or "", tag=tag)

if stream_logs:
stream_app_logs(res.app_id)
Expand Down Expand Up @@ -332,7 +333,10 @@ def serve(
def shell(
func_ref: Optional[str] = typer.Argument(
default=None,
help="Path to a Python file with an App or Modal function whose container to run.",
help=(
"Path to a Python file with an App or Modal function with container parameters."
" Can also include a function specifier, like `module.py::func`, when the file defines multiple functions."
),
metavar="FUNC_REF",
),
cmd: str = typer.Option(default="/bin/bash", help="Command to run inside the Modal image."),
Expand All @@ -341,6 +345,13 @@ def shell(
default=None, help="Container image tag for inside the shell (if not using FUNC_REF)."
),
add_python: Optional[str] = typer.Option(default=None, help="Add Python to the image (if not using FUNC_REF)."),
volume: Optional[typing.List[str]] = typer.Option(
default=None,
help=(
"Name of a `modal.Volume` to mount inside the shell at `/mnt/{name}` (if not using FUNC_REF)."
" Can be used multiple times."
),
),
cpu: Optional[int] = typer.Option(
default=None, help="Number of CPUs to allocate to the shell (if not using FUNC_REF)."
),
Expand Down Expand Up @@ -419,13 +430,15 @@ def shell(
)
else:
modal_image = Image.from_registry(image, add_python=add_python) if image else None
volumes = {} if volume is None else {f"/mnt/{vol}": Volume.from_name(vol) for vol in volume}
start_shell = partial(
interactive_shell,
image=modal_image,
cpu=cpu,
memory=memory,
gpu=gpu,
cloud=cloud,
volumes=volumes,
region=region.split(",") if region else [],
)

Expand Down
3 changes: 2 additions & 1 deletion modal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ def __init__(
self._stub: Optional[api_grpc.ModalClientStub] = None

@property
def stub(self) -> Optional[api_grpc.ModalClientStub]:
def stub(self) -> api_grpc.ModalClientStub:
"""mdmd:hidden"""
assert self._stub
return self._stub

@property
Expand Down
10 changes: 10 additions & 0 deletions modal/cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[s
)
)
resp = await resolver.client.stub.ClassCreate(req)
# Even though we already have the function_handle_metadata for this method locally,
# The RPC is going to replace it with function_handle_metadata derived from the server.
# We need to overwrite the definition_id sent back from the server here with the definition_id
# previously stored in function metadata, which may have been sent back from FunctionCreate.
# The problem is that this metadata propagates back and overwrites the metadata on the Function
# object itself. This is really messy. Maybe better to exclusively populate the method metadata
# from the function metadata we already have locally? Really a lot to clean up here...
for method in resp.handle_metadata.methods:
f_metadata = self._method_functions[method.function_name]._get_metadata()
method.function_handle_metadata.definition_id = f_metadata.definition_id
self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)

rep = f"Cls({user_cls.__name__})"
Expand Down
Loading

0 comments on commit 3a44d32

Please sign in to comment.