Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Refinement — what ships

The design is tempered sequential Monte Carlo. What ships is a short local Metropolis–Hastings walk. This page is about the difference, because it is easy to overstate.

Design versus implementation

The design describes generation as sampling from πβpgrammareβE[uθ]\pi_\beta \propto p_{\text{grammar}} \cdot e^{\beta \E[u_\theta]} by tempered SMC with a crossover population kernel. That is an intention, not a description of the code.

What runs is a dozen-to-forty-step adaptive single-site MH walk warm-started from each of the best pool members, keeping the final state: local hill-climbing on that target, rather than a draw from it.

What runs

Engine::refine is three lines over two primitives:

pub fn refine<R: Rng>(&mut self, rng: &mut R) {
    for parent_id in self.refine_begin() {
        self.refine_seed(rng, parent_id);
    }
}

refine_begin advances the generation counter and returns the top refine_seeds candidates by posterior utility, best first. It returns empty (and does not advance the counter) when there is no posterior or no standardizer, because there is no direction to climb in.

refine_seed clones the seed's tree, walks refine_steps MH steps with no locks, and injects one state of that walk as a child. It returns None if the walk was rejected or landed on a tree the pool already holds.

Which state of the walk gets injected

A walk renders and featurizes ~40 candidates and injects one, and which one is a free choice that had never been measured. SessionConfig::refine_keep makes it selectable so the comparison stays runnable, the same rule the acquisition enum follows:

RefineKeep::Lastthe state the walk ended on — the default
RefineKeep::Bestthe highest-logπβ\log \pi_\beta state it occupied, seed included

The archive is free. Every trace the kernel returns already carries its own logπβ\log \pi_\beta, so Best is one f64 compare per step and no extra render.

It is scored on the target, not on fitness alone, and that is the load-bearing choice: taking the argmax of E[u]\E[u] would discard the parsimony half of the very distribution the walk is sampling, and would do it with a bias — a bigger term has more modules to score well with, so fitness-argmax systematically returns the largest tree the walk touched.

Under Best the seed is in the archive, so a walk that finds nothing better than where it started injects nothing, rather than whatever it happened to be standing on at step 40.

Why the default has not moved

Best ships switched off. Argmax over a surrogate is the classic way to find that surrogate's errors rather than the user's preferences, and the always-on gate has already caught this happening: over 16 seeds, two produced pools −12.0 and −5.5 worse in the synthetic user's true utility after three generations, because insert_candidate admits and evicts by the model. Turning Best on without measuring it is the move most likely to make that worse.

refine_from(seed_id, locked) is the same thing from an explicit seed with an explicit lock set, the ⚡ evolve from this path.

Injection displaces the pool's lowest-utility member; pinned candidates are exempt.

The split is measured

Defaults, both scaled from the palette's operator count N_OPS = 20:

refine_steps=2NOPS=40,refine_seeds=NOPS/2=10\text{refine_steps} = 2 \cdot N_{\text{OPS}} = 40, \qquad \text{refine_seeds} = \lceil N_{\text{OPS}} / 2 \rceil = 10

Riding N_OPS matters: a structural proposal picks a new operator from a categorical that grew from six to twenty kinds, so a fixed budget would spend the same number of proposals covering a far wider move set and land children in a visibly thinner slice of it. The tuning survives a palette change.

The 40 × 10 split was an argument that could have been wrong in either direction, so search_health --budget-ab was written to settle it. Over 8 seeds, 6 generations, graded against a synthetic user's true utility:

stepsseedsproposalsmean uumax uu
40104001.7148.154shipped
4031201.2416.178same depth, fewer seeds
6631980.7746.281same total, fewer seeds
20204000.5686.790half depth, double breadth

The shipped split wins on both metrics, and moving off it in either direction is worse.

Two rows are worth more than the headline.

Depth from few seeds is 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 full 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.

Why local climbing suits this anyway

The gap between design and implementation is real, but the implementation is not merely a shortcut.

A candidate pool is not a sample. The pool's job is to hold a few dozen patches worth auditioning. A correct sample from πβ\pi_\beta would include low-utility regions in proportion to their (small but nonzero) probability mass, which is right for estimating an expectation and wrong for filling a shortlist a person will listen to.

Warm-starting from the best members is deliberate. It concentrates effort where the model already believes, which is what "propose toward me" means from the user's side.

Diversity comes from elsewhere. The measured result below is that the pool does not concentrate over a session anyway, so the thing SMC would primarily buy (maintained diversity via a population kernel) is being supplied by frontier-biased injection plus worst-eviction.

What is lost: any claim about the distribution of the pool. That is the whole of it — the other thing this section used to claim was lost turns out not to be.

The islands are not separated by a valley

This page previously said that a user with two distant islands "may find that refinement from island A never discovers island B, and has to reach it by hand or by the prior". That was an argument from the shape of a local walk, and it is false.

make islands teaches a genuinely bimodal synthetic user — two islands opposed on every coordinate they share — runs real generations, and asks how often a child lands on the island its parent was not on:

refinement events that cross islands99 / 473 (20.9 %)
of those, decisive — both ends > 1.0 onto their island64 (13.5 % of all events)
seeds whose pool ended on one island only0 / 8

The decisive column is the one that matters. A patch sitting on the decision boundary can flip island under an arbitrarily small change, and counting that as crossing a valley would be measuring nothing; the filter removes it and the answer survives. The pool share is reported beside it as a control, because both islands being occupied would say only that the prior scattered candidates over both.

Why the argument was wrong. It reasoned about a local walk in feature space. This is a reversible-jump walk over a tree grammar: one accepted structural move swaps a subtree, and that is a large jump in φ\varphi. The search does not have to travel through the low-utility space between the islands, so there is no valley for a tempering schedule to cross.

Tempered SMC may still be worth having for the distributional claim. It is no longer worth having for this.

The measured non-concentration

From the same harness, as a manipulation check that turned into a finding.

Final pool spread, measured as mean pairwise Δφ\lVert \Delta \varphi \rVert on the reference scale, was 7.7–7.9 evolving versus 7.2 static. Six generations over a 72-duel session did not concentrate the pool at all; it widened it slightly, because mutation pushes children into feature-space extremes faster than eviction trims them.

That has two consequences, and one of them decided a default:

  1. The diversity argument for SMC is weaker than expected at session horizon.
  2. The concentrated regime that BALD was hypothesized to win in never arises, so the measured tie between BALD and uniform pairing is not an artifact of a spread pool that only the static setup guaranteed. The product's own dynamics keep the pool spread.

The screening cascade

φstruct\varphi_{\text{struct}} is free (no compile, no render) so a structure-only surrogate can prune candidates before the expensive path. Survivors get rendered and scored in full.

This is designed into the feature split and is why φ\varphi is two-part rather than one vector. In the refinement path specifically, the affordability comes primarily from the render memo rather than from screening, because the walk re-scores its own current state on every step.

Lineage

Every injected child records a LineageEvent:

pub struct LineageEvent {
    pub kind: String,          // "refine" | "edit"
    pub parent_id: u64,
    pub child_id: u64,
    pub diff: Vec<DiffEntry>,  // what changed, in trace-address terms
    pub parent_utility: f64,   // posterior mean at event time
    pub child_utility: f64,
}

tree_diff produces the address-level diff, which the app renders as attack 0.59→0.83, +noise, −distortion · Δtaste +0.65.

Utilities are recorded at event time: a later refit changes the model, and re-deriving these numbers afterwards would rewrite history to look better-informed than it was.

Hand edits appear in the same log tagged "edit", because the lineage is a record of everything that produced a patch and not only of what the machine did.