Skip to content

Commit

Permalink
Merge branch 'main' into cweld/sandbox-snapshot-poc
Browse files Browse the repository at this point in the history
  • Loading branch information
azliu0 committed Jan 14, 2025
2 parents 8f8f71b + 303f67b commit 7bcf942
Show file tree
Hide file tree
Showing 68 changed files with 2,918 additions and 822 deletions.
54 changes: 46 additions & 8 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ jobs:
pull: "--rebase --autostash"
default_author: github_actions

- name: Install the client
run: |
inv protoc
pip install .
- name: Publish client mount
env:
MODAL_ENVIRONMENT: main
MODAL_LOGLEVEL: DEBUG
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: python -m modal_global_objects.mounts.modal_client_package


client-test:
name: Unit tests on ${{ matrix.python-version }} and ${{ matrix.os }} (protobuf=${{ matrix.proto-version }})
timeout-minutes: 30
Expand Down Expand Up @@ -192,20 +206,42 @@ jobs:
- name: Build wheel
run: python setup.py bdist_wheel

- name: Publish client mount
env:
MODAL_ENVIRONMENT: main
MODAL_LOGLEVEL: DEBUG
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: python -m modal_global_objects.mounts.modal_client_package

- name: Upload to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/* --non-interactive

publish-python-standalone:
name: Publish Python standalone mounts
if: github.ref == 'refs/heads/main'
needs: [client-versioning, client-test, publish-client]
runs-on: ubuntu-20.04
timeout-minutes: 5
env:
MODAL_LOGLEVEL: DEBUG
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}

steps:
- uses: actions/checkout@v3
with:
ref: v${{ needs.client-versioning.outputs.client-version}}

- uses: ./.github/actions/setup-cached-python
with:
version: "3.11"

- name: Build protobuf
run: inv protoc

- name: Build client package (installs all dependencies)
run: pip install -e .

- name: Publish mounts
run: python -m modal_global_objects.mounts.python_standalone


publish-base-images:
name: |
Publish base images for ${{ matrix.image-name }} ${{ matrix.image-builder-version }}
Expand All @@ -224,6 +260,8 @@ jobs:

steps:
- uses: actions/checkout@v3
with:
ref: v${{ needs.client-versioning.outputs.client-version}}

- uses: ./.github/actions/setup-cached-python
with:
Expand Down
120 changes: 117 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,120 @@ We appreciate your patience while we speedily work towards a stable release of t

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

### 0.72.8 (2025-01-10)

- Fixes a bug introduced in v0.72.2 when specifying `add_python="3.9"` in `Image.from_registry`.



### 0.72.0 (2025-01-09)

* The default behavior`Image.from_dockerfile()` and `image.dockerfile_commands()` if no parameter is passed to `ignore` will be to automatically detect if there is a valid dockerignore file in the current working directory or next to the dockerfile following the same rules as `dockerignore` does using `docker` commands. Previously no patterns were ignored.



### 0.71.13 (2025-01-09)

* `FilePatternMatcher` has a new constructor `from_file` which allows you to read file matching patterns from a file instead of having to pass them in directly, this can be used for `Image` methods accepting an `ignore` parameter in order to read ignore patterns from files.



### 0.71.11 (2025-01-08)

- Modal Volumes can now be renamed via the CLI (`modal volume rename`) or SDK (`modal.Volume.rename`).



### 0.71.7 (2025-01-08)

- Adds `Image.from_id`, which returns an `Image` object from an existing image id.



### 0.71.1 (2025-01-06)

- Sandboxes now support fsnotify-like file watching:
```python
from modal.file_io import FileWatchEventType

app = modal.App.lookup("file-watch", create_if_missing=True)
sb = modal.Sandbox.create(app=app)
events = sb.watch("/foo")
for event in events:
if event.type == FileWatchEventType.Modify:
print(event.paths)
```



### 0.70.1 (2024-12-27)

- The sandbox filesystem API now accepts write payloads of sizes up to 1 GiB.



### 0.69.0 (2024-12-21)

* `Image.from_dockerfile()` and `image.dockerfile_commands()` now auto-infer which files need to be uploaded based on COPY commands in the source if `context_mount` is omitted. The `ignore=` argument to these methods can be used to selectively omit files using a set of glob patterns.



### 0.68.53 (2024-12-20)

- You can now point `modal launch vscode` at an arbitrary Dockerhub base image:

`modal launch vscode --image=nvidia/cuda:12.4.0-devel-ubuntu22.04`



### 0.68.44 (2024-12-19)

- You can now run GPU workloads on [Nvidia L40S GPUs](https://www.nvidia.com/en-us/data-center/l40s/):

```python
@app.function(gpu="L40S")
def my_gpu_fn():
...
```



### 0.68.43 (2024-12-19)

- Fixed a bug introduced in v0.68.39 that changed the exception type raise when the target object for `.from_name`/`.lookup` methods was not found.



### 0.68.39 (2024-12-18)

- Standardized terminology in `.from_name`/`.lookup`/`.delete` methods to use `name` consistently where `label` and `tag` were used interchangeably before. Code that invokes these methods using `label=` as an explicit keyword argument will issue a deprecation warning and will break in a future release.



### 0.68.29 (2024-12-17)

- The internal `deprecation_error` and `deprecation_warning` utilities have been moved to a private namespace



### 0.68.28 (2024-12-17)

- Sandboxes now support additional filesystem commands `mkdir`, `rm`, and `ls`.
```python
app = modal.App.lookup("sandbox-fs", create_if_missing=True)
sb = modal.Sandbox.create(app=app)
sb.mkdir("/foo")
with sb.open("/foo/bar.txt", "w") as f:
f.write("baz")
print(sb.ls("/foo"))
```



### 0.68.27 (2024-12-17)

- Two previously-introduced deprecations are now enforced and raise an error:
- The `App.spawn_sandbox` method has been removed in favor of `Sandbox.create`
- Two previously-introduced deprecations are now enforced and raise an error:
- The `App.spawn_sandbox` method has been removed in favor of `Sandbox.create`
- `Sandbox.create` now requires an `App` object to be passed


Expand All @@ -24,7 +134,7 @@ We appreciate your patience while we speedily work towards a stable release of t



### 0.68.21 (2024-11-13)
### 0.68.21 (2024-12-13)

Adds an `ignore` parameter to our `Image` `add_local_dir` and `copy_local_dir` methods. It is similar to the `condition` method on `Mount` methods but instead operates on a `Path` object. It takes either a list of string patterns to ignore which follows the `dockerignore` syntax implemented in our `FilePatternMatcher` class, or you can pass in a callable which allows for more flexible selection of files.

Expand Down Expand Up @@ -56,6 +166,10 @@ img.add_local_dir(

which will add the `./local-dir` directory to the image but ignore all files except `.txt` files

### 0.68.15 (2024-12-13)

Adds the `requires_proxy_auth` parameter to `web_endpoint`, `asgi_app`, `wsgi_app`, and `web_server` decorators. Requests to the app will respond with 407 Proxy Authorization Required if a webhook token is not supplied in the HTTP headers. Protects against DoS attacks that will unnecessarily charge users.

### 0.68.11 (2024-12-13)

* `Cls.from_name(...)` now works as a lazy alternative to `Cls.lookup()` that doesn't perform any IO until a method on the class is used for a .remote() call or similar
Expand Down
53 changes: 33 additions & 20 deletions modal/_container_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
instrument_imports(telemetry_socket)

import asyncio
import base64
import concurrent.futures
import inspect
import queue
Expand Down Expand Up @@ -39,7 +38,7 @@
_find_callables_for_obj,
_PartialFunctionFlags,
)
from modal.running_app import RunningApp
from modal.running_app import RunningApp, running_app_from_layout
from modal_proto import api_pb2

from ._runtime.container_io_manager import (
Expand Down Expand Up @@ -117,7 +116,7 @@ class UserCodeEventLoop:

def __enter__(self):
self.loop = asyncio.new_event_loop()
self.tasks = []
self.tasks = set()
return self

def __exit__(self, exc_type, exc_value, traceback):
Expand All @@ -131,7 +130,10 @@ def __exit__(self, exc_type, exc_value, traceback):
self.loop.close()

def create_task(self, coro):
self.tasks.append(self.loop.create_task(coro))
task = self.loop.create_task(coro)
self.tasks.add(task)
task.add_done_callback(self.tasks.discard)
return task

def run(self, coro):
task = asyncio.ensure_future(coro, loop=self.loop)
Expand Down Expand Up @@ -337,14 +339,17 @@ def _cancel_input_signal_handler(signum, stackframe):
signal.signal(signal.SIGUSR1, usr1_handler) # reset signal handler


def get_active_app_fallback(function_def: api_pb2.Function) -> Optional[_App]:
def get_active_app_fallback(function_def: api_pb2.Function) -> _App:
# This branch is reached in the special case that the imported function/class is:
# 1) not serialized, and
# 2) isn't a FunctionHandle - i.e, not decorated at definition time
# Look at all instantiated apps - if there is only one with the indicated name, use that one
app_name: Optional[str] = function_def.app_name or None # coalesce protobuf field to None
matching_apps = _App._all_apps.get(app_name, [])
active_app = None
if len(matching_apps) == 1:
active_app: _App = matching_apps[0]
return active_app

if len(matching_apps) > 1:
if app_name is not None:
warning_sub_message = f"app with the same name ('{app_name}')"
Expand All @@ -354,12 +359,10 @@ def get_active_app_fallback(function_def: api_pb2.Function) -> Optional[_App]:
f"You have more than one {warning_sub_message}. "
"It's recommended to name all your Apps uniquely when using multiple apps"
)
elif len(matching_apps) == 1:
(active_app,) = matching_apps
# there could also technically be zero found apps, but that should probably never be an
# issue since that would mean user won't use is_inside or other function handles anyway

return active_app
# If we don't have an active app, create one on the fly
# The app object is used to carry the app layout etc
return _App()


def call_lifecycle_functions(
Expand Down Expand Up @@ -403,7 +406,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
# This is a bit weird but we need both the blocking and async versions of ContainerIOManager.
# At some point, we should fix that by having built-in support for running "user code"
container_io_manager = ContainerIOManager(container_args, client)
active_app: Optional[_App] = None
active_app: _App
service: Service
function_def = container_args.function_def
is_auto_snapshot: bool = function_def.is_auto_snapshot
Expand Down Expand Up @@ -450,8 +453,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
)

# If the cls/function decorator was applied in local scope, but the app is global, we can look it up
active_app = service.app
if active_app is None:
if service.app is not None:
active_app = service.app
else:
# if the app can't be inferred by the imported function, use name-based fallback
active_app = get_active_app_fallback(function_def)

Expand All @@ -464,13 +468,12 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
batch_wait_ms = function_def.batch_linger_ms or 0

# Get ids and metadata for objects (primarily functions and classes) on the app
container_app: RunningApp = container_io_manager.get_app_objects()
container_app: RunningApp = running_app_from_layout(container_args.app_id, container_args.app_layout)

# Initialize objects on the app.
# This is basically only functions and classes - anything else is deprecated and will be unsupported soon
if active_app is not None:
app: App = synchronizer._translate_out(active_app)
app._init_container(client, container_app)
app: App = synchronizer._translate_out(active_app)
app._init_container(client, container_app)

# Hydrate all function dependencies.
# TODO(erikbern): we an remove this once we
Expand Down Expand Up @@ -531,10 +534,13 @@ def breakpoint_wrapper():
with container_io_manager.handle_user_exception():
finalized_functions = service.get_finalized_functions(function_def, container_io_manager)
# Execute the function.
lifespan_background_tasks = []
try:
for finalized_function in finalized_functions.values():
if finalized_function.lifespan_manager:
event_loop.create_task(finalized_function.lifespan_manager.background_task())
lifespan_background_tasks.append(
event_loop.create_task(finalized_function.lifespan_manager.background_task())
)
with container_io_manager.handle_user_exception():
event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
call_function(
Expand All @@ -559,6 +565,10 @@ def breakpoint_wrapper():
with container_io_manager.handle_user_exception():
event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
finally:
# no need to keep the lifespan asgi call around - we send it no more messages
for lifespan_background_task in lifespan_background_tasks:
lifespan_background_task.cancel() # prevent dangling tasks

# Identify "exit" methods and run them.
# want to make sure this is called even if the lifespan manager fails
if service.user_cls_instance is not None and not is_auto_snapshot:
Expand All @@ -581,7 +591,10 @@ def breakpoint_wrapper():
logger.debug("Container: starting")

container_args = api_pb2.ContainerArguments()
container_args.ParseFromString(base64.b64decode(sys.argv[1]))
container_arguments_path: Optional[str] = os.environ.get("MODAL_CONTAINER_ARGUMENTS_PATH")
if container_arguments_path is None:
raise RuntimeError("No path to the container arguments file provided!")
container_args.ParseFromString(open(container_arguments_path, "rb").read())

# Note that we're creating the client in a synchronous context, but it will be running in a separate thread.
# This is good because if the function is long running then we the client can still send heartbeats
Expand Down
Loading

0 comments on commit 7bcf942

Please sign in to comment.