Module exec
Expand description
The subprocess executor: run any program, in any language, as a trial.
An InProcess objective is a Rust closure the study
loop calls on a thread. A Subprocess objective is a child process
the study loop launches, feeds a trial’s sampled parameters, and reads a
result back from (tune any
program). It is the any-language path
— the program can be a shell script, a Python trainer, a compiled binary —
and it is the path oniro drives today, because a process-global RNG cannot
share an address space with other trials.
The executor sits above ask/tell: for each trial it
asks the study for a context, suggests the declared parameters through it
(so trial n still gets the sampler’s decision for n, and nothing else),
spawns the program with those values, reads the objective from the child’s
output, and tells the result back. Because that is all it does, it is an
ordinary Objective and rides the existing loop — parallelism, retries,
budgets, panic isolation and resume are inherited unchanged, not
reimplemented (see Determinism and resume below).
This whole module lives behind the system feature: spawning a child is
operating-system code, absent on wasm32-unknown-unknown. It is a reusable
atune_core piece rather than a CLI-private helper, because oniro drives
the same executor.
use atune_core::exec::{CommandTemplate, Subprocess};
use atune_core::space::dsl;
use atune_core::study::{Budget, Study, StudyConfig};
use std::time::Duration;
// A search space, written in the string DSL.
let schema = dsl::parse_schema([
("lr", "loguniform(1e-4, 1e-1)"),
("opt", "choice(adam, sgd)"),
])?;
// `./train.sh --lr={lr} --opt {opt}` — argv-level substitution, no shell.
let template = CommandTemplate::new("./train.sh", ["--lr={lr}", "--opt", "{opt}"])?;
let executor = Subprocess::new(schema.clone(), template)?.timeout(Duration::from_secs(600));
let study = Study::builder()
.parallelism(8) // eight concurrent child processes
.budget(Budget::trials(200))
.create(StudyConfig::new("shell-tune").with_seed(1).with_space(schema))?;
executor.run(&study)?;§The command template and the environment handshake
Both channels are supported and may be used together (tune any program).
- Template.
CommandTemplateholds a program and argument templates in which{name}is replaced by the rendered value of the parametername—--lr={lr}or a bare{lr}. Substitution is argv-level: the value becomes one element of the child’s argument vector, or part of one, and is never re-parsed. See the trust model below. - Environment handshake. Every live-trial child is given
ENV_TRIAL(the trial id),ENV_TRIAL_WORKERandENV_TRIAL_EPOCH(the exact fenced owner operation),ENV_TRIAL_NUMBER(the per-study number), andENV_SEED(a per-trial objective seed), plusENV_STUDY. The complete identity is the handshake a scripted worker carries into a nestedatune ask/tell; a numericATUNE_TRIALalone is insufficient. Held-out reevaluation is deliberately read-only and omits the three mutation-authority variables while retaining number and seed. Withexport_paramseach sampled parameter is also exported as<prefix><name>(default prefixDEFAULT_PARAM_PREFIX), so a program that prefers to read its configuration from the environment can, without anatune askround trip. A forked or resumed trial is additionally givenENV_CHECKPOINT, the checkpoint reference to restore from (e.g. oniro’srun_training_resumerun-dir) — a reference hand-off, never bytes.
§The result protocol
The child reports its objective two ways at once, and both are total — neither can make the executor panic:
-
stdout carries the value on its last non-empty line, as a bare number (
0.37) or a JSON object with avalueorvaluesfield ({"values": [0.37, 12.0]}for a multi-objective study). -
exit status decides the trial’s fate:
Exit code Trial state 0Complete, with the value parsed from stdoutPRUNE_EXIT_CODE(42by default, configurable)Prunedany other non-zero, or a fatal signal Failed, with stderr kept in the recordA
0exit whose stdout does not carry a readable objective is a cleanFailed— a program that succeeded but printed garbage is a bug in the program, recorded as a failed trial, never a crash in the tuner.
A completed child may also carry a checkpoint reference on that same JSON
line — {"value": 0.37, "checkpoint": "runs/ppo/3", "step": 30000} — which
atune records (a reference, never bytes, 03 §5) so a later
Fork or resume can hand it back through
ENV_CHECKPOINT.
A timeout kills a child that overruns its deadline
and records the trial Failed.
§Trust and security model
The executor spawns a user-chosen program and substitutes sampled values
into its argument vector and environment. It does this without a shell:
std::process::Command receives an explicit argv, so there is no
/bin/sh -c, no word-splitting, no glob expansion, and no
shell-metacharacter injection surface. A sampled value of ; rm -rf /
is one literal argument string, not a command.
The trust boundary is therefore the same one the user already accepted when
they wrote the search space and named the program: the values substituted
are draws from the user’s own declared distributions — a float, an
integer, a categorical label, a boolean — not attacker-controlled input from
a network. The executor does not sandbox the child; a subprocess trial
runs with the same privileges, environment and filesystem access as the
process that launched it. Environment variables are set on top of the
inherited environment; parameter exports use a non-empty prefix by default
precisely so that a parameter named, say, PATH cannot clobber a meaningful
variable.
§Determinism and resume
The executor adds no randomness. The parameters a child receives for
trial number n are exactly what the sampler decided for n, obtained by
suggesting each declared parameter through the trial’s context — the same
priority chain
every objective walks. The
child’s own randomness comes from ENV_SEED, which is the trial’s
replicate_seed — outside a
multi-seed fan that is seed_for(study_seed, number, Stream::Objective), a
pure function of the trial number, so a trial reproduces regardless of
which worker ran it; under a SeedProtocol fan it is
replicate j’s seed, so the child of replicate j gets the
common-random-numbers seed.
Because a subprocess run is just Study::optimize_with over a
Subprocess objective, a study run this way is resumable exactly like
any other: point a fresh executor at a study
loaded from a journal and it continues
the numbering, re-derives the same seeds, and re-runs only what is left to
run.
Structs§
- Command
Template - A program plus its argument templates: the recipe for one child process.
- Subprocess
- Runs each trial as a child process, feeding it sampled parameters.
- Subprocess
Error - The error recorded for a trial whose child process failed.
Enums§
- Capped
Run - The captured result of
run_capped: the child exited, or it overran its deadline and was killed.
Constants§
- DEFAULT_
PARAM_ PREFIX - The default prefix for exported parameter environment variables.
- ENV_
CHECKPOINT - The environment variable carrying a checkpoint reference for the child to resume from.
- ENV_
SEED - The environment variable carrying the child’s per-trial objective seed.
- ENV_
STUDY - The environment variable naming the child’s study.
- ENV_
TRIAL - The environment variable naming the child’s trial (its storage-global id).
- ENV_
TRIAL_ EPOCH - The environment variable carrying the child’s lifecycle ownership epoch.
- ENV_
TRIAL_ NUMBER - The environment variable naming the child’s per-study trial number.
- ENV_
TRIAL_ WORKER - The environment variable carrying the child’s lifecycle owner id.
- MAX_
OUTPUT_ CAPTURE_ LIMIT - The largest
capture_limitan executor accepts. - PRUNE_
EXIT_ CODE - The exit code a child uses to say “prune this trial”, by default.
Functions§
- run_
capped - Runs
commandto completion under an optional wall-clocktimeout, capturing stdout and stderr, and never hanging past a bounded margin of the child’s exit no matter what it spawned. - run_
capped_ in_ parent_ group - Runs
commandunder an already trusted caller-owned Unix process group.