docs
Python SDKReference

Context

API reference for urun.Context dependency injection

API reference for urun.Context — distributed execution context via dependency injection.

Public API at a Glance

NameDescription
urun.ContextDistributed execution context (DI parameter)
ctx.session_idID of the current session
ctx.rankGlobal rank of current process
ctx.local_rankLocal rank on current node
ctx.world_sizeTotal number of processes
ctx.node_rankRank of current node
ctx.num_nodesTotal number of nodes
ctx.is_main_processWhether this is rank 0
ctx.deviceTorch device for current rank
ctx.connectedWhether the session transport is up
ctx.session_live()Cooperative per-frame "keep running?" gate (rank-synchronized)
ctx.stream(name, ...)Create session-scoped stream or pipeline sink
ctx.channel(name)Session-scoped bidirectional channel (alias of ctx.stream(name))
ctx.doc(key)Read the current session's replicated CRDT document
ctx.docsProcess-local session document registry
ctx.requests(...)Doc-based request/response responder (the standard RPC surface)
ctx.rpc(name, ...)Async-iterator request/response (legacy adapter — prefer requests/docs)
ctx.serve(spec)Serve a model-catalog model inside a function body
ctx.input(field=, hz=)Continuous ephemeral input (keyboard/mouse) sampled from awareness
ctx.source(fn, ...)Create pipeline source stage
ctx.pipe(fn, ...)Create pipeline processing stage
ctx.tap(fn, ...)Create pipeline side-effect stage
ctx.pace(...)Create frame-rate pacing stage
ctx.unbatch(...)Create unbatching stage
ctx.window(...)Clip the inbound stream into windows
ctx.run_all(*pipelines)Run several pipelines concurrently for the whole session
ctx.completing_work(max_seconds=)Guard a bounded work item so a lost browser leg cannot discard it
ctx.self_test()The one warmup ≡ precompile ≡ validate gate (context manager)
ctx.recordingsDVR recordings client (retention / pin)
ctx.artifactsSession artifact API (bulk bytes, e.g. a .ply)
ctx.session_statePer-session state handle
doc.desired.<prop>Live reactive read of a control-doc property (cheap config)
doc.bind(construct, deps)useMemo for stages — reconstruct + swap a stage on dep change

urun.Context

class urun.Context

Distributed execution context, injected via type annotation. Add ctx: urun.Context as a parameter to any @app.function and urun injects the context automatically at runtime.

import urun

@app.function(gpus="h100:8")
def world(ctx: urun.Context):
    print(f"Rank {ctx.rank} of {ctx.world_size}")
    model = model.to(ctx.device)  # each rank holds its shard of the pipeline

Use urun.Context as the primary interface for distributed execution info and streaming pipeline construction inside GPU functions.


Properties

PropertyTypeDescription
ctx.session_idstrID of the current session
ctx.rankintGlobal rank of current process (0 to world_size - 1)
ctx.local_rankintLocal rank on current node (0 to GPUs-per-node - 1)
ctx.world_sizeintTotal number of processes across all nodes
ctx.node_rankintRank of current node (0 to num_nodes - 1)
ctx.num_nodesintTotal number of nodes
ctx.is_main_processboolTrue if this is global rank 0
ctx.devicetorch.deviceTorch device for current rank (e.g., cuda:0, cuda:1)
ctx.connectedboolTrue while the session transport is up

All properties are read-only. Values are determined by the runtime environment and GPU allocation.


ctx.session_live()

while ctx.session_live():
    step()

Cooperative, lockstep "should this session keep running?" gate — call once per frame in a session loop. On a multi-rank group it broadcasts the rank-0-owned liveness flag over the SPMD communicator, so every rank ends the session on the same frame (no desync). On world_size == 1 it is a pure local read. Returns False once the session has been asked to stop (lease lapse, authoritative close, consumer abandonment).


ctx.completing_work()

async with ctx.completing_work(max_seconds=30):
    image = await pipeline.generate(prompt)
    await ctx.stream("out").send(image)   # emits → DVR records → S3

Mark a bounded discrete work item (one image render, one generation) so a lost browser leg cannot discard its result mid-flight. While the guard is open, a leg-lost verdict (browser disconnected) is deferred so the item finishes and streams — the DVR records it, and a later reconnect replays the result with zero GPU work. Platform-terminal verdicts (lease lapse, authoritative close) are never deferred, and max_seconds bounds the guard so a hung item ages out. Usable as with or async with.


ctx.run_all()

await ctx.run_all([p1, p2, p3])   # canonical; varargs also accepted

Run several pipelines concurrently and keep them all alive until session end — the multi-pipeline counterpart of await pipeline.run(). Returns when every pipeline's run() returns (session disconnected); propagates the first failure. Never hand-roll asyncio.gather for this.


ctx.channel()

ctx.channel(name: str) -> EventStream

Create or retrieve a session-scoped bidirectional channel — the control/data-oriented alias of ctx.stream(name) (same namespacing, same transport).


ctx.requests()

rpc = ctx.requests()          # bound to ctx.doc(); requests_key="requests", responses_key="responses"

@rpc.on_request("echo")
def echo(req):
    return {"echoed": req.payload}

rpc.start()                   # observes until the session ends (auto-stopped at teardown)

The standard doc-based request/response surface: a client writes a request register under the requests key of the session doc; the responder dispatches by kind and writes the correlated reply under responses of the same doc. No new transport — it rides the ctx.doc() CRDT primitive. Kwargs: key= (which session doc), requests_key=, responses_key=.


ctx.rpc()

async for req in ctx.rpc("generate", kind="image", codec="jpeg"):
    await req.stream.emit(image)     # ndarray/PIL → one binary JPEG turn
    await req.respond(body)          # or req.error("...") on failure

The async-iterator request/response primitive the TS client's session.request / requestStream / complete speaks. Replies ride the request's own response stream — responses never touch the doc. Legacy adapter: apps should never keystone on ctx.rpc; prefer ctx.requests() and doc desired-state, with outputs on streams.


ctx.serve()

@app.function(gpus="l4:1", lease=14400)
async def serve(ctx: urun.Context):
    await ctx.serve("qwen-coder:fp8@l4:1")

Serve a model-catalog model — the whole serving loop inside a normal @app.function body: resolves the spec against the catalog at boot, loads the engine, subscribes to the shared request registry on ctx.doc("llm"), and streams token deltas + the final OpenAI-compatible body to each requester's own consumer-addressed stream (request A's tokens never reach consumer B). spec is a model string "<id>[:<variant>][@<gpu>]" or a resolved ServeSpec.


ctx.input()

driven = ctx.input(field="input", hz=30.0) | ctx.window(...) | ctx.pipe(step) | ...
# or imperatively:
async for sample in ctx.input():
    ...

Continuous ephemeral input (held keys, pointer counters) published by the browser into the control doc's awareness (via @urun-sh/react useInputPresence), sampled at hz as per-sample deltas. Usable imperatively or as a pipeline head; starves when input is quiet. Split rule: continuous ephemeral input rides awareness; discrete semantic events ride the doc or named data streams.


ctx.recordings, ctx.artifacts, ctx.session_state

PropertyDescription
ctx.recordingsThe DVR recordings client. Sessions are recorded by default; use it to manage retention and pin recordings. See Sessions.
ctx.artifactsThe session artifact API — bulk byte outputs (a .ply, a file) that are artifacts, not media. The output lane for session_kind="doc" apps.
ctx.session_statePer-session state handle for values scoped to the current session.

ctx.stream()

ctx.stream(
    name: str = "default",
    *,
    format: str | None = None,
    codec: str | None = None,
    audio_codec: str | None = None,
    kind: Literal["audio", "video", "data"] | None = None,
    fps: int | None = None,
    gop: int | None = None,
    bitrate: int | None = None,
    host: str = "",
    port: int = 0,
    rank: int | None = None,
    rank_policy: str | None = None,
    broadcast: bool = False,
    record: bool = True,
    retention: str | None = None,
    delivery: Literal["paced", "latest"] = "paced",
    direction: Literal["auto", "in", "out"] = "auto",
) -> EventStream | SinkStage

Create a session-scoped stream. Has two modes depending on parameters:

Data stream mode (no codec/bitrate): Returns an EventStream for bidirectional JSON/event data. Session streams are automatically namespaced to the current execution session.

Pipeline sink mode (codec or bitrate set): Returns a SinkStage that encodes and streams media (e.g., H.264 video via RTP). Chainable with the | operator as the final stage in a pipeline.

ParameterTypeDefaultConstraintsDescription
namestr"default"--Stream name (namespaced to session)
formatstr | NoneNone--Data format hint
codecstr | NoneNone"h264", "vp9", "opus", "jpeg", etc.Media codec (activates sink mode)
audio_codecstr | NoneNonee.g. "opus"Audio codec for a muxed A/V sink (pair with codec= on the video side; see ctx.mux)
kindstr | NoneNone"audio", "video", "data"Stream kind. Inferred from codec when omitted (h264/vp8/vp9/av1 → video, opus/aac/pcm → audio); pass explicitly for a codec-less inbound consume (e.g. a named data stream)
fpsint | NoneNonePositive integerTarget frame rate
gopint | NoneNonePositive integerGroup-of-pictures size (keyframe interval)
bitrateint | NoneNonePositive integerTarget bitrate in bps (activates sink mode)
hoststr""--RTP destination host
portint0--RTP destination port
rankint | NoneNone--Pin this stream to a specific rank
rank_policystr | NoneNone--Rank assignment policy
broadcastboolFalsedata streams onlyTrue opts this stream name into legacy global broadcast (every subscriber receives it). Default = addressed/consumer-opt-in delivery (emit(data, to=consumer_id)). Ignored on the media-sink path
recordboolTrue--DVR is default-on — every stream is recorded unless you pass record=False to opt this stream out
retentionstr | NoneNone"24h", "7d", "1h30m" (max 30d) or "pin"The recording's declared expiry. None = the platform 24h default; "pin" keeps it permanently. Invalid with record=False
deliverystr"paced""paced", "latest"Delivery policy: paced playout or latest-wins
directionstr"auto""auto", "in", "out"Media polarity: "in" = decoded browser→runtime source (e.g. mic Opus→PCM), "out" = runtime→browser sink, "auto" = codec-selects-sink behavior

Returns: EventStream (data mode) or pipeline sink stage (codec/bitrate mode).

# CRDT document — bidirectional state (see ctx.doc())
control = ctx.doc("control")
listener = control.listen(keys=["prompt"])
while ctx.connected:
    change = listener.wait(timeout=0.1)
    if change:
        handle(change.snapshot.get("prompt"))

# Pipeline sink — video output with encoding
video = ctx.stream("video", codec="h264", bitrate=8_000_000, fps=24, rank=1)

pipeline = (
    ctx.source(denoise, device="cuda", rank=0)
    | ctx.pipe(vae_decode, device="cuda", rank=1)
    | ctx.pace(fps=24, rank=1)
    | video
)

ctx.doc()

ctx.doc(key: str)

Return one keyed replicated document for the current session. Documents are CRDT-backed (Yjs) and automatically synced from the browser via transport.sendDoc(key, patch). The key is app-defined — urun does not reserve any keys.

MethodDescription
snapshot()Return a deep copy of the document
get(path, default=None)Read a dotted path from the latest snapshot
listen(keys=None)Queue changes; consume with poll(), wait(), or wait_async()
on_change(callback, keys=None)Register a filtered callback
@app.function(gpus="b200:4")
def render(ctx: urun.Context):
    control = ctx.doc("control")
    prompt = control.get("prompt.text", "a cinematic landscape")
    keyboard = control.get("keyboard.keys", {})
    camera = control.get("camera.pose", {})

For reactive loops:

control = ctx.doc("control")
listener = control.listen(keys=["keyboard", "camera"])

while ctx.connected:
    change = listener.wait(timeout=0.016)
    if change is not None:
        latest = change.snapshot
        update_camera(latest["keyboard"]["keys"], latest["camera"]["pose"])

ctx.docs

ctx.docs

Process-local session document registry. Use for multi-session worker code that observes all active sessions. Normal handlers should use ctx.doc(key).

MethodDescription
for_session(session_id, key)Return one keyed session document
snapshot(session_id, key)Snapshot a specific keyed document
listen(session_id=None, key=None)Queue changes across the registry

Streaming pipeline

The pipeline builder methods on ctx create a composable, rank-aware streaming pipeline. Stages chain with the | operator and automatically insert IPC transport at rank boundaries.

ctx.source()

ctx.source(
    fn: Callable,
    name: str | None = None,
    device: str = "cuda",
    priority: int = 0,
    rank: int | tuple[int, ...] | None = None,
    rank_policy: str | None = None,
) -> SourceStage

Create the head of a pipeline. fn is a generator that yields items. It receives stop_event and config as keyword arguments — config is updated live when pipeline.update(prompt=...) is called.

ParameterTypeDefaultDescription
fnCallablerequiredGenerator function yielding pipeline items
namestr | NoneNoneStage name (for diagnostics)
devicestr"cuda"Device string -- "cuda" resolves to cuda:{local_rank}
priorityint0Thread priority hint
rankint | tuple[int, ...] | NoneNonePin to specific rank(s); None = current rank
rank_policystr | NoneNoneRank assignment policy
def denoise(*, stop_event, config):
    prompt = config.get("prompt", "default scene")
    while not stop_event.is_set():
        latents = model.generate(prompt)
        yield latents
        prompt = config.get("prompt", prompt)

pipeline = ctx.source(denoise, name="denoise", device="cuda", rank=0)
pipeline.update(prompt="A sunset over the ocean")

ctx.pipe()

ctx.pipe(
    fn: Callable,
    name: str | None = None,
    device: str = "cpu",
    priority: int = 0,
    rank: int | None = None,
    rank_policy: str | None = None,
    max_depth: int = 8,
    backpressure_at: int = 6,
) -> Stage

Create a processing stage. Calls fn(item) on each pipeline item and forwards the result downstream.

ParameterTypeDefaultDescription
fnCallablerequiredProcessing function fn(item) -> result
namestr | NoneNoneStage name
devicestr"cpu""cuda" for GPU stages, "cpu" for CPU
priorityint0Thread priority hint
rankint | NoneNonePin to rank; None = current rank
max_depthint8Queue depth before backpressure activates
backpressure_atint6Queue threshold to slow upstream

When a stage runs on a different rank than its predecessor, urun automatically inserts shared-memory IPC transport with CUDA IPC handles for zero-copy GPU tensor transfer.

pipeline = (
    ctx.source(denoise, device="cuda", rank=0)
    | ctx.pipe(vae_decode, name="vae", device="cuda", rank=1)
    | ctx.pipe(encode_h264, name="encode", device="cuda", rank=1, priority=-1)
)

ctx.tap()

ctx.tap(
    fn: Callable,
    name: str | None = None,
    device: str = "cpu",
    priority: int = 0,
    rank: int | None = None,
    rank_policy: str | None = None,
) -> TapStage

Create a side-effect stage. Calls fn(item) but does not forward a return value downstream — the original item passes through unchanged. Use for logging, metrics, saving snapshots, or any observation that should not alter the pipeline data.

def log_frame(item):
    print(f"Frame generation={item.metadata.prompt_generation}")

pipeline = (
    ctx.source(generate, device="cuda")
    | ctx.tap(log_frame, name="metrics")
    | ctx.pipe(encode, device="cuda")
    | video
)

ctx.pace()

ctx.pace(
    fps: float = 60.0,
    name: str = "pace",
    rank: int | None = None,
    rank_policy: str | None = None,
    max_depth: int = 256,
    backpressure_at: int = 192,
    min_buffer_frames: int = 0,
    target_buffer_frames: int | None = None,
    max_buffer_frames: int | None = None,
) -> PacedStage

Emit items at a fixed frame rate with adaptive buffering. Smooths bursty upstream output into steady playback.

ParameterTypeDefaultDescription
fpsfloat60.0Target output frame rate
min_buffer_framesint0Minimum buffer depth before emitting
target_buffer_framesint | NoneNoneDesired steady-state buffer
max_buffer_framesint | NoneNoneHard ceiling -- drops oldest frames above this

The pacer adapts dynamically: it grows the buffer when frame underruns are detected and shrinks it when the upstream is keeping up. This prevents both stalls (too few buffered frames) and latency creep (too many).

pipeline = (
    ctx.source(denoise, device="cuda", rank=0)
    | ctx.pipe(vae_decode, device="cuda", rank=1)
    | ctx.unbatch(rank=1)
    | ctx.pace(fps=24, rank=1, min_buffer_frames=4, target_buffer_frames=8)
    | video
)

ctx.unbatch()

ctx.unbatch(
    name: str = "unbatch",
    rank: int | None = None,
    rank_policy: str | None = None,
) -> UnbatchStage

Receive a list/batch from upstream, emit each element individually downstream. Preserves pipeline metadata (prompt generation, flush markers) across the unbatch boundary.

Use when an upstream stage produces batches (e.g., a VAE decoding a window of latent frames into multiple video frames).


ctx.window()

ctx.window(
    window: int,
    stride: int | None = None,
    idle: Any = ...,            # pad value when the head has nothing buffered
    name: str = "window",
    rank: int | None = None,
    rank_policy: str | None = None,
    max_depth: int = 256,
    backpressure_at: int = 192,
) -> WindowStage

Clips/buffers the inbound stream into windows — the streaming dual of ctx.pace. Where the pacer spaces items out in time, the window accumulates window inbound items and emits them as one clip every stride (default non-overlapping). Insert it after an inbound ctx.stream(...) for a model that wants windows rather than the raw stream:

pipeline = (
    ctx.stream("mic")
    | ctx.window(window=24)
    | ctx.source(make_model(model), device="cuda")
    | ctx.pace(fps=24)
    | ctx.stream("captions")
)

idle= supplies a pad value used when the inbound head has nothing buffered (idle-park). A pattern that seems to need hand-rolled buffering is a missing windowing stage — use ctx.window, don't roll your own loop.


ctx.self_test()

with ctx.self_test():
    ...

A context manager — the one end-to-end gate. It is self-driving: the app's block drives one representative pass through the composed pipeline (with pipeline.drive(frames=N)) and asserts its own success criteria. That single block simultaneously compiles every component (precompile), warms caches/allocator (warmup), and validates (self-test).

with ctx.self_test():
    out = pipeline.drive(frames=1)      # the app drives its own representative pass
    _assert_frames_shaped(out)          # the app asserts its own criteria — shape/dtype, no value read

Mandatory-gate semantics are the block's natural exception flow:

  • Clean exit = validated. A passed self-test is recorded (the readiness gate reads it) and, in prod, the weight-excluded snapshot is triggered at this validated point, then serving proceeds.
  • A raise inside the block = self-test failed. A failed report is recorded, readiness is forced not-open (nothing serves, nothing is snapshotted), the last-validated snapshot keeps serving, and the exception propagates so the deploy/boot path sees the real failure.

The same block is your CPU test's pass/fail check: under MockContext with the mocked store, a clean exit passes and a raise fails. See Strict mode & CPU testing.


Reactive control

A live control doc steers a running pipeline in exactly two ways — cheap config is read inline; structural change reconstructs a stage. There is no field-mapping table.

Cheap / live config (inline read)

doc.desired.<prop> is a live reactive accessor over the CRDT: its .value reads the current snapshot each time. A stage reads it inline at a chunk boundary — no machinery, no bind:

def make_denoise(models, doc):
    def denoise(*, stop_event, config):
        while not stop_event.is_set():
            yield models.denoise(doc.desired.prompt.value)   # live read each chunk
    return denoise

doc.bind()

doc.bind(
    construct: Callable[[Doc], Stage],
    deps: list[LiveProp] | None = None,
) -> Stage

useMemo for stages. construct is (doc) -> stage (it builds a composable stage, reading what it needs from the doc); deps is a list of live doc properties ([doc.desired.vae.model]). bind evaluates construct(doc) once and, when any dep property value changes (shallow compare, like useMemo), re-runs construct → reconstructs the stage → the pipeline drains and swaps it at a safe boundary. It returns a composable stage, so it slots straight into the | chain:

doc = ctx.doc("control")
pipeline = (
    ctx.stream("mic") | ctx.window(window=24)
    | ctx.source(make_denoise(models, doc))                                   # cheap reads inline
    | doc.bind(lambda d: ctx.pipe(make_vae(models, d)), [doc.desired.vae.model])  # useMemo
    | ctx.pace(fps=24) | ctx.stream("video", codec="h264")
)
await pipeline.run()

Use doc.bind ONLY for structural change (model / resolution / topology) that needs a rebuild; cheap config stays an inline live doc.desired.* read.

pipeline.bind(doc, mapping) is retired

The one-off pipeline.bind(doc, {doc_path: config_key}) field-mapping is superseded. There is no mapping and no reread-vs-reconstruct detection to configure — cheap config is an inline live read, structural change is doc.bind(construct, [live props]).

Auto tensor conversion: when a pipeline stage outputs a float tensor (NHWC, values in [0,1]), the ctx.stream sink auto-converts it to encoded frames before sending. The app emits a tensor and never touches PIL/numpy itself.

On this page