Runtime
View sourceWhat you’ll learn: the path from an authored graph to a running one — validate() compiles a Pipeline to an IR, step() advances one debugger-friendly graph tick, and run(backend=...) schedules the same IR on multiprocessing or Dora. Given the same ordered timestamped input history, deterministic Flows produce the same discrete-event output history.
A Pipeline is the executable graph. You normally wire Flows with the fluent source.then(target, sync=...) form; the examples on this page use the explicit connect(source, target, sync=...) equivalent because runtime validation and IR examples benefit from naming both endpoints. After wiring, the same object exposes four surfaces:
validate() compiles the graph to an IR
Section titled “validate() compiles the graph to an IR”validate() checks the wiring — ports exist, source and destination types match, clocks and adapters are well-formed — and returns an IR: the validated logic graph, with every node’s clock and every edge’s adapter made explicit. The IR is the artifact backends execute, visualizers render, and Hub modules load.
IR.from_json(...) re-runs structural validation on load, so IR read from disk or a Hub module is not trusted blindly — duplicate node ids, dangling edge or adjacency references, and unknown ports fail with a named node or edge before a backend tries to execute the graph.
Local step() is a debugger-friendly graph tick
Section titled “Local step() is a debugger-friendly graph tick”pipe.step(dt=...) advances the graph in one process: it samples each edge, calls each Flow.step(), and publishes outputs. This is intentionally a local debug tick, not a full backend scheduler: Rate(...) flows execute once per step() call, while Trigger(...) flows still wait for new inputs. Breakpoints inside step() fire in your own process, and the trace is reproducible. Use an async backend when you want wall-clock Rate(...) scheduling.
Two independent runs produce the identical trace because they use the same fixed input history, deterministic Flow.step() bodies, and the same graph state. This is Retriever’s functional determinism boundary: fix the ordered timestamped input history and the discrete-event output history is fixed too.
The contract, precisely. Fix a graph with deterministic Flow.step() bodies and sync policies, fixed initial states, strictly causal cycles, and the same ordered timestamped external input history; replay stochastic model calls from logged seeds or logged outputs. Then the discrete-event output history is uniquely determined. Latest(now=t) selects the event with the greatest timestamp satisfying timestamp <= t; without now, it selects the greatest timestamp in the buffer. Buffers retain timestamp order even when a transport drains fan-in queues in another arrival order. Events with equal timestamps currently preserve insertion order, so scheduler-independent replay requires that tie order to be part of the recorded input history (or that producers attach a deterministic sequence key).
Latest() uses event time, not append order
Section titled “Latest() uses event time, not append order”Suppose a transport appends these records out of order:
Retriever orders the buffer by timestamp before sampling:
| Sample | Result | Reason |
|---|---|---|
Latest() |
"newer" |
0.03 is the greatest timestamp. |
Latest(now=0.02) |
"older" |
0.01 is the greatest timestamp at or before 0.02. |
Latest(now=0.005) |
raises IndexError |
No event is available at that logical time. |
Boundaries worth knowing:
step(now=...)is exact relative to the supplied ordered input history.step(dt=...)keeps logical deltas stable but anchors the first tick at the stepper’s starting time.- Determinism concerns the discrete event path, not bitwise GPU numerics.
- A live async run (
multiprocessing,dora) does not replay its wall-clock scheduling. Its scheduling determines the captured input history; replay reproduces that history, not the original process schedule. - Equal timestamps require a fixed tie order. The runtime resolves ties as last-inserted on every sampling path; use monotonic timestamps or sequence keys when multiple producers can emit at exactly the same time.
- An event stamped after the sampling tick is not yet visible: the field reads as absent that tick and the event is delivered once
nowpasses its timestamp. On distributed transports this also absorbs producer/consumer clock skew instead of erroring.
The idea has deep roots: Kahn process networks showed deterministic processes over ordered channels compose deterministically (Kahn 1974), and functional reactive programming made time-indexed streams a first-class semantics (Elliott & Hudak 1997, Wan & Hudak 2000). The Retriever paper states this property as the functional determinism theorem and proves it as trace determinism.
Same IR, different backend
Section titled “Same IR, different backend”The graph you debug in-process is the graph you deploy. Only the backend changes:
| Backend | What it does | Use for |
|---|---|---|
in-process |
One process; step() / run() in your interpreter. |
Debugging, tests, deterministic replay. |
multiprocessing |
One OS process per Flow, local channels. | Local async execution and parallelism. |
dora |
Compiles to a Dora dataflow across processes/machines. | Deployment; distributed, high-rate control. |
Record and replay turn runs into evidence
Section titled “Record and replay turn runs into evidence”Recording preserves the consumed, ordered input history. Replaying that history through deterministic Flows reconstructs the same discrete-event outputs; stochastic model calls must come from logged seeds or logged outputs.
Record with pipe.record(...) (in-process) to MCAP or RRD; replay drives the same graph from the recording instead of live inputs. See Debug and Visualize for the full record/replay walkthrough.
