Runtimes
Every Simulo job executes in a Simulo runtime: a prebuilt, fully-provisioned
cloud environment that ships everything a robotics simulation and training job
needs — the simulation engine, the GPU stack, torch, numpy, and the rest of
the scientific-Python toolchain. Simulo owns and builds the base image; you
never bring your own image or registry. On top of that base, a Runtime can
layer two guarded extras: pip_install() adds public-PyPI packages the
base doesn’t ship, and env() sets environment variables for the job. Most
apps need neither — the default runtime is fully provisioned — but when your
reward or task genuinely depends on a third-party library, this is how you
bring it.
The default runtime
Section titled “The default runtime”You normally write nothing at all — every app runs on the current default,
simulo/gpu-rl:2026.06:
import simulo
app = simulo.App("cartpole") # runs on the default Simulo runtimePicking a runtime
Section titled “Picking a runtime”As the curated lineup grows, an app can pick a different Simulo runtime by name:
app = simulo.App( "cartpole", runtime=simulo.Runtime.from_registry("simulo/gpu-rl:2026.06"),)Runtime.from_registry(name) names one of Simulo’s runtimes — it is a choice
from the catalog, not a door to arbitrary images. There is no
bring-your-own-image and no private registry: if your job needs something the
base image itself must ship (a different CUDA version, a compiled system
library), that’s a runtime request for the Simulo team, not a per-app install.
For a plain public-PyPI package, see the next section — that one you add
yourself.
The name has to be one Simulo offers
Section titled “The name has to be one Simulo offers”If it isn’t, the submit is refused — before a single byte is uploaded and before any job exists — and the message names the runtimes that do:
✗ unknown runtime 'simulo/python-cpu:3.11' — this platform does not offer it; available runtimes: simulo/gpu-rl:2026.06This is part of the pre-submit check simulo run makes automatically, and the
same one simulo prepare runs on its own,
so you see it whichever way you submit.
Naming a runtime that doesn’t exist used to be a warning, and your job would quietly run on the default instead. That was worse than it looks: the run succeeded, the output looked right, and nothing told you that the environment you asked for was not the environment you got. Being refused up front is the point — you find out while you can still fix the name.
Since the lineup is one runtime today, in practice this only fires on a name
that was never real — a typo, or a runtime someone read about that Simulo does
not offer. Omit runtime= entirely and you always get the default, so an app
that never names a runtime can never hit this.
Bringing your own PyPI packages: Runtime.pip_install()
Section titled “Bringing your own PyPI packages: Runtime.pip_install()”The base runtime ships torch, numpy, and the scientific-Python stack, but
not every third-party library a reward or task might need. pip_install()
declares public-PyPI packages the Simulo image builder installs on top of the
base runtime before your job runs — you never build or push an image
yourself:
import simulo
runtime = ( simulo.Runtime.from_registry("simulo/gpu-rl:2026.06") .pip_install("shapely"))app = simulo.App("zone-approach", runtime=runtime)
with app.runtime.imports(): import torch # noqa: F401 — base runtime, resolved on the worker from shapely.geometry import Point, Polygon # noqa: F401 — added by pip_install()Construct a dedicated Runtime via Runtime.from_registry(...) and pass
it to App(runtime=...), rather than mutating app.runtime on an App that
omits runtime= — every such app shares the same default runtime instance,
and pip_install() mutates whatever Runtime it’s called on. A task can then
genuinely depend on the library — for example, computing a reward with real
shapely.geometry.Polygon distance/contains checks against a target zone,
not a token import that’s never called. pip_install() and env() (next
section) chain onto the same Runtime. The public pip-install-shapely
sample does exactly that: the target zone’s center is a real env()-set parameter.
pip_install() is chainable and cumulative — each call appends an ordered
layer, and the builder installs layers in the order you declared them:
runtime = ( simulo.Runtime.from_registry("simulo/gpu-rl:2026.06") .pip_install("shapely") .pip_install("wandb==0.17.0", pre=False))Guardrails:
- Public PyPI only. Each argument is a plain PEP 508 requirement — a name,
optionally with extras and a version specifier (
"shapely","transformers[torch]>=4.40"). URLs, VCS refs (git+...), local paths, and archive files are rejected at call time — there is no way to pointpip_install()at anything but the public index. - You can’t override a base package. A spec naming a package the base
runtime already provides —
torch,numpy, the CUDA wheels, thesimulodistributions themselves — fails at submit with a clear error listing the offenders, before anything uploads. This is deliberate: a shadowing version of a base package has broken the runtime’s compiled bindings in ways invisible from your own job’s traceback. - Your packages install at the tail. Even for a package the base runtime doesn’t ship, your layer is added after the base environment, never ahead of it — the platform’s own pins always resolve first.
pip_install()with no arguments is a no-op — it records nothing, rather than a vacuous empty layer.
Setting environment variables: Runtime.env()
Section titled “Setting environment variables: Runtime.env()”env() merges environment variables into the job’s execution environment.
Later calls win on key conflicts, and it’s chainable like pip_install():
runtime = simulo.Runtime.from_registry("simulo/gpu-rl:2026.06").env( {"WANDB_PROJECT": "cartpole", "WANDB_MODE": "offline"})Only a specific set of runner/worker-owned names is rejected — the exact keys
and narrow prefixes the platform reserves for mount paths, job/worker
identity, and operator knobs (PATH/PYTHONPATH/HOME plus a handful of
SIMULO_* names and prefixes), not a blanket ban on SIMULO_*. Job-tunable
SIMULO_* knobs your app already reads — SIMULO_CHECKPOINT_EVERY,
SIMULO_RESUME, SIMULO_EVAL_* — are not reserved and DO take effect through
env(). A reserved key is skipped with a warning rather than applied — it
never redirects the platform’s own machinery, and it never fails your submit
either. Set anything else your job reads from os.environ — an
experiment-tracking project name, a feature flag, a tuning knob your own code
checks for.
The remote-import boundary: app.runtime.imports()
Section titled “The remote-import boundary: app.runtime.imports()”Heavy imports — torch, anything not safe to resolve on a GPU-less submit
machine — are written once, at module level, inside
with app.runtime.imports():
with app.runtime.imports(): import torch # noqa: F401 — resolved only on the worker- On your machine, the import is recorded as deferred and never actually resolved — submitting stays fast and works with nothing heavy installed.
- On the cloud worker, it’s a plain pass-through — the import happens for real.
This is the one place heavy imports belong. Anywhere else at module level,
an import torch would break simulo run on a machine that doesn’t have
torch installed. (Imports inside a job body need no guard — job bodies
never run at submit.)
JIT-compiled kernels: @app.runtime.torch_jit
Section titled “JIT-compiled kernels: @app.runtime.torch_jit”Reward or observation kernels that benefit from torch.jit.script
compilation are written as module-level functions decorated with
@app.runtime.torch_jit:
@app.runtime.torch_jitdef _compute_rewards(pole_pos, pole_vel, cart_pos, cart_vel, reset_terminated): ... return reward- On your machine, this is a no-op marker — the function is recorded and returned unchanged, so defining it costs nothing torch-wise.
- On the worker, it’s replaced with the real
torch.jit.script-compiled function.
This lets a JIT-compiled kernel live at module level (readable, testable) — you don’t need to construct it lazily inside a job body.