App & Jobs
simulo.App is the top-level container for a Simulo application: it names
the app and declares any volumes it mounts.
Its jobs execute in a Simulo-provided runtime — the
default unless the app picks a different one. Every app file declares
exactly one App and one or more @app.job functions. @app.entrypoint is
optional — the CLI maps simulo run’s flags onto a job’s own signature
directly, so an entrypoint is only needed for actual multi-job orchestration
(one submit call driving several jobs, or picking which one to run yourself).
The smallest possible app has no entrypoint at all:
import simulo
app = simulo.App("hello")
@app.job(timeout=5 * 60)def hello(name: str = "world", repeat: int = 3) -> dict: for i in range(repeat): print(f"hello, {name}! ({i + 1}/{repeat})") return {"greeting": f"hello, {name}", "repeat": repeat}simulo run app.py --name ada --repeat 5--name and --repeat map straight onto hello’s own parameters — no second
concept to learn. See Submitting without an entrypoint
below for the full behavior, including apps with more than one job.
How a run works
Section titled “How a run works”When you submit an app to the cloud, simulo run app.py follows four steps:
-
Your entrypoint runs on your machine — or, with none, the CLI submits the job directly. With an
@app.entrypoint,simulo runimports the file and calls it. With no entrypoint,simulo runmaps its own--flagvalues onto the sole job’s signature (or, with several jobs, the--job-selected one) and calls.spawn()itself — see Submitting without an entrypoint. Either way, job bodies do not run here, and heavy imports liketorchstay deferred (see Runtimes) — so this step works on any machine with Python 3.11+, GPU or not. -
.spawn()packages the app. The.spawn()call — yours, inside an entrypoint, or the CLI’s own on the no-entrypoint path — builds a content-addressed package — your source plus a manifest recording the entrypoint (if any), the picked Simulo runtime (plus anyenv()/pip_install()layers it declared), declared resources, volumes, and arguments. When you are signed in or have selected a cloud environment, it uploads that package and creates a cloud job. -
The cloud runs the job. A GPU worker picks the job up, imports the same packaged source, and calls your
@app.jobfunction — this is wheretorch, the simulator, and your training loop actually run. Because the package captures your code unchanged, the file you wrote on a GPU-less laptop is byte-for-byte the file that trains on the worker. -
You observe.
simulo runfollows the cloud job’s logs to completion by default (--detachto skip waiting), andsimulo jobs/logs/result/models/outputs/recordings/viewcover the job produces — see Observe.
Without a login or selected cloud environment, .spawn() writes the package
locally and does not run the job. Sign in, then run the command again to
submit it.
A direct python app.py submits nothing: Simulo detects the plain-python
invocation, prints the equivalent simulo run command, and exits with an
error — a script that wraps the wrong form fails loudly instead of silently
doing nothing. See Run & submit.
@app.job registers a function as a managed unit of work. It records
metadata — it never runs the function body at decoration time. Common
arguments:
gpu— the GPU class the job needs (e.g."L4"); omit when the job needs no GPU.timeout— maximum wall-clock seconds before the job is killed.retries— how many times a failed/preempted job is retried.callbacks— a list of job lifecycle callbacks, e.g.simulo.callbacks.ResumableCheckpoint(every=50).
A single App can register multiple jobs that share volumes — a
train → evaluate → infer lifecycle, for example, rather than one
do-everything entrypoint. See the Examples section for a
worked multi-job app.
Submitting without an entrypoint
Section titled “Submitting without an entrypoint”Most apps declare no @app.entrypoint at all — simulo run maps its flags
onto a job’s own signature directly, the same introspection an entrypoint
would otherwise need:
-
Exactly one
@app.job: every--flag valuemaps onto that job’s own parameter of the same name (--num-envs→num_envs), and the job is submitted with the explicitly-passed flags plus its own defaults for everything else. -
More than one
@app.job: pass--job NAMEto choose which one thissimulo runsubmits — the remaining flags map onto that job’s signature. Job names are listed in declaration order (the order they appear in the file), not alphabetically, since a staged app’s stages (e.g.train→evaluate→rollout) usually have to run in that order:Terminal window simulo run app.py --job evaluate --num-episodes 10Omit
--jobon a multi-job file andsimulo runrefuses rather than guessing, naming the valid set:app.py declares no @app.entrypoint and 3 jobs (train, evaluate, rollout);pass --job NAME to choose the one to submit (e.g. --job train).
--job is deliberately not one of simulo run’s own listed flags (see
Run & submit) — it is resolved from the
job’s own parameters after the app file is read, precisely so a job
parameter that happens to be named job (a real shape some staged apps
used before this existed) is never shadowed. A job parameter whose flag form
collides with one of simulo run’s own flags — --entrypoint, --detach,
--viewstream, --frozen, --strict-assets, --from, --skip-preflight,
--describe, or --job/--help themselves — is rejected at submit with
a clear message rather than silently failing to reach the job.
@app.entrypoint is for the case flag-mapping alone can’t cover: real
multi-job orchestration, where one simulo run should spawn more than
one job, or run other logic before/between/after spawning. Declare it and
simulo run invokes it on your machine instead of mapping flags onto a job
directly — its job is to call .spawn() (or .submit()) on one or more
@app.job functions, never to do the work itself.
@app.entrypointdef main(num_envs: int = 4096, max_iterations: int = 200) -> None: handle = train.spawn(num_envs=num_envs, max_iterations=max_iterations) print(f"job: {handle.job_name}")Its parameters become the simulo run command’s flags automatically —
--num-envs and --max-iterations above — so there’s no separate argument
parser to maintain. A file with several entrypoints picks one via
simulo run --entrypoint NAME.
spawn() and JobHandle
Section titled “spawn() and JobHandle”Calling .spawn(**kwargs) on a @app.job function creates its package
(manifest + source bundle). When you are signed in or have selected a cloud
environment, it also uploads the package and creates a cloud job. It returns a
JobHandle:
| Attribute / method | What it gives you |
|---|---|
.job_name |
The job’s name (the decorated function’s name). |
.package_path |
Where the package was written. |
.job_id |
For a cloud job, the server-assigned id used by simulo logs, result, and models. A local package has no cloud job to inspect. |