Skip to content

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}
Terminal window
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.

When you submit an app to the cloud, simulo run app.py follows four steps:

  1. Your entrypoint runs on your machine — or, with none, the CLI submits the job directly. With an @app.entrypoint, simulo run imports the file and calls it. With no entrypoint, simulo run maps its own --flag values 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 like torch stay deferred (see Runtimes) — so this step works on any machine with Python 3.11+, GPU or not.

  2. .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 any env() / 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.

  3. The cloud runs the job. A GPU worker picks the job up, imports the same packaged source, and calls your @app.job function — this is where torch, 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.

  4. You observe. simulo run follows the cloud job’s logs to completion by default (--detach to skip waiting), and simulo jobs / logs / result / models / outputs / recordings / view cover 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.

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 value maps onto that job’s own parameter of the same name (--num-envsnum_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 NAME to choose which one this simulo run submits — 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. trainevaluaterollout) usually have to run in that order:

    Terminal window
    simulo run app.py --job evaluate --num-episodes 10

    Omit --job on a multi-job file and simulo run refuses 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.entrypoint
def 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.

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.