Skip to content

simulo.App

The user-facing job container. Declare it once at module scope.

An app names your project and (optionally) mounts assets/volumes into every job. Its jobs execute in a Simulo-curated runtime — the platform’s default runtime unless the app picks a different Simulo runtime via runtime=. Keep the app and task in task.py; a submitted file contains one typed job() and imports them. simulo run train.py [--flags] maps flags onto that job’s signature:

from typing import Any
import simulo
from task import CartpoleTask, app
@app.job(type="train", system=simulo.SystemType.TIER_1)
def train(num_envs: int = 16, max_iterations: int = 2) -> dict[str, Any]:
env = simulo.LearningEnv(task=CartpoleTask(), num_envs=num_envs, device="cuda")
trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42)
stats = trainer.train(max_iterations=max_iterations)
trainer.close()
env.close()
return {"num_envs": num_envs, **stats}

Args:

  • name — Non-empty app name, recorded in the package manifest.
  • runtime — Optionally pick a different Simulo runtime for this app’s jobs (simulo.Runtime.from_registry("simulo/gpu-rl:2026.06")). Omitted means the platform default.
  • mounts — Optional mapping of mount name to simulo.Asset / simulo.Volume, shared by every job of this app.

Raises:

  • ValueError — if name is empty or whitespace-only.

Real usage, from the shipped hello example. task.py declares the app and the task, and train.py holds the file’s one typed job:

task.py
app = simulo.App("hello")
# train.py
from task import CartpoleTask, app
@app.job(type="train", system=simulo.SystemType.TIER_1, timeout=10 * 60)
def train(num_envs: int = 16, max_iterations: int = 2) -> dict[str, Any]:
env = simulo.LearningEnv(task=CartpoleTask(), num_envs=num_envs, device="cuda")
trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42)
stats = trainer.train(max_iterations=max_iterations)
trainer.close()
env.close()
return {"num_envs": num_envs, **stats}

simulo run train.py --num-envs 16 --max-iterations 2 submits it: each job parameter becomes a flag. See simulo.Volume for a job that also mounts a writable scratch folder.

simulo.App(
name: str,
*,
runtime: Optional[object] = None,
mounts: Optional[Mapping[str, Mount]] = None,
)
App.job(
fn: Optional[F] = None,
*,
type: Optional[Union[JobType, str]] = None,
callbacks: Sequence[JobCallbackProtocol] = (),
system: Optional[SystemType] = None,
timeout: Optional[int] = None,
retries: int = 0,
**extra: Any,
) -> Union[JobFunction, Callable[[F], JobFunction]]

Register a function as a job of this app. Records metadata; never runs the body.

type= is required and accepts a simulo.JobType or its wire string, for example @app.job(type="train"). Returns a JobFunction wrapping the original function — calling it still runs the body in-process; .spawn() / .submit() submit it to the platform instead.

Args:

  • fn — The function being decorated (bare-decorator form only — never pass it alongside keyword options).
  • callbacks — Lifecycle callbacks from simulo.callbacks, recorded into the package manifest and honored at execution.
  • system — The GPU system tier to request, as a simulo.SystemType member (e.g. simulo.SystemType.TIER_1), recorded in the job’s resource spec as resources["system"] (the tier’s wire value, e.g. "tier1"). Validated HERE, at decoration time, against the published catalog: a value that is not a SystemType member (a raw string such as "tier1" included) raises TypeError, and a tier the platform cannot provision yet raises ValueError. simulo systems lists every tier and which are available today. The selection is recorded on the job AND scheduled against: the control plane filters a tiered worker’s claim to its own tier plus untiered work, so the tier decides which GPU model runs the job. It is not a priority setting. simulo systems reports this package’s catalog, not which tiers your platform has deployed. Naming a tier it has no fleet for is refused when you submit, and the error names the tiers it does have.
  • timeout — Requested runtime limit for one managed execution, in seconds (a positive integer; validated HERE, at decoration time — a zero, negative, or non-integer value raises ValueError immediately rather than failing later at run time on the worker). At submission, the platform uses the smaller of this value and your organization’s runtime limit. When that deadline expires, the job is reported timed_out with reason org_runtime_limit_exceeded. Separately, every GPU machine enforces its own MAX_JOB_TIMEOUT_S ceiling (1 hour by default); a job killed by that ceiling is reported failed with reason timeout. When omitted (None, the default), your organization’s effective runtime limit applies.
  • retries — Automatic re-attempts after a failure (default 0). All attempts share one timeout budget.
  • **extra — Additional resource requests, recorded verbatim in the job’s resource spec. Three names are RESERVED and rejected — produces, consumes and gpu (see Raises below); everything else passes through to the resource spec unchanged.

Raises:

  • ValueError — timeout is not None and is not a positive integer. True and False are not accepted as timeouts. Also raised when system names a tier the platform cannot provision yet (SYSTEM_SPECS[system].available is false).
  • JobDeclarationError — produces= or consumes= was passed. Outputs are not declared on the decorator. Callbacks declare their own supported outputs. Return small JSON values from the job, or use a named volume for files another job needs. consumes= is reserved; continue from a checkpoint with simulo run train.py --from policy_x:best instead. Also raised for gpu= (the free-text GPU request this parameter replaced; it would otherwise fall through **extra into the resource spec unvalidated) and for a system value that is not a simulo.SystemType member.
App.name: str

property

The app’s name, as passed at construction.

App.runtime: Any

property

The Simulo runtime this app’s jobs execute in (the default unless picked).

App.mounts: Mapping[str, Mount]

property

The assets/volumes mounted into every job of this app, by mount name (a copy).

App.jobs: Mapping[str, JobSpec]

property

The registered job specs, by job name (a copy).

What @app.job returns: the original function, wrapped for submission.

Calling it runs the body in-process, exactly as if it were undecorated (that is also how the platform invokes it on a worker). spawn() and submit() package and submit it instead. You never construct one yourself. Decorate one training function and submit its file with simulo run train.py.

class JobFunction
JobFunction.spawn(**kwargs: Any) -> JobHandle

Submit this job to the Simulo cloud (once logged in), or write it locally.

Submitting writes the manifest + source bundle (memoised once per app), recording this job’s name and kwargs into the manifest so the job can be run later with fn(**kwargs).

Cloud mode — credentials exist (simulo login), or SIMULO_API_URL/SIMULO_ENV names a target (and SIMULO_SUBMIT is not local): the package is tarred, uploaded, and a job is created on the Simulo cloud. The returned JobHandle carries the SERVER-assigned job_id and .get() streams the real result.

Local-disk mode (the default with no cloud signal present): NEVER trains and never runs the job — the handle exposes only .package_path / .job_id (a locally-derived id), and .get() raises, pointing you at simulo login to submit to the cloud instead.

Positional arguments are rejected: a job body receives only the keyword arguments recorded in the manifest.

JobFunction.submit(*args: object, **kwargs: object) -> JobId

Submit this job (keyword args only): write the package, return its id.

Like spawn() but returns just the JobId (JobFunctionProtocol). It writes the package and never executes; nothing is dispatched.