Getting Started
Write, deploy, and stream from your first uRun GPU function
This guide takes you from an empty file to a deployed GPU function you can invoke as a real-time session. You will write a Python backend, deploy it with the urun CLI, and see it appear in your console.
Install
Everything you need on the Python side lives in one package: urun-cli.
It installs the urun command (urun deploy, urun list, …) and the importable
urun compute library (from urun import App) you write your function against.
Install it into the project environment where you author your app so both the command and
the import are available together:
uv is a fast Python package manager. Install it with curl -LsSf https://astral.sh/uv/install.sh | sh, then add it to your project:
uv add urun-cliTo install the urun command globally on your PATH (handy if you only deploy and
author the app elsewhere):
uv tool install urun-clipip install urun-cliTo run the command without installing it (CI or a throwaway shell):
uvx urun-cli --versionVerify it:
urun --versionOne Python package, two faces
There is no separate urun package on PyPI — pip install urun-cli gives you both the
urun command (deploy/list/scale) and the import urun compute library. The only
other SDK is @urun-sh/core, the browser client covered in the
TypeScript SDK.
Authenticate
uRun authenticates the CLI with an org-scoped deploy API key of the form urun_sk_ followed by 43 base64url characters. An operator vends this key for your org from the console.
urun login --api-key urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uVThis verifies the key against your org config and stores it locally. For CI, set it in the environment instead:
export URUN_API_KEY=urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uVDeploy keys are not browser credentials
The urun_… key authorizes deploys from your machine or CI. Never ship it to a browser. Browser clients authenticate with WorkOS or a customer JWT — see TypeScript SDK auth.
Write a streaming function
Create app.py. This mirrors a real shipped app: one long-lived runtime owner on a single GPU that holds a model warm and produces a steered video stream.
import urun
from urun import App, Context
from urun.core import Dependencies, Credentials
app = App("hello-stream")
DEPS = Dependencies(python=["torch>=2.5.0", "diffusers", "transformers"])
CREDS = Credentials(optional_env=["HF_TOKEN"])
# The decorator kwargs are the function's config — GPU shape, deps, secrets.
# They are fixed at deploy time; session creation cannot override them.
@app.function(gpus="b200:1", deps=DEPS, credentials=CREDS)
async def generate(ctx: Context):
# @store.cache keeps the model resident in GPU memory across sessions:
# the first session loads it, every later session reuses it warm.
pipe = load_pipeline(str(ctx.device))
# A named video stream the browser subscribes to by name ("video").
video = ctx.stream("video", codec="h264", fps=16)
# The control doc syncs from the browser. Cheap config (the prompt) is a
# live reactive read inline in the generator — no mapping, no polling loop.
control = ctx.doc("control")
def frames(*, stop_event, config):
while not stop_event.is_set():
prompt = control.desired.prompt.value or "a calm ocean at sunset"
for frame in pipe(prompt=prompt, num_frames=17).frames[0]:
yield frame
pipeline = (
ctx.source(frames, device="cuda")
| ctx.pace(fps=16)
| video
)
await pipeline.run()
@urun.store.cache(key="pipeline") # gpu_pin=True is the default — the model stays in GPU memory
def load_pipeline(device: str):
import torch
from diffusers import DiffusionPipeline
model_path = urun.store.path("hf://Wan-AI/Wan2.1-T2V-1.3B")
pipe = DiffusionPipeline.from_pretrained(model_path, torch_dtype=torch.bfloat16)
return pipe.to(device)What the moving parts do:
@app.function(gpus="b200:1", …)— registers a deployed function.gpus="b200:1"requests one B200; the format is"{type}:{count}". This is config, fixed at deploy.ctx: Context— uRun injects the session context:ctx.device,ctx.rank/ctx.world_size(for multi-GPU),ctx.session_id,ctx.stream(...), andctx.doc(...).@store.cache(gpu_pin=True)— loads the model once and keeps it in GPU memory across sessions on the same worker. See Store.ctx.stream("video", codec="h264")— a named media sink. Passingcodec/bitratemakes it a pipeline video sink that encodes frames automatically.ctx.source | ctx.pace | video— the streaming pipeline: a generator head, frame-rate pacing, and the encoded video sink.control.desired.prompt.value— a live reactive read of the browser-synced control doc, inline in the generator (cheap config); structural change usesdoc.bind. See reactive control.await pipeline.run()— drives the pipeline continuously until the session disconnects.
Deploy
urun deploy app.pyYou will see uRun package the app directory, register a content-addressed manifest, upload any missing blobs, and poll the build to readiness:
Deploying hello-stream (fa61f31b0961)
Files: 1 app, deps: pip
Uploading 3 missing blob(s)
Deployment queued: 9c2f…
Waiting for build to complete (--no-wait to skip)Once the build is ready and capacity is warm, urun app list shows the app as ready. (See the CLI docs for the full status table.)
Invoke it from the browser
The deployed function name (generate) is exactly what the browser calls. With the TypeScript SDK:
import { App } from '@urun-sh/core'
const app = App('hello-stream', { baseUrl: 'https://urun.sh', orgId: 'your-org-id' })
const session = app.generate() // returns a Session
const video = session.stream('video')
video.on('track', (track) => {
videoEl.srcObject = new MediaStream([track])
})
// steer it live by writing the control doc:
session.doc('control').set({ prompt: { text: 'a forest at dawn' } })The names line up: ctx.stream("video") ↔ session.stream("video"), ctx.doc("control") ↔ session.doc("control").
What just happened
urun deploypackaged your app and registered the function's config (GPU shape, deps, credentials) with the control plane.- The browser's
app.generate()call allocated a session, warmed the GPU, and dialed the media transport. @store.cachekept the model resident so the second session skips the load.- The control doc gave the browser a live steering surface with no REST endpoints — just named streams and a synced doc.
Next steps
- App & functions — the decorator, local entrypoints, runtime overrides.
- Compute — GPU/CPU specs and multi-GPU replica groups.
- Streaming pipelines — the full
ctxpipeline API. - Store — model downloads, versioned artifacts, and
@store.cache.