VM Memory Snapshots

Memory snapshots freeze the execution state of a sandbox, including memory and the filesystem, to be restored later. They can be used to fork execution, save warm execution state, and more.

The experimental VM runtime supports memory snapshots when running on the V2 Sandbox backend.

This is only supported for a set of enabled customers. Please reach out if you’d like early access to this feature.

Examples 

Pausing an HTTP Server 

Sandboxes can be paused by calling sb._experimental_snapshot(). This will return a handle that can be used to restore/unpause the sandbox.

This example pauses a running HTTP server. Please note that, while listeners will remain functional, live TCP streams will be reset after being restored.

import os

import modal

os.environ["MODAL_SANDBOX_V2"] = "1"

image = modal.Image.debian_slim().apt_install("curl", "procps")
app = modal.App.lookup("sandbox-snapshot", create_if_missing=True)

with modal.enable_output():
    sb = modal.Sandbox.create(
        "python3",
        "-m",
        "http.server",
        "8000",
        experimental_options={"vm_runtime": True},
        app=app,
        image=image,
        _experimental_enable_snapshot=True,
    )

print(f"Performing snapshot of {sb.object_id} ...")
# Pause the sandbox, returning a snapshot of its state.
snapshot = sb._experimental_snapshot()

Create a new Sandbox from the returned SandboxSnapshot with Sandbox._experimental_from_snapshot:

print(f"Restoring from snapshot {snapshot.object_id} ...")
sb2 = modal.Sandbox._experimental_from_snapshot(snapshot)

print("Let's see that the http.server is still running...")
p = sb2.exec("ps", "aux")
print(p.stdout.read())

# Talk to snapshotted Sandbox http.server
p = sb2.exec("curl", "http://localhost:8000/")
reply = p.stdout.read()
print(reply)  # <!DOCTYPE HTML><html lang...

In-Memory Bazel Cache 

This is a more complex example that shows using memory snapshots to keep a warm analysis cache in the Bazel server to speed up builds.

import os
import time
import modal

os.environ["MODAL_SANDBOX_V2"] = "1"

REPO_URL = "https://github.com/buchgr/bazel-remote.git"
REPO_SHA = "ead20798fed6eb1d4bf40efd08e3653868d34feb"
REPO_DIR = "/root/bazel-remote"

BAZEL_VERSION = "9.2.0"
BAZEL_BUILD = f"cd {REPO_DIR} && USE_BAZEL_VERSION={BAZEL_VERSION} bazel build //:bazel-remote"
BUILD_TIMEOUT = 10 * 60

image = (
    modal.Image.debian_slim()
    .apt_install("git", "curl", "ca-certificates", "build-essential", "python3", "unzip", "zip")
    .run_commands(
        "curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.22.0"
        "/bazelisk-linux-amd64 -o /usr/local/bin/bazel",
        "chmod +x /usr/local/bin/bazel",
        f"mkdir {REPO_DIR} && cd {REPO_DIR} && git init -q && git fetch --depth 1 {REPO_URL} {REPO_SHA}"
        " && git checkout -q FETCH_HEAD",
        f"USE_BAZEL_VERSION={BAZEL_VERSION} bazel --version",
    )
)


def timed_build(sb: modal.Sandbox, label: str) -> float:
    t0 = time.monotonic()
    p = sb.exec("bash", "-c", BAZEL_BUILD, timeout=BUILD_TIMEOUT)
    out = p.stdout.read() + p.stderr.read()
    p.wait()
    elapsed = time.monotonic() - t0
    assert p.returncode == 0, f"{label} build failed:\n{out[-4000:]}"
    print(f"{label} build: {elapsed:.1f}s")
    return elapsed


if __name__ == "__main__":
    app = modal.App.lookup("example-memory-snapshot", create_if_missing=True)

    # Snapshotting has to be opted into at creation time.
    sb = modal.Sandbox.create(
        app=app,
        image=image,
        cpu=4.0,
        memory=8 * 1024,
        timeout=20 * 60,
        experimental_options={"vm_runtime": True},
        _experimental_enable_snapshot=True,
    )
    restored = None
    try:
        # The cold build populates the on-disk cache and, more importantly,
        # leaves a Bazel server running with a warm analysis cache in memory.
        cold = timed_build(sb, "cold")

        snapshot = sb._experimental_snapshot()
        print(f"snapshot_id: {snapshot.object_id}")
        sb.terminate()

        # Restore into a fresh Sandbox. The Bazel server comes back as a live
        # process with the same pid and in-memory state so execs come back warm
        t0 = time.monotonic()
        restored = modal.Sandbox._experimental_from_snapshot(snapshot)
        timed_build(restored, "warm")

        restore_and_warm = time.monotonic() - t0
        print(f"restore + warm build: {restore_and_warm:.1f}s")
        print(f"speedup vs cold: {cold / restore_and_warm:.1f}x")
    finally:
        sb.terminate()
        if restored is not None:
            restored.terminate()

Limitations 

  • The same limitations as gVisor memory snapshots remain for now
  • Performance will continue to improve as this feature matures
  • Volumes are not supported when _experimental_enable_snapshot=True