Skip to main content

Pbt

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 a Decision::Fork whose parent is 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 fork mutation. 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::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 occupies_population) — 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.

§Single-seed vs multi-seed

This is single-seed PBT: it works directly on the M4.0 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 or partially evaluated mid-fan decision.

§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::new(10)?;
assert_eq!(pbt.interval(), 10);

Implementations§

§

impl Pbt

pub fn 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::new).

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

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

Sets the explore perturbation (default Perturb::default).

pub fn with_frozen<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

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>

The parameter names the explore step never perturbs, in sorted order.

pub const fn with_objective(self, obj: usize) -> Self

Selects which objective dimension PBT ranks by (default 0).

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

The exploit quantile.

pub const fn top_fraction(&self) -> f64

The top quantile.

Trait Implementations§

§

impl Clone for Pbt

§

fn clone(&self) -> Pbt

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
§

impl Debug for Pbt

§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
§

impl PartialEq for Pbt

§

fn eq(&self, other: &Pbt) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
§

impl Scheduler for Pbt

§

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>>

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.

§

fn fan_report_mode(&self) -> FanReportMode

Declares whether this scheduler accepts aligned aggregate fan reports. Read more
§

fn scripted_capability(&self) -> ScriptedSchedulerCapability

Declares whether scripted ask/tell can recreate this scheduler. Read more
§

fn on_trial_end( &self, study: &StudyView, trial: &FrozenTrial, ) -> Result<Vec<Command>>

Called once a trial reaches a terminal state. Read more
§

fn resume_candidates(&self, study: &StudyView) -> Result<Vec<Command>>

Paused trials worth waking on a freed work slot, in priority order. Read more
§

fn restore_state(&self, blob: &SchedulerState) -> Result<()>

Restores a scheduler from a previously persisted blob. Read more
§

impl StructuralPartialEq for Pbt

Auto Trait Implementations§

§

impl Freeze for Pbt

§

impl RefUnwindSafe for Pbt

§

impl Send for Pbt

§

impl Sync for Pbt

§

impl Unpin for Pbt

§

impl UnsafeUnpin for Pbt

§

impl UnwindSafe for Pbt

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more