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