Getting started¶
Five commands, about a minute, and you end on a real comparison verdict. No GPU, no simulator and no checkpoints: the example runs against a built-in fake benchmark that produces outcomes directly, so the machinery is real and only the robot is not.
$ pip install refractal[execute,compare]
1. Make a catalog¶
$ mkdir demo && cd demo
$ refractal init .
wrote 7 file(s) under .
next: refractal plan catalog --hardware laptop
You now have catalog/ with five YAML files and a README.md. One scene, two
tasks on it, two scenario sets, two checkpoints called baseline and
candidate.
2. Compile it¶
$ refractal plan catalog --hardware laptop -o plan.json
cube-bowl-v1 30 scenarios 360 episodes 2 worker(s) <=37 min
------------------------------------------------------------------
360 episodes across 1 scene(s), 2 worker(s), at most 37 min
that is an upper bound: every episode is costed at its full step limit,
and episodes that succeed finish sooner.
tier=full seeds=[0, 1, 2] checkpoints=['baseline', 'candidate'] mode=serial
wrote plan.json (plan_schema 4, plan_id sha256:82b73f180a7a...)
plan is the compiler. It expands two scenario sets into 30 distinct scenarios,
crosses them with tasks, seeds and checkpoints to get 360 episodes, assigns each
one to a worker, and writes the result. It ran on a laptop with no simulator
installed, which is the point: you can see what a run will cost before spending
anything.
plan_id is the experiment's identity. It is a hash of everything that
decides whether two runs are comparable. Change a step budget or a predicate and
it moves; change the output directory or the worker layout and it does not.
The estimate above is an upper bound from the catalog's declared figures. Once
you have actually run something, --expect-from RESULTS prints an expected
duration learned from it alongside the bound, which is the more useful number:
$ refractal plan catalog --hardware laptop --expect-from ./results -o plan.json
...
360 episodes across 1 scene(s), 2 worker(s), at most 37 min
that is an upper bound: every episode is costed at its full step limit,
and episodes that succeed finish sooner.
expected 21 min, from 360 prior episode(s) of ['baseline', 'candidate']
Add --expect-plan PLAN_ID when the prior results are from a different
experiment than the one being planned. If nothing usable is found it says
no usable prior episodes; expected duration not computed and prints the bound
alone, rather than quietly falling back to it.
--hardware PROFILE |
which profile in hardware.yaml to plan against |
--expect-from DIR |
prior results to learn an expected duration from |
--expect-plan ID |
the plan_id of those results, if not this plan |
--pack-below-startup-sec N |
pack starved scenes sequentially when scene construction costs more than the parallelism saves; default 30 |
-v, --verbose |
add one line per worker under each scene: its cpuset, episode count, duration bound and envs per process |
refractal init . takes --force to overwrite files that are already there.
3. Run it¶
$ refractal run plan.json -o results --catalog catalog
session 09b49614 360 episode(s) written, 0 already done
next: refractal compare results sha256:82b73f180a7a...
The default backend simulates. --catalog copies the catalog into the results
directory so the run can explain itself later.
Run it again:
$ refractal run plan.json -o results --catalog catalog
session a2b57450 0 episode(s) written, 360 already done
Resume is by episode identity, not by counter. It read the episode ids already on disk and subtracted them. Interrupt a real run at any point and this is what happens.
4. Compare¶
$ refractal compare results $(python -c "import json;print(json.load(open('plan.json'))['plan_id'])")
Scenarios in all 2 checkpoints: 60
Statistics computed on the 60 scenarios in common.
cube-bowl-v1 / cube-in-bowl (30 scenarios)
rates: baseline: 65.6% candidate: 62.2% (baseline baseline)
candidate fails candidate succeeds
baseline fails 2 7
baseline succeeds 7 14
design effect on the paired difference: 1.53
(observed Var 0.20958 vs binomial 0.13704, 30 scenarios, 3.0 seeds each)
baseline: within-cell ICC 0.041, between-scenario Var 0.00626
candidate: within-cell ICC 0.284, between-scenario Var 0.04994
clustering is load-bearing here: an unclustered test would be
anti-conservative by roughly this factor in variance.
baseline -> candidate: -0.033 [-0.200, +0.122] p=0.7463 holm=0.7463
McNemar p=1.0000 -> no change
note: 30 scenarios is below 200, where the percentile interval measures
~7% false positives against a nominal 5%.
Reading it:
Scenarios in all 2 checkpoints: 60: the comparison is paired. Only scenarios both checkpoints attempted are used, and this line tells you how many survived. If it is far below what you planned, something did not run.- The 2×2 is scenarios, not episodes.
7and7are the scenarios that flipped, and they are what McNemar tests. A rate that moved with few flips is a uniform shift rather than a set of scenarios breaking. - The design effect is how much more variable the paired difference is than independent coin flips would be. Above ~1.25 and clustering matters: treating each episode as independent would understate the uncertainty.
- The interval is a clustered bootstrap. It resamples whole scenarios, not episodes, because five repeats of one scenario are not five independent facts.
-> no changeis the verdict.compareexits 0 for no regression, 1 for a regression, 2 when the question cannot be answered.
The exit code is the gate; the text is for the human deciding what to do about it.
5. Look at what was written¶
results/comparison_id=82b73f18…/
plan.json the experiment, copied in
catalog/ the source, copied in
render/<digest>.json what was supplied at RUN time
harness/<surface>.json digests of the code that ran
checkpoint=baseline/scene=cube-bowl-v1/episodes/part-….parquet
checkpoint=candidate/scene=cube-bowl-v1/episodes/part-….parquet
Partitioned by comparison, checkpoint and scene, so several workers write concurrently without coordinating and a later run can list what already exists. One row per episode, with the identity it was run under and the provenance of the process that ran it.
The four provenance entries answer different questions, and the split is the point:
plan.json |
what the experiment is — the identity comparison_id digests |
catalog/ |
where it came from, so the run can explain itself later |
render/ |
server addresses, backend, results URI — what was supplied when it ran |
harness/ |
per-file digests of the harness code that actually loaded |
render/ is recorded and deliberately not hashed. A server address is a
property of a machine, not of an experiment; folding it into plan_id would
make the same experiment on two machines two experiments — the same reason
results_uri is excluded, since otherwise writing to ./results and to s3://
produces ids that never join.
It is keyed by a digest of those settings and appended rather than overwritten, so a comparison resumed on a second machine keeps both. More than one entry means the runs did not agree about how they were deployed, which is what somebody needs to see before explaining a difference in the numbers.
What to change next¶
- Point it at a real simulator. You write an adapter (see the adapter contract and the worked example), and Refractal drives it.
- Change the catalog. Writing a catalog covers what goes in each file and which edits invalidate existing results.
- Run the checkpoints against real model servers with
--backend vla-eval, or one container per worker with--backend compose.
What the example is not¶
The fake benchmark produces outcomes from a hash of the episode id. They are deterministic, they have realistic within-scenario correlation, and they are not a robot. Every number above is real arithmetic over synthetic outcomes. The statistics are exercised, the conclusion is about nothing.