Skip to content

Ready states

00 / The cookbook

A ready state is the point a rollout starts from. You bake an image once, warm it, and checkpoint it into an artifact; every fork then wakes at that exact state instead of paying the setup cost per branch. This page explains the two kinds of ready state and the Ready helpers that declare them — the rest of the cookbook is worked recipes.

Warm

Imports resolved, caches hot, model weights mapped, the interpreter primed — the setup work is done, but nothing is left running. A fork wakes with the heavy imports already paid, so the rollout skips the import tax and gets straight to work.

Live service

A long-running process — a browser, a Jupyter kernel, a database, a model server — is started at bake time and left running, so the snapshot captures it alive. Every fork wakes with the service already up; the rollout attaches to it instead of launching it.

Warm is the common case and often all you need: pay import torch (or the npm install, or the dataset load) once, at bake time, instead of on every branch. Live service goes one step further — it keeps a process running across the snapshot, so the fork inherits not just a warm cache but a live connection point.

You name what to run and how "ready" is detected; Collimate does the rest — launching the process, waiting until it is actually serving, and snapshotting at the right moment. There is no launch-and-wait shell to hand-write.

from collimate_rl import Ready, Template, connect
client = connect() # a col_pro_* key via COLLIMATE_API_KEY
Template.bake(
client,
name="torch-warm",
image="ghcr.io/you/trainer@sha256:…", # pin by digest
ready=Ready.warm("python -c 'import torch, numpy'"), # ready when it exits 0
width=64,
)

Live service — start it, wait until it serves

Section titled “Live service — start it, wait until it serves”

Pick the readiness signal that matches the service:

# HTTP service (model/tool server, browser over CDP): poll a health URL.
ready = Ready.http_service("vllm serve --port 8000", port=8000, path="/health")
# Anything with a probe command (Postgres, Redis): wait for it to exit 0.
ready = Ready.service("pg_ctl -D $PGDATA start", await_cmd="pg_isready -q")
# Anything that just needs its port to accept a connection.
ready = Ready.service("jupyter kernel --KernelApp.ip=127.0.0.1", await_port=8888)
handle = Template.bake(
client,
name="my-live-service",
image="ghcr.io/you/my-live-service@sha256:…",
ready=ready,
width=64,
)
handle.wait(timeout=600) # Pending → Converting → Distributing → Live

Each live-service helper compiles to a warm command that launches the process detached (so it outlives the bake step and lands in the snapshot), then blocks until the real serving signal passes — an HTTP 2xx, a TCP port accepting a connection, or your probe exiting 0 — and only then exits 0, which is what tells Collimate to snapshot. That launch → wait-until-serving → snapshot sequence is the whole contract; the helper writes it so you don't.

A rollout against a live-service ready state attaches to the already-running process; it does not launch one. The pattern is the same across services — connect to the live endpoint, do isolated per-rollout work, disconnect without tearing the shared service down:

from collimate_rl import connect
client = connect()
ROLLOUT = """
# runs inside the sandbox — attaches to the service the snapshot is already running
import urllib.request
print(urllib.request.urlopen("http://127.0.0.1:PORT/…").read())
"""
sb = client.create_sandbox("my-live-service")
result = client.exec(sb["id"], commands=[["python3", "-c", ROLLOUT]], timeout_seconds=60)
print(result["stdout"])
client.delete_sandbox(sb["id"])

Fan a single warmed parent out across many rollouts with fork — every child inherits the same live service:

# count is the per-call fan-out, capped by tier (Pro 32, Ent 64); call again
# (or use Session.fork_group) for a wider group.
children = client.fork(sb["id"], count=32)["children"]
for child in children:
client.exec(child["id"], commands=[["python3", "-c", ROLLOUT]], timeout_seconds=60)

Give each rollout its own isolated scope against the shared service (a fresh browser context, a scratch database schema, a per-rollout kernel session) and tear down that scope on exit — never the service itself, which is the shared warm state.

Each cookbook page declares its service with one Ready helper — a different service and readiness signal swapped in:

RecipeServiceReady helper
Live browserHeadless Chromium over CDPReady.http_service(…, path="/json/version")
Live JupyterA running Jupyter kernelReady.service(…, await_port=…)
Live PostgresAn initialized Postgres serverReady.service(…, await_cmd="pg_isready")
Live model serverA small inference/tool serverReady.http_service(…, path="/health")