Struct Pbt
pub struct Pbt { /* private fields */ }Expand description
Population-based training: exploit-and-explore at a fixed interval — the guide’s population-based training runs one end to end.
A population of trials trains in parallel; at a fixed interval (in the
study’s ResourceUnit) each trial that reports
is ranked against the population, and an underperformer (in the worst
bottom_fraction) is told to exploit and
explore:
- exploit — copy a randomly chosen top performer (from the best
top_fraction): its parameters and its checkpoint reference. That is aDecision::Forkwhoseparentis the top trial, so the child resumes from the parent’s checkpoint; - explore — perturb the exploited scalar hyperparameters (a learning
rate, an entropy coefficient) by a documented factor
(
Perturb); these become the forkmutation. Shape parameters (categorical/boolean) are never perturbed — they change the model’s architecture and the parent checkpoint would not load into a differently-shaped model, which is why the loop refuses a shape mutation.
§Frozen parameters — the scalar shape trap
“Shape parameter” and “categorical” are not the same set, and conflating
them is a live footgun. The skip rule above is typed: Cat/Bool are never
perturbed. But a network width is usually declared as an integer
(int(64, 512)), and PBT perturbs integers — so a fork would hand the child
a different architecture and the parent checkpoint would refuse to load into
it (oniro fails the resume with checkpoint load incomplete). The loop’s
scalar-only guard does not catch
this: the mutation is scalar.
with_frozen is the fix — an explicit list of parameter
names PBT must never perturb:
use atune_core::scheduler::Pbt;
// `sac.hidden` is an Int, so it would otherwise be perturbed; freezing it
// makes every fork child inherit the parent's width verbatim.
let pbt = Pbt::try_new(1_000)?.with_frozen(["sac.hidden"]);
assert_eq!(pbt.frozen().collect::<Vec<_>>(), ["sac.hidden"]);Freezing is deliberately not the same as removing the parameter from the search space: the sampler still tunes it across trials, which is sound (one trial’s segments all share one configuration); only perturbing it on a fork, where a checkpoint crosses the boundary, is not.
A trial above the exploit quantile just continues. PBT thereby learns a hyperparameter schedule: a lineage can carry one learning rate early and, through a later exploit-and-explore, inherit and perturb it to another — an adaptation no single fixed value achieves.
§The checkpoint contract the objective must honour
Exploit is only real if there is a checkpoint to copy. A PBT objective is
therefore expected to
record_checkpoint at each
interval (an oniro run-dir path, an artifact id — atune stores the
reference, never the bytes). A forked child then
receives that reference through
parent_checkpoint — for a
Subprocess child, as the
ENV_CHECKPOINT environment variable — and is
expected to warm-restart from it. If an objective records no checkpoint,
exploit degrades gracefully to copy the parameters only (a cold start with
the winning configuration); nothing panics.
§The execution model, and one seam limitation reported honestly
PBT works against today’s oniro through the chained
run_training(steps = interval) → run_training_resume model: a trial
trains toward the interval,
record_checkpoints and
reports there; PBT ranks it; the child
it forks resumes the winner. The population at a decision is every trial
that has reported at that step and is still rankable — running and paused
trials and finished ones alike (see
pruned members) — so it
accumulates over the study rather than being capped by
parallelism(N); a
single-threaded run forks once enough trials have crossed the same
aligned step. Parallelism decides how much of the population is alive at
once, not how large it is.
The one limitation. The frozen Decision cannot both stop the
reporting underperformer and fork a replacement in a single answer: a
Fork deliberately does not stop the trial that
emitted it — it spawns a sibling. So atune’s PBT
does not overwrite an underperformer in place the way the canonical
algorithm does; it adds the exploit child and lets the underperformer run out
its own segment. Over a run the exploit children dominate the population (they
carry the winning configurations forward), so the search is sound, but a true
in-place replacement would want a small additive seam — a companion
Command that stops the parent, emitted
alongside the fork. This is reported, not silently worked around.
§Pruned members and small populations
A trial occupies the population by its state:
Running,
Paused,
Complete and
Pruned trials rank at every step they
reported, and Failed and
Waiting trials never do. A pruned
trial therefore stays eligible, because pruning ends its training, not the
measurements it already reported: when it is among the best at a step it can
be the donor whose parameters and checkpoint an exploit copies.
Both quantiles count whole trials, rounded down, over the population at the
step with the reporter included: of n trials, the worst
floor(bottom_fraction · n) may exploit and the best
floor(top_fraction · n) may be copied
(with_bottom_fraction,
with_top_fraction). While either count is zero
every report continues, so a baseline forms before
anything is copied. With the default fractions of 0.2, a population of four
or fewer never exploits, and the first exploit at a step needs five trials
to have reported there. A nonzero fraction f names nobody until at least
1/f trials have; a zero fraction names nobody at all, so no report
exploits.
§Single-seed vs multi-seed
This is single-seed PBT: it works directly on the
Fork/checkpoint seam, where each trial is one evaluation
and streams a value per report. A
fanned population receives deterministic aligned aggregates only after all
replicates finish a fan. PBT can therefore fork at aggregate report
intervals, but cannot make a per-replicate decision. With
fan_sync_steps at its
interval boundaries it receives each boundary’s aggregate before any
replicate trains past it, so it can fork there; the child resolves the
parent’s per-replicate checkpoints when it is claimed, as a single-seed
child does.
§Determinism
Single-worker PBT with a fixed seed is reproducible: the population a
decision reads is the ask-time StudyView, the exploited top performer is
chosen by an RNG seeded from the reporting trial’s derived seed and the step
(never ambient entropy), and the perturbation draws from the same RNG. Under
parallelism the population at each
interval depends on completion order, so — exactly like every
history-dependent scheduler (where determinism
stops) — a PBT study is
replayable, not pre-determined. The genealogy is fully reconstructable
from storage (parent links + the recorded parameters, read through
Study::lineage), so PBT keeps no persistent
scheduler state — state is None, like ASHA’s rung
tables are recomputed from the view.
use atune_core::scheduler::Pbt;
// Decide every 10 resource units; Jaderberg defaults (bottom/top 20%,
// perturb by 0.8 / 1.2).
let pbt = Pbt::try_new(10)?;
assert_eq!(pbt.interval(), 10);Implementations§
§impl Pbt
impl Pbt
pub fn try_new(interval: u64) -> Result<Self>
pub fn try_new(interval: u64) -> Result<Self>
Builds a PBT scheduler that decides every interval resource units, with
the Jaderberg defaults (DEFAULT_BOTTOM_FRACTION,
DEFAULT_TOP_FRACTION, Perturb::default).
§Errors
Error::InvalidSpace if interval is 0 — an interval of zero would
gate every trial before it has run, exactly as a rung at step 0 does
(RungLadder::try_new).
pub fn with_bottom_fraction(self, fraction: f64) -> Self
pub fn with_bottom_fraction(self, fraction: f64) -> Self
Sets the exploit quantile — the worst fraction of the population
exploit (finite values clamped to [0, 1]; 0 disables exploiting).
A non-finite value is retained and rejected when a report uses the
scheduler.
pub fn with_top_fraction(self, fraction: f64) -> Self
pub fn with_top_fraction(self, fraction: f64) -> Self
Sets the top quantile — an exploit copies a random member of the best
fraction (finite values clamped to [0, 1]; 0 disables exploiting).
A non-finite value is retained and rejected when a report uses the
scheduler.
pub const fn with_perturb(self, perturb: Perturb) -> Self
pub const fn with_perturb(self, perturb: Perturb) -> Self
Sets the explore perturbation (default Perturb::default).
pub fn with_frozen<I, S>(self, names: I) -> Self
pub fn with_frozen<I, S>(self, names: I) -> Self
Names parameters the explore step must never perturb, so a fork child inherits the parent’s value for them verbatim.
This is the escape hatch for a scalar shape parameter — a network
width declared as int(64, 512), a layer count — which the typed
Cat/Bool skip rule cannot recognise and which makes the parent
checkpoint unloadable in the child
(see the type docs). Names
are matched exactly against the parameter names the trial recorded; a name
that no trial carries is simply inert.
Calling it twice adds to the set rather than replacing it, so a caller may combine a hand-written name with a list a helper computed.
§Determinism
A frozen parameter is skipped before its perturbation draw, so freezing one shifts the (still fully deterministic, still seed-derived) draw sequence of the parameters after it. The empty default therefore leaves every pre-existing study byte-identical.
pub fn frozen(&self) -> impl Iterator<Item = &str>
pub fn frozen(&self) -> impl Iterator<Item = &str>
The parameter names the explore step never perturbs, in sorted order.
pub const fn with_objective(self, obj: usize) -> Self
pub const fn with_objective(self, obj: usize) -> Self
Selects which objective dimension PBT ranks by (default 0).
pub const fn with_horizon(self, final_step: u64) -> Self
pub const fn with_horizon(self, final_step: u64) -> Self
Declares final_step, the resource step at which every trial ends.
A report at or past it is terminal: the study records it like any
other report, and other members still rank against its value, but the
reporter is never forked — a child forked there would have nothing left
to train. That lets an objective report its final segment instead of
leaving it unreported to avoid a useless fork. Without a horizon (the
default) PBT cannot tell a final report from an interior one and treats
every interval boundary alike. A final_step of 0 is rejected when a
report uses the scheduler.
pub const fn with_tie_break(self, ties: TieBreak) -> Self
pub const fn with_tie_break(self, ties: TieBreak) -> Self
Sets how a reporter ranks against members tied with its value (default
TieBreak::Strict).
pub const fn horizon(&self) -> Option<u64>
pub const fn horizon(&self) -> Option<u64>
The declared final report step, if any (see
with_horizon).
pub const fn tie_break(&self) -> TieBreak
pub const fn tie_break(&self) -> TieBreak
The tie policy (see with_tie_break).
pub const fn with_restart(self, mode: RestartMode) -> Self
pub const fn with_restart(self, mode: RestartMode) -> Self
Makes every exploit a Restart in mode instead
of a Fork (default: fork).
The donor and the explore mutation are chosen exactly as for a fork, so
the seeded choice of who exploits whom does not change; only what the
child does with the donor’s state does. A
RestartMode::Fresh child re-initializes with the donor’s mutated
configuration and no checkpoint; a RestartMode::ShrinkPerturb child
loads the donor’s checkpoint and shrinks and perturbs it. An invalid
mode is rejected when a report uses the scheduler.
pub const fn restart(&self) -> Option<RestartMode>
pub const fn restart(&self) -> Option<RestartMode>
The restart an exploit emits, or None for a fork (see
with_restart).
pub const fn interval(&self) -> u64
pub const fn interval(&self) -> u64
The interval, in the study’s resource unit, at which PBT decides.
pub const fn bottom_fraction(&self) -> f64
pub const fn bottom_fraction(&self) -> f64
The exploit quantile.
pub const fn top_fraction(&self) -> f64
pub const fn top_fraction(&self) -> f64
The top quantile.
Trait Implementations§
§impl Scheduler for Pbt
impl Scheduler for Pbt
§fn on_report(
&self,
study: &StudyView,
trial: &TrialMeta,
step: u64,
values: &[f64],
) -> Result<Decision>
fn on_report( &self, study: &StudyView, trial: &TrialMeta, step: u64, values: &[f64], ) -> Result<Decision>
Exploits-and-explores at each interval; see the type docs.
§Errors
Error::InvalidSpace if a
configuration knob or selected objective is invalid, or if an explored
scalar cannot represent its requested perturbation.
§fn state(&self) -> Result<Option<SchedulerState>>
fn state(&self) -> Result<Option<SchedulerState>>
None: PBT’s genealogy lives in storage (parent links + checkpoints), so
the scheduler recomputes every decision from the StudyView and persists
nothing — see the type docs.