Skip to content

Forking

fork() creates N independent copies of a running sandbox. Each child inherits the parent’s filesystem state and its running process state at the moment of the fork. The children diverge from there.

This is the AI-agent backtracking primitive: do expensive setup once in a parent sandbox, then fork it to explore N hypotheses in parallel.

Why this is fast

The runner snapshots the parent VM once (~370 ms for a 1 GiB VM via Firecracker’s diff snapshot), then each child:

  1. Hardlinks the parent’s mem.snap into its own runDir. Same inode → Linux page cache shared across all children in the batch. 100 forks of a 1 GiB sandbox cost ~1 GiB of total page cache, not 100 GiB.
  2. Reflinks the parent’s scratch disk via XFS FICLONE. Instant copy-on-write at the block level.
  3. Restores Firecracker from the snapshot in ~16 ms.
  4. Re-attaches to a fresh TAP device with a new guest IP — every fork has its own network identity.

The whole operation for a 5-child batch: ~490 ms total (snapshot + 5 × restore). For larger batches the per-child cost is the only thing that scales.

Use case: AI agent backtracking

Run setup once. Fork to explore hypotheses in parallel. Discard the losers, keep the winner.

from isorun import Sandbox
with Sandbox("python:3.12") as parent:
parent.create()
# 30 seconds of expensive setup
parent.exec("pip install transformers torch datasets")
parent.exec("python download_model.py")
# Fork into 5 children — each one gets the loaded model in
# memory, no need to re-load.
children = parent.fork(count=5)
# Try 5 different hypotheses in parallel
hypotheses = [
"lr=1e-4 batch=32",
"lr=3e-4 batch=32",
"lr=1e-4 batch=64",
"lr=3e-4 batch=16",
"lr=5e-5 batch=128",
]
results = []
for child, hp in zip(children, hypotheses):
out = child.exec(f"python train.py {hp}")
results.append((hp, out.stdout))
# Pick the winner, destroy the rest
winner = max(results, key=lambda r: parse_score(r[1]))
for child in children:
child.destroy()

What gets inherited

Verified by the test suite — every fork sees:

  • Filesystem state. Files written by the parent before the fork are visible in every child. Files written by a child are visible only to that child (CoW at the block level via reflink).
  • Memory state. Pages loaded by the parent — including JIT caches, ML model weights, cached database connections, in-memory parsers — are shared across forks via the same page cache. No re-load cost.
  • Running processes. A sleep 600 started by the parent is visible in every child at the same PID. The kernel’s process table is part of the snapshot.
  • Environment variables and cwd. Whatever the parent set is what each child sees.

What does not get inherited

  • TCP connections. Each fork gets a new TAP device and a new guest IP. Established outbound connections from before the fork are reset. Listen sockets survive.
  • The vsock socket the runner uses to talk to the agent. Each fork gets a fresh one (this is what the ~16 ms per-child cost is paying for).
  • In-flight I/O. Open file descriptors survive but anything buffered in kernel page cache for “the wrong inode” gets consistent — children see the parent’s filesystem state at the exact moment of the fork.

Limits

ConstraintValue
Max forks per batch50
Source must be in staterunning
Cost of fork() operation~490 ms for 5 children, scales linearly past that (~16 ms/child after the snapshot)
Per-fork memory overhead5 MiB PSS (the dirty CoW pages only)

Why this matters

Every other sandbox provider you’ve evaluated does forking by re-running the setup phase: agent_loop_iteration_2 starts from scratch, re-installs the deps, re-loads the model, retries the hypothesis. With isorun’s fork primitive, the second hypothesis starts from where the first one diverged — at no extra cost beyond ~16 ms.

For an evaluation pipeline running 100 hypotheses against a 30-second setup, that’s the difference between 50 minutes and 60 seconds.