Skip to main content
On Unix, a live cerulion graph run executes your graph as several OS processes — one per process group — running in lockstep. You get fault isolation for free: a node that crashes takes down its own group, not the whole robot. Execution is identical either way: the workers stay in step at every execution level, so the run is byte-identical to running everything in one process. Multi-process buys isolation, not latency — nodes fused into one process still take the fastest path between each other.

What happens by default

The derived partition is the cost-aware optimum when graphs/<name>.costs.yaml exists (written by cerulion graph profile), and otherwise a process-per-node baseline — maximum fault isolation. Declining is not aborting: the run proceeds with exactly the deployment a written file would have produced. Ctrl+C is how you abort.

Choose the split deliberately

Two commands turn the default into a decision you own.

Measure the graph

Runs the graph locally and writes graphs/perception.costs.yaml — per-node median tick cost, per-edge fire rates, and a frozen compute budget. A node that never sampled enough fires is reported as such rather than guessed at. Each node’s fire target is derived from a warm-up observation of its own rate; --fires N overrides that with one uniform target for every node, which is what you want when a node’s first fire lands after the warm-up — a period longer than three seconds, say.A profile run is a measurement, not a deployment, so it stays local-only by design.

Derive the partition

Prints the derived groups and the YAML diff without writing. Drop --dry-run to write (--yes skips the confirmation), and pass --budget-ns N to override the artifact’s frozen per-group budget.

See what the runtime sees

Prints the execution levels derived from the wiring, which nodes sit in each, and — when the graph declares process_groups: — which group covers which levels. It exits non-zero on an invalid partition, which makes it a useful CI check next to graph validate.

Writing process_groups: by hand

Each group is a list of node ids, and the groups must be a partition of the graph’s nodes — every node in exactly one group. The derived partition always co-locates the whole flow of a block topic, because crossing a process boundary costs a real hop. A hand-written process_groups: block may split one, and the supervisor accepts the split when the edge is creditable — exactly one in-graph producer and no non-block consumers. It then mints that edge a cross-process credit word, a shared-memory page both ranks map, so the producer’s defer crosses the boundary without losing a frame.
Two shapes are still refused, before any worker spawns, each naming the bar it hit:
  • Two or more in-graph producers on a multi_publisher_topics topic. One credit word counts one producer’s outstanding frames, so two writers would each spend the other’s credit. Co-locate the flow, or reduce the topic to a single in-graph producer.
  • A mixed topic — block consumers alongside non-block siblings. Those block consumers are already degraded to drop_oldest, so there is no lossless defer left to credit. Move the named siblings into the producer’s group, or make them block too.
cerulion graph levels tells you before you run. Its partition verdict names every split block edge it found creditable:
Note the caveat the line carries: graph levels judges on source metadata, while graph run re-checks the built cdylibs, so a stale .so can still turn an accepted split into a refusal. Re-deriving the partition over a hand-written creditable split replaces it, and says so loudly after the write — the .bak beside the graph file is the way back.

When a worker dies

The supervisor’s default is to keep the robot up: Under continue, a cross-process block edge whose consumer’s rank died is called out specifically: the warning names the topic, the consumer input, the dead rank, and the producers now deferred behind a credit nobody will return — loudly once per regime, with a periodic re-announcement while it lasts, and a retraction if one of those producers turns out to have died too. If you are chasing the last microseconds at level boundaries, CERULION_BARRIER_SPIN_US tunes how long a worker waits actively before it sleeps. CERULION_CREDIT_WAKE=0 turns off the credit wake plane that lets a credited producer park until the consumer drains — correct either way, you only lose wake latency. See Environment variables.

Recording a multi-process run

Every multi-process run keeps the record of what fired when, whether or not you asked it to record — that is what makes a run you never planned to record still re-executable afterwards. cerulion graph run --no-rings declines that bookkeeping on a memory-tight robot; because nothing captured without it could be re-executed, such a run takes no captures at all, and --no-rings is refused together with --record.

Next steps

Record and replay a run

Turn a run into an MCAP bag and re-execute it.

Backpressure and deadlines

The block policy and the constraint it puts on partitioning.