Pipeline
API reference for streaming pipeline internals — items, rank boundaries, and lifecycle
API reference for the streaming pipeline internals. For the stage builder methods (ctx.source, ctx.pipe, ctx.tap, ctx.pace, ctx.unbatch, ctx.window, ctx.stream in sink mode) see the Context reference. For the concept-level walkthrough, see Streaming Pipelines.
Streaming pipeline
Stages chain with the | operator to form a data-flow graph that runs across GPU ranks. A Stage is a stream transducer with 0..N inputs; a list of streams as the head is fan-in ([a, b] | stage). See the unified Stage DSL.
| Name | Description |
|---|---|
await pipeline.run() | Start the pipeline and hold it alive until the session ends |
pipeline.drive(frames=N) | Drive N representative items through synchronously — for ctx.self_test |
pipeline.update(**kwargs) | Push live config updates to the source stage |
pipeline.stop() | Drain in-flight items and stop all stages |
Reactive control is not on the pipeline — it is on the doc: cheap config is an inline doc.desired.* read, structural change is doc.bind(construct, [deps]) (useMemo). The old pipeline.bind(doc, mapping) field-mapping still works but is discouraged/legacy.
await pipeline.run()
await pipeline.run(*, poll_interval: float = 0.1, **config)The canonical way to drive a composed pipeline. It starts the pipeline (the stage threads do the work), holds it alive while the session transport is attached, and stops it cleanly on disconnect or when the awaiting task is cancelled. This replaces any hand-rolled while ctx.connected: ... pipeline.update() poll.
run() does not reconcile control itself — reactive control lives on the doc (inline doc.desired.* reads + doc.bind). poll_interval only bounds how quickly a disconnect is observed; it is a sleep granularity, not a reconcile poll. Any **config is forwarded to start as the initial desired-state.
doc = ctx.doc("control")
pipeline = (
ctx.source(make_frames(models, doc))
| doc.bind(lambda d: ctx.pipe(make_vae(models, d)), [doc.desired.vae.model])
| ctx.pace(fps=24) | ctx.stream("video", codec="h264")
)
with ctx.self_test():
_drive_one_and_assert_shape(pipeline)
await pipeline.run() # keep-alive until disconnectpipeline.drive()
pipeline.drive(frames: int = 1) -> listDrives frames representative items through the composed pipeline synchronously and returns the produced outputs. This is the driver inside ctx.self_test: the self-test block calls pipeline.drive(frames=N) and asserts the result's shape/dtype — the app drives and asserts its own representative pass. The same call runs under the CPU harness (mocked primitives), where reaching the end of the self-test block without raising means the test passed.
with ctx.self_test():
out = pipeline.drive(frames=1)
_assert_frames_shaped(out) # shape/dtype only — no pixel/value readPipelineItem
from urun.core.streaming.gpu_pipeline import PipelineItem, PipelineMetadataWraps a payload with control metadata that survives stage hops and rank boundaries.
@dataclass(frozen=True)
class PipelineItem:
payload: Any
metadata: PipelineMetadata
@dataclass(frozen=True)
class PipelineMetadata:
prompt_generation: int = 0
flush_generation: int = 0
cutover: bool = False
drop_generation: int = 0
pts: int = 0
idle: bool = False
capture_ts: float | None = None
input_seq: int = 0
input_at_ms: int = 0
timeline: tuple[tuple[str, float], ...] = ()| Field | Description |
|---|---|
prompt_generation | Increments when the user changes the prompt -- downstream stages can detect prompt boundaries |
flush_generation | Increments on pipeline reset -- stages discard items from stale generations |
cutover | Marks the transition frame between two prompt generations |
drop_generation | Optional control/session generation, separate from the content domain (0 = same as prompt_generation) -- for apps whose clip counter advances faster than their control counter |
pts | Presentation timestamp in the producing media's sample clock, so ctx.mux can realign video against the assistant audio that drove it (lip-sync). 0 = unstamped |
idle | True on a frame a liveness/idle stage synthesized while not being driven -- ctx.mux pads the audio side with silence instead of waiting for a partner that never arrives |
capture_ts | Monotonic capture timestamp stamped at the source emit point -- the anchor for end-to-end frame-age observability (urun_pipeline_frame_age_ms at the egress sink) |
input_seq / input_at_ms | Mouse-to-photon correlation: the frontend's input-presence publish counter and its client capture clock for the newest input sample that conditioned this item. 0 = untagged (non-input-driven pipelines) |
timeline | Per-hop timing timeline: (hop, monotonic_ts) pairs appended by every stage (<stage>.in at dequeue, <stage>.out at emit) plus the input head's input.recv. Bounded; local-rank only (dropped at a rank boundary, like idle) |
Mouse-to-photon timing
Input-driven pipelines get an always-on, per-hop timing surface with zero app wiring: ctx.input lifts each sample with its correlation fields (input_seq / input_at_ms) and an input.recv hop; ctx.window carries the newest tick's correlation onto each emitted window; and every stage stamps <stage>.in / <stage>.out into timeline. The egress sink folds the timeline into one structured [timing:<stream>] log line per conditioned window (per input_seq, never per frame), exposing per-hop queue-wait vs compute from the user's input to the frame leaving the runtime. Apps can also observe it programmatically via the sink's on_timing hook. Pipelines steered by doc/prompt (no continuous input) still report per cutover, so steer→egress timing comes free there too.
Factory methods:
| Method | Description |
|---|---|
PipelineItem.generation(payload, gen) | Wrap payload with a prompt generation number |
PipelineItem.cutover(payload, gen) | Wrap payload as a cutover point (flush + new generation) |
Most stages handle PipelineItem transparently — they unwrap the payload, process it, and re-wrap the result with the same metadata. You only need PipelineItem directly when implementing custom cutover logic in a source stage.
Rank boundaries
When consecutive stages run on different ranks, urun automatically inserts shared-memory IPC transport:
pipeline = (
ctx.source(denoise, device="cuda", rank=0) # rank 0
| ctx.pipe(vae_decode, device="cuda", rank=1) # rank 1 — IPC inserted here
| ctx.pace(fps=24, rank=1)
| video
)The IPC transport uses CUDA IPC handles for zero-copy GPU tensor transfer between ranks on the same node. CPU tensors use shared-memory ring buffers.
Pipeline lifecycle
pipeline.update(prompt="new prompt")
pipeline.stop()| Method | Description |
|---|---|
pipeline.update(**kwargs) | Push live config updates to the source stage |
pipeline.stop() | Graceful shutdown -- drains in-flight items, then stops all stages |