Strict mode & CPU testing
One requirement — no data-dependent value-reads in the hot path — enables torch.compile, CUDA graphs, and zero-app-code CPU tests, validated by ctx.self_test.
A correctly-written uRun app runs CPU-only with zero app-specific test code. Because the platform owns everything heavy, stateful, or GPU-bound behind primitives, the shared test harness mocks the primitives and runs your app's real composition logic on a laptop — no GPU, no weights, no per-app stubs.
That guarantee rests on one requirement.
The core requirement
Do not read tensor values in the hot path. Reading a computed value off a model output — int(frames.shape[0]) from a data-dependent shape, if latent.mean() > thresh, frame_health(pixels), .item(), a numpy/PIL conversion mid-pipeline — is the single thing that:
- graph-breaks
torch.compile, - is illegal under CUDA graphs (which require static shapes and control flow), and
FakeTensorModecannot trace (it raisesDataDependentOutputException).
Avoiding it is the single requirement that simultaneously enables torch.compile, CUDA graphs, and zero-app-code CPU tracing. For an app that obeys it, the harness meta-inits the model and runs the whole forward + compose loop under FakeTensorMode on CPU: every shape is derived from registered fakes and meta kernels, so no real data is needed.
The custom-kernel hole closes the same way: a compliant custom kernel must be a registered torch.library op with a register_fake (abstract/meta) impl — which is exactly what FakeTensorMode needs to trace through it. One requirement serves compile, cudagraphs, and the harness.
Strict vs relaxed
| Strict | Relaxed | |
|---|---|---|
| Data-dependent value-reads in the hot path | No (guard-enforced) | Yes |
| Custom kernels | registered torch.library ops + register_fake | anything |
| Grants | torch.compile, CUDA graphs, zero-app-code FakeTensor CPU tests, fast cacheable cold-start/snapshot | runs, but none of these guarantees |
| Intended for | reference apps + production-grade apps | initial spikes, prototyping, genuinely dynamic long-tail |
| Tested by | FakeTensor trace (free, zero app code) | seeded real-tiny tensors / pure-fn unit tests / GPU + E2E |
Strictness is enforced by the conformance guard and is also what unlocks the performance and automatic-testing benefits above. Relaxed is the explicit, opt-in escape for the long tail — nothing is blocked by being relaxed, it just doesn't get the strict perks.
Where value-logic lives
Value-logic (drift bounding, frame health, reanchor decisions) reads numbers, so it is relaxed by nature. The fast architecture already separates it from the model:
- a strict compiled core — the forward (compile / cudagraph / FakeTensor-clean), and
- a thin eager control layer that reads values occasionally (drift every N chunks, a reanchor decision) outside the compiled region.
Three ways to keep value-logic out of the strict path, in order of preference:
- Push it device-side. Replace a Python
if x.mean() > t:branch withtorch.where(...)/ masked ops so the decision stays in the graph as a tensor op — no host read at all. - Put it on a flag-gated observability lane that is off in the strict trace (metrics/health you sample occasionally, not in the per-frame path).
- Mark it
@relaxed— only when the value-read is irreducible.@relaxedis both a decorator and a context manager; it marks the eager control layer as the explicit compiler / harness-inject seam.
The self-test is self-driving
with ctx.self_test(): is the driver, and it is self-contained: inside the block, the app writes its own representative pass and asserts its own success criteria, in code. It drives the composed pipeline (pipeline.drive(frames=N)) and asserts what "working" means for this app (_assert_frames_shaped(...)):
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 success criteriaIf anything external has to drive the pipeline to test it, it is not the right self-test — the block must be self-contained (drive + assert). That single block simultaneously compiles every component (precompile), warms caches/allocator (warmup), and validates the output (self-test): they are one pass, not three steps.
Mandatory-gate semantics are the block's natural exception flow — a clean exit = validated, a raise = rejected (nothing serves, the last-validated snapshot keeps serving, the error propagates).
One definition, used in five places
There is exactly one ctx.self_test per app, and it is the single definition of "working" — reused identically everywhere the platform needs to know the app is healthy:
- prod warmup ≡ precompile ≡ validate — the one E2E pass that compiles and warms everything;
- the prod readiness gate — nothing serves until it passes;
- the weight-excluded snapshot trigger — taken at the validated clean-exit point;
- the CPU-harness result — run the handler under the mocked primitives; the block drives itself, and a clean exit means pass while a
raisemeans fail; - the iterate / hot-reload re-validation gate — re-run on a code delta that touches the validated path.
A passing CPU-harness run therefore means exactly one thing: the app's own production readiness gate passes under the mocked primitives.
The CPU harness replays the lifecycle
The harness is not a bespoke test runner. It mocks the primitives and replays the standard session lifecycle — the same launcher code path prod runs (bootstrap → import the app → load via the mock store → compose → the warm phase), just with mocked GPU/weights/streams and truncated at warm (no snapshot, no serve, no continuous run). The warm phase is ctx.self_test() (it drives + asserts). Reach the end of warm without raising → the test passes. Same path as prod = high fidelity, zero app test code.
So the per-app CPU test is a ~10-LOC uniform driver with zero app-specific test code — no StubModel, no injected-utterance script, no bespoke asserts, no test-side pipeline.drive(). It is essentially identical across apps:
# illustrative — `replay_lifecycle` is the not-yet-final driver name (see the callout below)
from pathlib import Path
from urun.testing import replay_lifecycle, use_test_store, assert_strict, assert_primitives_only
import restyle.app as appmod
APP = [Path("restyle/app")]
def test_restyle_cpu():
with use_test_store(): # mocked model loading — no weights, no GPU
replay_lifecycle(appmod) # bootstrap → import → load → compose → WARM (ctx.self_test)
# reaching the end of warm without raising means the test passed
def test_restyle_strict():
assert_strict(APP) # no value-reads in the strict path, no device/IO escapes
assert_primitives_only(APP) # no raw sockets / subprocess / threads-as-driverAPI shapes
ctx.self_test, pipeline.drive(frames=N), doc.bind, app.store, store.repo, and — in urun.testing — use_test_store, assert_strict, and assert_primitives_only are all built today. The one API not yet final is the replay_lifecycle(...) driver — the lifecycle-replay model and the zero-app-code contract are settled; urun.testing ships Lifecycle / install_store_cache_lifecycle today, so track the SDK reference for the final driver import.
Keep the self-test shape-only
The assert inside ctx.self_test() must check shape and dtype, not pixel or scalar values — a value-read there would itself break the FakeTensor trace. "Did the pipeline produce a tensor of the right shape and dtype" is the contract; correctness of the content is a GPU/E2E concern.
assert_frame — the canonical shape assertion
from urun import assert_frame is the platform-provided strict-safe shape check. Unlike a hand-written assert, it works correctly in all modes — including FakeTensorMode for the CPU harness:
from urun import assert_frame
with ctx.self_test():
frames = pipeline.drive(frames=2)
# shape= accepts a single shape tuple OR a set of allowed shapes.
# Use None for a dynamic (data-dependent) dimension.
assert_frame(frames, shape=(CFG.height, CFG.width, 3))
# For a model that supports multiple resolutions:
assert_frame(frames, shape={(480, 832, 3), (720, 1280, 3)})
# For a dynamic batch dim:
assert_frame(frames, shape=(None, CFG.width, 3))The auto-stub model seam (use_test_store()) emits shape-conformant zero-frames derived from the shape= argument, so the self-test passes on CPU with no real weights. Never assert pixel values or scalar statistics inside ctx.self_test() — those are data-dependent reads that break FakeTensor tracing.
The conformance guard
The strict-mode requirements are enforced by a conformance guard that runs in CI, locally, and at iterate-time (folded into the lifecycle self-test). For a strict app it checks: no data-dependent value-reads in the hot path, custom kernels registered as torch.library ops with a register_fake impl, and no IO/device escapes (no hardcoded device="cuda" / .item() in the hot path, no raw sockets / subprocess / threads-as-driver / file watchers). A strict app that regresses fails CI. The diagnostic doesn't just gate — it points at the offending line and the remedy ("move this .item() into a @relaxed region", "take device from ctx, not 'cuda'").
In your test suite this is two lines next to the lifecycle-replay test:
assert_strict(APP) # no value-reads in the strict path, no device/IO escapes
assert_primitives_only(APP) # no raw sockets / subprocess / threads-as-driverConfig, not env vars
Strict apps configure via @app.function(...) kwargs and one frozen @dataclass config — never behavior-tuning environment variables. No os.environ.get("APP_*"), no setdefault forests, no escape-hatch flags. The only env vars that remain are external inputs (secrets, and launcher/k8s-injected values like RANK / WORLD_SIZE / LOCAL_RANK). This is part of what makes the app deterministic enough to trace on CPU. See Anatomy of a uRun app.
Related:
- Anatomy of a uRun app — the whole canonical shape + app-hygiene rules
- Streaming Pipelines — the unified Stage DSL and
ctx.self_test - Store —
@store.cacheweights residency + snapshot collaboration - Context reference —
ctx.self_testsignature