Run trials through spacedeck¶
atune::exec::SpacedeckExecutor evaluates each trial on another
machine by handing it to spacedeck's run command. The study, its storage and its sampler stay in the process that
drives them; only the objective travels. It is the supported way to spread an
atune study over a fleet of hosts that spacedeck manages.
The direction matters. atune is spacedeck's client: the driving process asks
spacedeck to run one command, and spacedeck places it, runs it and reports back.
The reverse — atune run workers on remote hosts joining a study that
atune serve exposes over atune:// — is a different topology with no worker
mode today, and would need its own design decision before it exists.
What one trial costs¶
Each trial is one synchronous spacedeck --json run --on <target> process
(with --pick and its flags when spacedeck picks the host). The
executor builds its argument vector, waits for spacedeck's spacedeck/v1 JSON
envelope, and requires exactly one host result in it. A study that runs trials
in parallel runs that many spacedeck processes at once; nothing is pooled or
kept warm between trials.
So every trial pays a fixed overhead before the objective starts: spacedeck's own start-up, its capacity reads of the candidate hosts, and the round trip to the chosen one. That is small next to a training job that runs for minutes or hours, and it can dominate an objective that finishes in seconds — batch such work into fewer, longer trials, or keep it on the subprocess executor.
Set it up¶
The executor lives in atune::exec behind the system feature. It is built from
three things:
| Input | What it is |
|---|---|
| The declared space | Every {name} placeholder in the command must be a parameter of it; the constructor refuses one that is not. |
| A target selector | spacedeck's --on argument, naming where the trial runs. It must be non-empty and contain no NUL byte. |
A atune::exec::CommandTemplate |
The program and its argument templates, exactly as for the subprocess executor. |
The command reaches spacedeck as an argument vector after --; the executor
adds no shell, and spacedeck quotes each argument for the remote shell, so a
sampled value arrives as one literal argument. Its run
method checks that every parameter it names is in the study's declared space,
then drives the study's ordinary optimize loop, so retries, budgets, parallelism
and resume behave as they do for any other objective.
spacedeck forwards only the environment variables its --env flags name. The
executor names every variable it sets: the trial handshake (ATUNE_STUDY and
ATUNE_TRIAL_NUMBER; ATUNE_TRIAL, ATUNE_TRIAL_WORKER and
ATUNE_TRIAL_EPOCH while the trial is owned by a live lease; ATUNE_SEED;
ATUNE_CHECKPOINT for a forked or resumed trial; the ATUNE_RESTART_*
variables for a restarted one; and ATUNE_EVALUATE and ATUNE_RECIPE in a
final evaluation stage) and every sampled parameter under the ATUNE_PARAM_
prefix, so each parameter name must form a valid variable name there. The
environment reference describes each one.
Which spacedeck binary¶
The executor runs spacedeck from PATH unless
SPACEDECK_CLI names another
executable. The variable is read once, when the executor is built;
SpacedeckExecutor::cli_path overrides it for that executor.
Timeouts¶
SpacedeckExecutor::timeout passes a per-host bound to spacedeck's --timeout,
in whole milliseconds rounded up, so spacedeck stops the remote command and
reports a timeout result itself; a bound under one millisecond is sent as
1ms, never as spacedeck's unbounded 0ms. The executor also keeps a local deadline of that timeout
plus one minute: if the spacedeck process has not answered by then, it is killed
and the trial fails. Without a timeout, or with a zero one (spacedeck's "no
bound"), the executor waits for spacedeck indefinitely.
On Unix the spacedeck process runs in its own process group, which is killed when it exits or overruns, so nothing it started outlives the trial on the driving host. Elsewhere only the spacedeck process itself is killed. The envelope it may print is bounded too: one host's retained output, JSON escaped, and no more.
Let spacedeck pick the host¶
SpacedeckExecutor::pick runs each trial on one host that spacedeck chooses
from the target set, through spacedeck's run --pick (its decision D-095),
instead of on every host the target names. The target stays the candidate set,
so @all considers every host in spacedeck's inventory. A
atune::exec::SpacedeckPick carries the optional constraints, each
passed as the run flag of the same name:
| Builder method | spacedeck flag | Effect |
|---|---|---|
window("Europe/Luxembourg,22:00,06:00") |
--window |
A daily window, TZ,HH:MM,HH:MM; outside it no host is admitted and none is contacted |
min_disk_avail("500G") |
--min-disk-avail |
The free disk a host needs where the command runs |
unattended_idle_hours(2) |
--unattended-idle-hours |
Excludes a host where another account's interactive session has been idle for less than that many hours; 0 excludes any such session |
prefer(SpacedeckPrefer::MostDisk) |
--prefer |
Which of equally free hosts to take: atune::exec::SpacedeckPrefer |
spacedeck checks the values and ranks the hosts it admits: those with the
fewest free slots first, then the preference among them (the most available
disk, the lowest one-minute load average, the coolest hottest GPU, or the
fewest leased slots), then the host name. It names the chosen host and every
excluded one on its stderr and in the envelope's placement;
the executor reads only the host result. pick itself refuses an empty window
or size. A pick that admits no host runs nothing: spacedeck refuses it with the
code no_eligible_host, and the trial fails with that code and spacedeck's
reason. spacedeck's run holds no slot, so trials picked at the same time can
choose the same host; lower the study's parallelism when they must not share
one.
How a result becomes a trial state¶
The executor reads the payload's exit status from the envelope, never spacedeck's own exit code, which belongs to spacedeck's namespace:
| Envelope | Trial |
|---|---|
Payload exited 0 |
Complete, with the objective parsed from the payload's standard output by the subprocess executor's rule — the last non-empty line, a number or JSON — including an optional checkpoint reference |
Payload exited 42 |
Pruned; unlike the subprocess executor, the code is not configurable here |
| Payload exited with any other code | Failed, with the remote stderr in the message |
| Payload reported no exit | Failed, with spacedeck's error, or its result status when it gives none, in the message |
A refusal, an unknown host, invalid JSON, a schema other than spacedeck/v1, a cut-off envelope, or other than one host result |
Failed |
DGX Spark hosts¶
On the NVIDIA GB10 (DGX Spark) hosts of this fleet, nvidia-smi reports GPU
memory as [N/A]. A check that decides a host is idle from free GPU memory
therefore sees nothing; judge idleness from GPU
utilization and the compute-apps list instead, before choosing a host as a
target.
What this page rests on¶
- The envelope (decision D92). The executor accepts only spacedeck's
spacedeck/v1envelope; a refusal, which carries acodeand anerrorbut noresults, fails the trial and names its code. Real output of spacedeck 0.2.0, recorded on 2026-10-19, is kept per schema version incrates/atune_core/tests/fixtures/xc-12-spacedeck-run-v1.json, and the executor's tests replay it, so a change to the envelope fails them before it reaches a study. - The downstream build. Oniro's
oniro_tuning, the atune consumer that compiles atune on these hosts whenever it trains there, passescargo check --lockedagainst this atune revision (2026-10-19, x86_64). On a GB10 node itself (aarch64, Rust 1.92.0, 2026-10-19), atune passescargo check --lockedwith thesystemandpb2featuresoniro_tuningenables, and its facade contract tests pass. This is checked by hand once per release candidate, because noaarch64CI runner exists.
Where to go next¶
| If you want to | Go to |
|---|---|
| Write the objective program the remote host runs | The subprocess contract |
| Run parallel workers against one study file instead | Resume and scale |
| Read every variable a trial receives | Environment reference |
| Read the executor's API, method by method | Rust API |