Skip to content

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.

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 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.

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.06

This 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 point pip_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, the simulo distributions 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.

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.)

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_jit
def _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.