pub struct SessionConfig {Show 21 fields
pub pool_size: usize,
pub max_draws: usize,
pub refine_steps: usize,
pub refine_seeds: usize,
pub beta: f64,
pub refine_keep: RefineKeep,
pub k_styles: usize,
pub phrase: PhraseSpec,
pub render_policy: RenderPolicy,
pub audio_cache: usize,
pub mcmc_samples: usize,
pub mcmc_warmup: usize,
pub recency_half_life: Option<f64>,
pub proposal_tilt: f64,
pub duel_utility_weight: f64,
pub duel_repeat_penalty: f64,
pub duel_exposure_penalty: f64,
pub duel_temperature: f64,
pub duel_check_every: usize,
pub acquisition: Acquisition,
pub sis_between_fits: bool,
}Expand description
Engine configuration.
Fields§
§pool_size: usizeVetted candidates to maintain in the pool.
max_draws: usizeMaximum prior draws attempted per fill_pool (vet failures burn
attempts).
refine_steps: usizeMH refinement steps per seed (scaled up when locks waste proposals).
The default scales with N_OPS: a structural proposal picks a new
operator from a categorical that the v2 palette widened from six to
twenty, so a fixed budget would spend the same number of proposals
covering a far wider move set and land the pool’s children in a
visibly thinner slice of it.
§The split is measured, not reasoned
2·N_OPS steps from N_OPS/2 seeds was an argument, and the argument
could have been wrong in either direction. search_health --budget-ab
exists to settle it; over 8 seeds, 6 generations, graded against the
synthetic user’s true utility:
| steps | seeds | proposals | mean u | max u | |
|---|---|---|---|---|---|
| 40 | 10 | 400 | 1.714 | 8.154 | shipped |
| 40 | 3 | 120 | 1.241 | 6.178 | same depth, fewer seeds |
| 66 | 3 | 198 | 0.774 | 6.281 | same total, fewer seeds |
| 20 | 20 | 400 | 0.568 | 6.790 | half depth, double breadth |
The shipped split wins on both metrics, and it is a genuine optimum rather than the top of a slope: moving off it in either direction is worse. Two rows are worth more than the headline.
Depth from few seeds is actively harmful. 66×3 runs 65% more proposals than 40×3 and scores lower (0.774 against 1.241) — a long chain from a bad starting point converges confidently on somewhere you did not want to be, and the extra steps are what get it there.
Breadth is not free either. 20×20 spends the shipped budget and is
the worst row of the four. Twenty steps is not enough for a chain to
leave its seed, so the generation is twenty barely-moved copies of the
current top — which is also why it has the second-best max: it
preserves the frontier by never straying from it.
Re-run this before changing either number.
refine_seeds: usizeHow many top candidates to refine from. Also scaled with N_OPS —
more seeds is more starting points, which is what actually buys
coverage of a wider palette, whereas more steps per seed buys depth
around one. See SessionConfig::refine_steps for the measurement
that fixes the ratio between them.
beta: f64Boltzmann sharpness β of the refinement target.
refine_keep: RefineKeepWhich state of a refinement walk becomes the injected child.
k_styles: usizeMaximum style components in the taste mixture (max-of-linear-experts); the fitted K grows with evidence up to this cap.
K is also the fit’s dominant cost driver, because single-site MH
rebuilds the whole program every step and the site count is
d·K + n_sessions + 5 — at today’s d = 40, that is 46 at K = 1 and
206 at K = 5 (printed by fit_bench, so it moves with φ). Two
consequences, both measured by auracle-taste/examples/fit_bench.rs:
the fit is ~4× slower at the cap than at the first fit, and the step
budget is fixed, so a mature fit gets ~4× fewer sweeps per site than
an early one — growing K makes the fit both slower and statistically
thinner.
Open option, deliberately not taken here: cap this at 3 (sites
206 → 126, a ~1.6× mature-fit win at no engineering cost). It is left
open because unlike the address hoist and the budget cut it is not a
pure efficiency change — it removes model capacity, and capacity is
the whole point of the mixture (a user with four islands of taste
cannot be represented by three lenses). Take it only on evidence:
TastePosterior::style_share
reports what fraction of the pool each lens claims, and if lenses 4
and 5 sit near zero share across real sessions they are paying 54
sites per step for nothing. learn_synthetic --compare is the A/B.
phrase: PhraseSpecThe audition stimulus.
render_policy: RenderPolicyHow the pool retains audition audio.
audio_cache: usizeAudition buffers kept resident under RenderPolicy::Lazy, most
recently auditioned first. Sized for the current duel pair, the bench
subject, and enough recent history that stepping back through the bank
is free.
mcmc_samples: usizePost-warmup MH steps per posterior fit.
This is the one knob in this struct that buys wall time with statistics, so it is set from a measurement rather than a guess. Only 500 draws survive thinning at any budget, so the budget does not buy draws — it buys sweeps per site, and at K = 5 (206 sites) even 10 000 steps is only ~49 sweeps.
Recovery vs budget at the mature operating point (K = 5, n_obs = 100,
12 seeds, cargo run --release -p auracle-taste --example fit_bench -- sweep 12): held-out duel agreement with the noiseless ground-truth
ordering, and the cosine of the best lens against θ*.
| steps | held-out acc | best-lens cos | native fit |
|---|---|---|---|
| 30 000 | 0.767 | 0.724 | 1.79 s |
| 20 000 | 0.757 | 0.717 | 1.16 s |
| 10 000 | 0.746 | 0.686 | 0.60 s |
| 8 000 | 0.738 | 0.690 | 0.49 s |
| 6 000 | 0.737 | 0.653 | 0.38 s |
| 5 000 | 0.729 | 0.655 | 0.33 s |
| 3 000 | 0.713 | 0.599 | 0.20 s |
That curve is smooth, so it says where the trade stops paying. The
second instrument is the end-to-end M4 gate
(closed_loop_learns_synthetic_taste, which runs at exactly this
budget through the real render → vet → feature pipeline). One run of
it is a single draw — over the pool lottery, the duel answers and
the chain — so it is replicated over 13 seeds here (cargo run --release -p auracle-session --example closed_loop_sweep). Its
pool/truth correlation r against the 0.6 gate, plus the other two
metrics the test asserts:
| steps | mean r | min r | seeds with r ≤ 0.6 | mean top-5 | mean cos |
|---|---|---|---|---|---|
| 30 000 | 0.736 | 0.575 | 1/13 | 3.14 | 0.528 |
| 20 000 | 0.722 | 0.576 | 2/13 | 2.88 | 0.497 |
| 10 000 | 0.726 | 0.551 | 2/13 | 2.78 | 0.475 |
| 8 000 | 0.715 | 0.503 | 1/13 | 3.07 | 0.456 |
| 6 000 | 0.747 | 0.600 | 1/13 | 3.39 | 0.497 |
| 5 000 | 0.689 | 0.476 | 2/13 | 3.15 | 0.392 |
Read that as a noisy measurement, because it is one. Within a single
budget the seed-to-seed spread of r is sd ≈ 0.07–0.10 over a range
of ≈ 0.25; between budgets from 6 000 up the means sit in
0.715–0.747, i.e. inside one standard error (≈ 0.02) of each other —
and 6 000 posts the highest mean of the six, which is the plainest
sign that this instrument’s ranking of the upper budgets is noise.
From 6 000 to 30 000 it cannot tell them apart. Only 5 000
separates at all — lowest on mean r, on min r and on cos — and
even that gap to 30 000 (0.047) is barely over one standard error of
the difference.
So the argument for 10 000 is not that it passes where 5 000 fails.
Every budget here fails the 0.6 gate on some seed, including the old
30 000 (1 of 13), and 5 000 clears it on 11 of 13. The argument is:
10 000 is 3× cheaper than 30 000 and gives up 0.010 of mean r, which
is inside the noise; the fit_bench sweep above — 12 seeds on a
metric with far less variance — prices the same cut at 0.021 of
held-out accuracy and 0.038 of cos; and cutting further to 5 000 saves
only another 0.27 s per fit while costing 0.017 more held-out
accuracy, 0.031 more cos and 0.037 of mean r, the one budget both
instruments mark down. 10 000 is where the two instruments agree, not
where a threshold was crossed.
(An earlier revision of this table read the M4 gate at a single seed,
0xE05, and concluded that 5 000 “fails outright” at r = 0.565 while
10 000 held “the widest margin of any budget tried”. Both are
artifacts of that one draw: 0xE05 sits ~1.2 sd low at 5 000 and right
on the mean at 10 000. The per-seed numbers reproduce exactly — the
inference from one of them did not.)
The earlier 30 000 also predated the address hoist in
auracle_taste::model, which made every step ~1.7× cheaper on its
own; the two together take a mature fit from ~1.86 s to ~0.60 s
natively (~13 s → ~4 s in the browser).
mcmc_warmup: usizeWarmup (adaptation) steps per fit, held at ~30 % of
Self::mcmc_samples. Warmup only tunes the per-site proposal
scales; it produces no draws, so it is pure overhead beyond the point
the scales converge.
recency_half_life: Option<f64>Recency half-life for the taste likelihood, in observations
(None = no forgetting). Tastes drift; old votes should fade.
proposal_tilt: f64Strength of the taste→grammar proposal tilt (0 disables): structural
θ components multiply the grammar’s kind weights by
exp(η·θ) during refinement.
duel_utility_weight: f64λ in the duel objective: how much the pleasantness of a duel counts against its informativeness, applied to pool-standardized utility. The user’s enjoyment is a resource too — two mud patches are a cheap question and an expensive answer.
Keep it small. Information gain is bounded by ln 2 ≈ 0.693 nats, so
a λ near 0.3 lets the ±2σ enjoyment term swing the objective by ±0.6 —
as much as the entire information range — and the acquisition function
quietly reverts to “duel the two best patches”, which is the best-arm
behaviour BALD was adopted to escape. Measured on the synthetic user
(learn_synthetic --compare), λ = 0.3 cost 0.15 of pool-ranking
correlation against λ = 0; 0.1 leaves it a tie-breaker.
duel_repeat_penalty: f64γ in the duel objective: penalty per previous showing of the same pair. Without it the acquisition function re-asks its favourite question until the next refit.
duel_exposure_penalty: f64Penalty per previous appearance of either candidate, regardless of who it was paired against.
The pair penalty alone does not stop degeneracy, and the shipped app
proved it: over twelve consecutive duels one candidate appeared in
six. Every pairing #1 vs #7, #1 vs #15, #1 vs #22 is a distinct
pair and pays no pair penalty at all, while the enjoyment term keeps
nominating the highest-utility candidate. The user does not experience
“distinct pairs”; they experience hearing the same patch over and over.
This term is what makes the candidate budget finite.
duel_temperature: f64Softmax temperature over the duel objective, as a fraction of the objective’s own spread across the candidate pairs.
Scale-free for the same reason the enjoyment term is standardized: an
absolute temperature is a bet on how far apart the scores happen to
be. Shipped at an absolute 0.05 nats it was a bad bet — the objective
spans several tenths of a nat once the enjoyment term is in it, so
exp(ΔJ/T) ran to e¹⁰ and the “softmax” was an argmax with extra
steps. Expressed as a fraction of the observed SD, 0.6 means the same
softness whatever the spread.
duel_check_every: usizeShow one uniformly-random “check” duel every N duels. An information-seeking acquisition deliberately picks pairs near p = 0.5, so calibration measured on acquisition-chosen duels is selection-biased; these are the unbiased subsample.
Redundant under Acquisition::Random, where every duel is already
uniform and is tagged as a check — the setting is kept because it is
exactly what Acquisition::Bald would need, and because one in ten
was measured to be underpowered anyway (a few forecasts out of fifty
cannot fill a five-bin reliability diagram). 0 disables.
acquisition: AcquisitionWhich rule picks the next duel.
sis_between_fits: boolFold each new observation into the posterior weights by importance sampling between full refits. Off makes the posterior frozen between fits, which is what the A/B compares against.
Trait Implementations§
Source§impl Clone for SessionConfig
impl Clone for SessionConfig
Source§fn clone(&self) -> SessionConfig
fn clone(&self) -> SessionConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more