The contract
1 · A provider is a callable, a Runtime is an address
The whole layer is two definitions. A Provider is a structural protocol - no base class, no
registry - so any function of the right shape is a valid placement strategy. A Runtime is the
value it yields: where to connect, nothing more.runtime.py · the contract
- The task flows into placement. A provider receives the row it is placing, so per-row decisions (this env from that file, this row on a bigger GPU) live in the provider, and the engine never branches on them.
- A
Runtimeis itself a provider - the borrowed case. Calling it returnsnullcontext(self): entering provisions nothing and yields the address as-is, exiting tears down nothing, because whoever launched that server owns it. This is the degenerate provider every routing pattern below bottoms out in.
LocalRuntime, SubprocessRuntime, DockerRuntime, ModalRuntime,
DaytonaRuntime, HUDRuntime) are all providers of this shape. They differ only in where the
substrate runs and how its url is reached; all of them serve the same module and speak the same
channel.
Resolution
Placement is chosen once per batch. The scheduler,Taskset.run(agent, runtime=...)
(hud/eval/taskset.py), resolves a single provider for all rows and threads it into every rollout;
per-row behavior happens inside the provider, never in the scheduler.
When runtime= is omitted, _resolve_placement uses what the process already knows:
- A taskset loaded from
.pysource serves that source fresh per rollout (aLocalRuntimeon the file), provided the source actually declares the rows’ envs. - A platform taskset runs against the platform (
HUDRuntime). - Rows naming envs declared in already-imported modules serve each env fresh from its declaring file.
- Anything else raises, naming the forms to pass.
HostedRuntime, which submits the whole rollout to the platform and folds the result back into a
Run. The scheduler picks between the two, so the atom never branches on placement.
Routing patterns
Because the contract is a callable that receives the row, routing is composition, not configuration. Four patterns cover what exists today.Fresh substrate per rollout
The default, and what every built-in does: one acquisition brings up one new substrate and tears it down on exit. Isolation is structural - two rollouts never share state because they never share a substrate.Build the environment from the row
TheLocalRuntime constructor form takes a (task) -> Environment callable, invoked per
acquisition with the placed row. This is the pattern for benchmarks where one process holds one
scene (an Isaac sim, where the scene is fixed at build time): one declaration file, many rows, and
the constructor reads the row to build the right scene.
Route rows across providers
A lambda that reads the row and picks a provider routes a heterogeneous batch: mixed-env tasksets, per-row images, per-row resources. The scheduler still resolves one “provider” - the lambda - and all branching stays inside it.Borrow warm substrates
When provisioning is expensive (a sim that takes a minute to boot), serve long-lived processes once and map rows onto them as bareRuntime addresses. Entering the context is a no-op, so episodes
against one task reuse its warm substrate, and per-episode variation travels through task args
instead of fresh builds.
A control channel supports concurrent sessions, one suspended task each (see the
control channel). Independent rows can use a
shared warm server concurrently. Its substrate is one resource, so rows that drive a single shared
world (one sim) require bounded concurrency or sequential execution.