Skip to main content
An optimizer proposes candidates (instruction or demo overlays), evaluates them with your metric on your trainset, and keeps the best. The convenience entry point is each optimizer’s compile_module method: it takes a module, a training set, and a metric, then searches for better instructions, and in some cases demos, for each Predict leaf. After it returns, the winner is installed and calling the module produces better results with no code changes.
The module must declare its optimizable leaves via Predictors (one predictors! line). All six optimizers are thin strategies over the shared evaluation engine. Candidates are data, never mutation: each candidate is a name-keyed Candidate injected ambiently per rollout (fx::with_params) — nothing touches the module during evaluation, so different candidates evaluate concurrently — and the winner is installed exactly once at the end (OptimizeTarget::install). The engine types (Engine, Candidate, Budget, Spend, ParetoView) are documented in Optimizer engine. Five strategies tune parameter values through overlays; the sixth, Structural, proposes graph edits over the edit calculus and runs on the program lane only. Step-by-step how-to pages: COPRO, MIPROv2, GEPA, and Structural.

The Optimizer trait

The trait is object-safe by design: optimizers compose (Box<dyn Optimizer> pipelines can share one Engine — one budget, one rollout cache, one score matrix — across stages). The target carries the thing under optimization and its example set by reference; the engine carries the spend. OptimizeTarget is the lane-erased pair of (thing under optimization, evaluation harness), one of two lanes:
  • OptimizeTarget::module(&mut module, &trainset, &metric) — a typed Module (+ Predictors discovery), a trainset slice, and a TypedMetric. The trainset is &[E] for any row type that projects into the module’s input via ToInput; the Serialize bound feeds rollout-cache uids, which content-hash the whole row. OptimizeTarget::module_with_valset(...) adds an optional validation set (the layout GEPA’s Pareto bookkeeping uses). Construction runs the naming pass: every declared leaf is stamped with its declared name, so trace spans, candidate entries, and persistence all address the same names.
  • OptimizeTarget::program(&interp, &examples, &metric) — an interpreter-loaded IR Program, labeled DemoRow examples, and a JSON-native ProgramMetric. The winner is retrievable as an ir::Overlay (OptimizeTarget::winner_overlay) for Program::bake.
For the common case you never build these by hand — each optimizer’s compile_module(&mut module, &trainset, &metric) inherent method constructs a module target and a default engine, runs compile, and installs the winner. compile returns an error when the target has no optimizable leaves, when a metric evaluation fails, or when an LM call fails during candidate evaluation. compile returns the Report enum (Report::None, Report::Gepa(GEPAResult), Report::Simba(SimbaReport), Report::Bootstrap(BootstrapReport), and Report::Custom(serde_json::Value) as the third-party extension point), with into_gepa()/into_simba()/into_bootstrap() accessors. The typed compile_module sugar unwraps it: Structural is the exception to this table and to the trait: it edits program structure, so its input is an interpreter-loaded program rather than a lane-erased target, and its entry point is compile_program instead of compile_module/compile.

Choosing an optimizer

GEPA is the only optimizer that requires textual feedback from the metric. The others use numerical scores alone.

COPRO

Breadth-first instruction search: generates breadth candidate instructions per predictor, evaluates each on the trainset, installs the best, and repeats for depth rounds. The base instruction always competes in every round. How-to: COPRO. Cost: approximately breadth × depth × num_predictors × trainset_size LM calls, minus rollout-cache hits. Runs with an unlimited engine budget.

MIPROv2

Trace-guided instruction and demo optimizer. Four phases: one traced teacher pass over the trainset; demo bootstrapping from successful spans via the trace name-join; generation of num_candidates instruction variants per predictor seeded by prompting tips; evaluation of up to num_trials candidates per predictor on one sampled minibatch, keeping the best. Demos are installed before instruction search so candidates are scored against the module as it will actually run. How-to: MIPROv2. Public helper type: PromptingTips (the rotation of prompting best practices appended to candidates, default_tips() and format_for_prompt()). compile_module returns (); through the trait, the report is Report::None.

GEPA

Genetic-Pareto instruction evolution driven by per-example feedback. Each generation samples a parent proportional to Pareto coverage, re-evaluates it on a trainset minibatch, has a reflection LM (prompt_model) rewrite the instruction from the feedback and the mutated component’s execution trace, then scores the child on the validation columns. Without a prompt_model, mutation degrades to deterministic feedback concatenation. Only instruction space is searched: no demo mutation, no crossover. How-to: GEPA.
GEPA errors if any Eval from the metric has feedback: None. Build metrics with Eval::with_feedback.
GEPA additionally exposes compile_module_with_valset(module, trainset, valset, metric) — sugar over OptimizeTarget::module_with_valset plus the Optimizer trait. With Some(valset), initial evaluation and child scoring use the validation set while parent re-evaluation uses trainset minibatches; with None, the trainset serves both roles (this is what compile_module does).

GEPAResult

GEPACandidate carries id, instruction, module_name, example_scores: Vec<f32>, parent_id: Option<usize>, and generation, plus average_score() and mutate(new_instruction, generation).

SIMBA

Stochastic Introspective Mini-Batch Ascent, the cheap agentic default. SIMBA keeps one current program as a candidate overlay and hill-climbs. Each step: sample a seeded trainset minibatch; pick the best and worst rollout of the current program on it (served from engine bookkeeping, no extra rollouts); propose exactly one move; accept it through the engine’s minibatch gate. A child is promoted to a full-trainset evaluation only when its minibatch mean strictly beats the current program’s, so rejected moves never pay for a full pass. The winner is installed when compile returns. The two moves, in order of preference: Cost: trainset_size for the baseline pass, then minibatch_size rollouts per step, plus the remaining trainset_size - minibatch_size only on promotion and one reflection call per rule move. The run stops cleanly when the budget no longer fits a step.

SimbaStep and SimbaReport

Each SimbaStep records one step: The SimbaReport summarizes the run:

BootstrapFewShot

The simplest complete optimizer: one teacher pass, one candidate, one comparison. It runs the module over the trainset under trace capture, harvests few-shot demos from successful spans of rollouts scoring at least min_demo_score, evaluates the demo candidate on the same engine (teacher rollouts already sit in the rollout cache, so the baseline never re-runs), and installs the demos only when the candidate’s mean beats the baseline.

BootstrapReport

adopted: false with a populated demos_per_predictor means demos were harvested but did not beat the baseline; the module is left unchanged.

Structural

LM-guided hill-climbing over the edit calculus, program lane only. Each generation gathers the legal_edits menu for every leaf, has a reflection LM (prompt_model) choose one edit from the serialized menu plus the incumbent’s evaluation feedback, applies it with Program::edited, carries the incumbent overlay across the change with migrate_overlay, loads the child through a caller-supplied RuntimeEnv factory, and accepts it through the engine’s minibatch gate: only a strict win on the shared minibatch promotes the child to a full-set evaluation and makes it the new incumbent. Edits that fail to apply, children that fail to load, and reflection replies that do not parse are recorded and skipped. How-to: Structural. Entry points: compile_program(&interp, &examples, &metric, env) and compile_program_with_overlay(&interp, Some(overlay), &examples, &metric, env), where env: Fn() -> RuntimeEnv supplies fresh bindings for each child load. The winner is returned in the report (program plus migrated overlay), never installed; bake it with Program::bake.

StructuralStep and StructuralReport

Each StructuralStep records one generation: The StructuralReport summarizes the run:

Demo harvesting

Demo harvesting is a pure name join over captured traces. A rollout trace records one span per Predict invocation under the leaf name the module declares via Predictors (stamped by the target’s naming pass), so successful spans (parsed output present) scoring at least the optimizer’s min_demo_score become flat demo rows for exactly the predictor that produced them: no pointer identity, identical behavior for fx and struct harnesses. Rows are gated and ranked by their effective score, deduplicated on input fields so repeated inputs do not crowd the demo set, and capped per predictor. BootstrapFewShot, MIPROv2, and SIMBA share this machinery; it is internal to the crate and not part of the public API. A span’s effective score is the whole-rollout metric score unless the metric attached a span-level eval through TypedMetric::evaluate_spans (see Evaluation), which then takes precedence in both directions: a span scored down stays out of the demo pool even when its rollout won, and a span scored up qualifies even when its rollout lost. Without span evals the behavior is exactly the whole-rollout join described above.

See also