Catalog reference

Every file, every field. Writing a catalog explains the decisions; this is for looking things up.

A key not listed here is rejected. Every model sets extra="forbid", so a typo fails at refractal plan naming the exact path — scenes.0.resource_shape.0.vram_per_env_md — rather than being silently ignored. That is the single highest-value line in the schema.

refractal init . writes a working example of all five.

The five files

file answers hashed into
scenes.yaml what world, and what one environment of it costs scene_hash
tasks.yaml what counts as success, and the step budget task_hash
scenarios.yaml what varies between episodes scenario_hash
run.yaml which checkpoints, how many seeds plan_id
hardware.yaml what machine to plan against nothing

hardware.yaml is deliberately outside identity: the same experiment planned for a laptop and for three H100s is the same experiment, and plan_id does not move. Only the worker layout does.

The wrapper every file shares

field required what it does
apiVersion yes refractal.dev/v1alpha1. Present in all five files.

and then exactly one list, named for the file: scenes, tasks, scenario_sets, run, hardware_profiles.


scenes.yaml

apiVersion: refractal.dev/v1alpha1
scenes:
  - id: cube-bowl-v1
    engine: mujoco
    model: assets/cube_bowl.xml
    resource_shape:
      - hardware_profile: laptop
        envs_per_process: 1
        vram_per_env_mb: 0
        cpu_cores: 1
        sec_per_1k_steps: 12.0
        startup_sec: 2

scenes[]

field required default what it does
id yes Names the scene. Referenced by tasks[].scene and scenario_sets[].scene.
engine yes mujoco, mjx or isaac. Decides which image a rendered deployment uses.
model one of None Path to the model file, relative to the catalog. Exactly one of model and external — a required choice rather than an optional field, because an optional one is the one people forget.
external one of None For a scene defined by someone else's benchmark. See below.
engine_version no None Written by refractal build, not by hand. Part of the scene's identity.
model_hash no None Written by refractal build. Never hand-written.
assets no [] Extra files folded into scene_hash. A texture the model references is part of the scene.
resource_shape no [] One entry per hardware profile. See below.
metrics no [] What to measure while an episode runs. One entry per extractor. See below. Not hashed by itself: measuring more about a run does not make it a different run.
description no "" Free text. Not hashed.

scenes[].external

For a scene that lives in an installed package rather than a file here.

field required default what it does
provider yes Import string of the benchmark class, e.g. vla_eval.benchmarks.libero.benchmark:LIBEROBenchmark.
ref no {} How that provider identifies the scene, e.g. {suite: libero_spatial}.
params no {} Passed to the provider's constructor. Not name-validated — Refractal does not know the constructor's signature. They are hashed into scene_hash and recorded in build.lock, so changing one contradicts the lock and plan refuses until you rebuild. A typo present from the start is caught later, by the constructor.

scenes[].resource_shape[]

What one worker of this scene needs, per hardware profile. The planner divides the machine by this and refuses a plan that will not fit — so these numbers decide whether it can refuse at all.

field required default what it does
hardware_profile yes Which profile in hardware.yaml this shape describes.
envs_per_process yes Environments one worker holds at once. 1 for classic MuJoCo, which cannot batch.
vram_per_env_mb yes GPU memory per environment. Measure it. A wrong number gives a wrong plan; a zero removes the constraint entirely and the planner will never refuse anything.
vram_base_mb no 0 GPU memory the worker needs regardless of batch width. Total is vram_base_mb + vram_per_env_mb × envs. Put a fixed per-process cost here, not in the per-env figure — they are identical at one env and diverge the moment a scene batches.
cpu_cores yes Cores per environment. The worker's cpuset is this times the concurrent environments.
memory_mb no 2048 Host RAM per worker. Becomes the container memory limit.
sec_per_1k_steps yes Seconds per 1000 simulated steps, including inference if the worker blocks on a model server. Drives the duration bound.
startup_sec yes Cost of constructing the scene once. Above --pack-below-startup-sec, starved scenes are packed sequentially rather than given their own worker.
partition_unit no task The indivisible unit a worker owns whole: task or scene. task is the finer grain, and the ceiling on parallelism is the number of these.
max_envs no None A hard ceiling on batch width the planner cannot infer — a driver limit, a licence, a known instability. Validated but not acted on: it is checked against envs_per_process and otherwise only documents the limit.
measured_at no None A date. Read by nothing; a note to the next person that these were measured, and when.

tasks.yaml

field required default what it does
id yes Names the task. Must be unique across the catalog — merging two catalogs that both number from task-0 collides.
scene yes Which scene this task runs on.
instruction yes The language goal. For a wrapped benchmark this is the selector — the harness filters its task list on this string, so it must match exactly.
predicate yes Import string deciding success, e.g. refractal.predicates:from_benchmark to let the benchmark decide.
predicate_args no {} Passed to the predicate.
phases no [] Sub-goals, for tasks scored in stages. Each has a name.
max_steps no 400 The step budget, and it is identity: two budgets are two experiments. A failure at the cap is a timeout, not an inability — say which you intended.
provider_ref no {} How the provider identifies this task, e.g. {task_id: 3}.
success no None Metrics to a verdict. Absent means the provider's own boolean decides, which is right for a wrapped suite and wrong where success is a degree. Hashed as a unit into task_hash: the thresholds and the import strings of the extractors it names. See below.
description no "" Free text. Not hashed.

scenarios.yaml

Scenarios are generated, not enumerated: you describe a grid once and the description is what gets stored as provenance.

field required default what it does
id yes Names the set.
scene yes Which scene these scenarios vary.
generator yes Import string, e.g. refractal.generators:linspace_grid or :latin_hypercube.
generator_seed yes Makes a sampling generator reproducible.
params yes Map of parameter name to a spec — see below.
filter no None A predicate dropping generated scenarios. Evaluated by refractal build, which needs the engine.
tasks no None Restrict this set to named tasks. None means every task on the scene.
perturbations no [] Timed changes applied during the episode. Hashed into the scenario, so a perturbed episode is a different experiment from its unperturbed counterpart. See below, and perturbations.

perturbations[]

field required default what it does
type yes Which effect to run, e.g. scale_actuator, drop_observation.
target no None What it acts on — a name the adapter resolves for a world effect, a field of the observation for a transform.
at_step yes When it fires, counted from the episode's first step. 0 for anything measuring robustness at a level: an episode that ends before its trigger never experienced the perturbation, and that bias favours whichever checkpoint finishes sooner.
until_step no None When it ends. Absent means it lasts to the end of the episode. Not every effect can carry one — an effect with no inverse is refused rather than given semantics that only hold when nothing else is happening.
args no {} That effect's parameters, e.g. {factor: 0.1}.

params[] — the parameter spec

Which fields you use depends on the generator. A grid takes range + steps; a sampler takes range + samples + distribution.

field default what it does
range None [low, high].
steps None How many evenly spaced points across range. Grid generators.
choices None An explicit list of values.
value None A constant — held fixed while other parameters vary.
samples None How many points to draw. Sampling generators.
distribution None uniform or normal. How samples are drawn from range.
mean None Centre, for normal.
std None Spread, for normal.

run.yaml

field required default what it does
checkpoints yes What is being compared. Two or more — one checkpoint is not a comparison.
results_uri yes Where rows are written. A local path or an object-store URI.
seeds no 3 Replicates per scenario. With a stochastic policy these are genuine replicates, not a way to reproduce a run.
seed_base no 0 First seed value; seeds run seed_base … seed_base + seeds - 1. Part of plan_id.
tier no full Which tier of the catalog to run.
scenario_sets no None Restrict the run to named sets. None means all of them.
execution_mode no serial Within one task, serial runs every seed for checkpoint A, then for checkpoint B, then moves to the next task. concurrent runs the checkpoints together, one thread per checkpoint. This is about checkpoints within a worker, not about whether workers are parallel. See below.

What serial actually orders

The loop is task-outer, checkpoint-inner, so a three-checkpoint run reads pi0 pi05 starvla pi0 pi05 starvla … — one block per task, measured at 30 blocks across 30 tasks. No two checkpoints ever step at the same instant (0 overlapping invocations in 124), which is the guarantee serial makes.

An earlier version of this table said it "runs every task for checkpoint A, then for checkpoint B — one policy resident at a time". Both halves were wrong. Checkpoint is the inner loop, not the outer one; and because the round-robin returns to each checkpoint every task, every model server stays up for the whole run. Nothing is unloaded between blocks, and a reader planning GPU memory on the promise of one resident policy would under-provision by the size of the other servers.

The ordering is worth keeping deliberately rather than treating as an implementation detail: interleaving the checkpoints per task means any drift in machine state — thermal, cache, a noisy neighbour — lands on all arms alike. Running every task for A and then every task for B would confound the checkpoint with the hour it ran in.

Workers are a separate question, and whether they run in parallel is the backend's answer, not the catalog's. The same table previously asserted workers "always are" parallel. They are independent units, which is what makes parallelism possible — but local runs them one after another in a single process, measured here as two workers occupying 0→4788s and 4789→7291s with no overlap. compose and k8s give each worker its own container and do run them concurrently. That difference is placement, so it moves the wall clock and not plan_id — which is why the makespan line reports work and wall clock as two numbers rather than one.

checkpoints[]

field required default what it does
id yes Names the arm. Used by --server id=URL at run time.
path yes The checkpoint itself — a hub id or a path.
server yes Import string of the model-server class.
server_args no {} Passed to that class. In plan_id, and they change what the policy does: chunk_size, state_key, a normalisation key. One set of weights with two server_args is two checkpoints, correctly.
vram_mb no 8192 What this server reserves on its device. A fixed cost, however many workers connect — and it comes out of the pool per-worker environments draw on.

No server address appears in the catalog. Where a server listens is placement, supplied at run time as --server id=ws://host:port, and plan_id cannot see it. Which server and how it is configured is identity; where it is running is not.


hardware.yaml

Not hashed into anything. The same experiment on a different machine is the same experiment.

field required default what it does
id yes Profile name, passed as --hardware.
cpu_cores yes Cores available to workers.
memory_mb yes Host RAM available.
devices no [] GPUs — {id, vram_mb} each.
max_workers no None A ceiling on workers regardless of what fits.

devices[]

field required what it does
id yes e.g. cuda:0.
vram_mb yes Allocatable, not the card's total. nvidia-smi reports a nameplate figure of which a few hundred MiB is driver and display context no allocation can reach. Declaring the nameplate hands the planner memory it cannot give out, and that margin decides worker counts.

What invalidates what

change this and this moves
a scene's model, assets, external.params scene_hash, every episode on it
a task's instruction, predicate, max_steps task_hash, every episode of it
a scenario's generated values or perturbations scenario_hash
checkpoints, seeds, seed_base, tier, execution_mode plan_id
resource_shape, hardware.yaml, results_uri, the backend nothing — placement, not identity

scenes[].metrics[] — what to measure

An extractor turns live simulator state into named numbers. They land in the metrics column of episodes.parquet as a map<string, double>.

field required default what it does
extractor yes Import string of the callable, e.g. your_adapter.metrics:route_completion. Refractal ships none: an extractor reads live simulator state, so it belongs in the adapter that owns the simulator.
produces yes The metric names it writes. Declared, not discovered: refractal plan runs with no simulator and cannot import the extractor to ask, and the refusal below has to be answerable from the catalog alone. refractal build checks the declaration where the extractor is importable.
args no {} Passed to the extractor. Not name-validated, like every other pass-through.

On the scene because an extractor reads simulator state, and the scene is what owns a simulator.

tasks[].success — metrics to a verdict

success:
  all_of:
    - {metric: route_completion, at_least: 0.95}
    - {metric: collisions, at_most: 0}
field required what it does
all_of yes Every threshold must hold. A conjunction is the whole language.

Each threshold:

field required what it does
metric yes A name some extractor on this task's scene produces.
at_least one of Passes when the metric is greater than or equal to this.
at_most one of Passes when the metric is less than or equal to this.

Exactly one bound per entry. {at_least: 0.9, at_most: 0.8} is a range that can never hold and there is no reading of it that is not a mistake. A band is two entries.

A missing metric fails. Absence is not evidence of success.

Applied where the row is written, never at read time. Two consequences, and the second is the reason:

compare is untouched. It reads success exactly as before and never sees a metric, so McNemar, Holm, the clustered bootstrap and the k-checkpoint outcome patterns all work unchanged.

And re-scoring an old run under a new rule is impossible by construction. Editing 0.95 to 0.90 moves task_hash, so it is a different experiment. Computing the verdict at read time would have let that edit silently re-score episodes already recorded, which is what the identity model exists to prevent.

Not the same as --dichotomy. That collapses several seeds of one scenario into the bit McNemar consumes, and is scenario-level. This collapses metrics into the verdict for one episode. Two collapses at two levels.

Refused at plan time: a metric no extractor on the task's scene declares in produces. Pure function of the catalog, checked at the cheapest point that can see it.