MIPROv2 starts after you have defined a program and the behavior you will accept. Given training examples, a validation set, and a metric, it searches over instructions and few-shot demonstrations supported by the program. The result is an optimized program artifact, not a prompt someone edited by hand.

Stanford’s MIPROv2 paper1 describes the method for multi-stage language-model programs. DSPy.rb implements the same broad phases with a Ruby optimizer and a different Bayesian search backend.

What MIPROv2 Compiles

A DSPy program may contain one predictor or several. Each predictor has parameters such as its instruction and demonstrations. MIPROv2 proposes candidate values for those parameters, runs the complete program, and scores the final behavior with your metric.

That separation matters in a multi-stage program. A locally plausible instruction can make the final result worse. MIPROv2 evaluates candidates through the program’s end-to-end objective rather than treating prompt wording as its own goal.

You still choose the objective. Weak examples and a metric that rewards the wrong behavior will produce a well-optimized mistake.

The Search In Five Phases

Summarize The Dataset

DSPy.rb’s grounded proposer can summarize the training examples and program structure before proposing instructions. The current API performs this inside MIPROv2; DatasetSummaryGenerator remains an internal collaborator rather than a public DSPy::Teleprompt object.

The summary gives the proposal model evidence about the task. It cannot guarantee that every proposal fits the domain, so candidates still have to survive evaluation.

Bootstrap Demonstrations

MIPROv2 builds candidate few-shot sets from labeled and bootstrapped examples. These settings control the size of that search surface:

optimizer = DSPy::Teleprompt::MIPROv2.new(metric: metric)
optimizer.configure do |config|
  config.num_instruction_candidates = 3
  config.bootstrap_sets = 2
  config.max_bootstrapped_examples = 2
  config.max_labeled_examples = 4
end

More candidates and demonstration sets increase cost. They widen the search; they do not guarantee a better result.

Propose Instructions

The grounded proposer uses the signature, program structure, dataset summary, examples, and previous trial information to generate instruction candidates. Developers still write task and field descriptions. MIPROv2 compiles those descriptions and the available evidence into candidate provider-facing instructions.

Search Candidate Combinations

The paper’s reference implementation uses Optuna’s TPE sampler. DSPy.rb’s :bayesian strategy encodes candidate combinations, fits a Gaussian Process, and selects candidates with an Upper Confidence Bound acquisition function. The :greedy and :adaptive strategies are also available.

The backends differ, and neither is universally better. Their behavior and cost depend on the candidate space and evaluation noise.

optimizer.configure do |config|
  config.optimization_strategy = :bayesian
  config.num_trials = 12
  config.minibatch_size = 10
end

When num_threads is greater than 1, minibatch_size sets the chunk size for parallel candidate evaluation. DSPy.rb evaluates every chunk and combines the results, so each candidate still sees the complete evaluation set. The setting changes scheduling while preserving the evidence and model calls used for an observation.

Evaluate The Complete Program

For programs that expose multiple predictors, MIPROv2 can apply separate instruction and demonstration candidates to each predictor. The metric still sees the program’s final prediction.

This also applies to a ReAct module that exposes several predictors. MIPROv2 can optimize supported predictor parameters; it leaves the program’s control flow and tool policy unchanged.

Install The Optional Gem

MIPROv2 ships separately so applications that do not use its numerical dependencies do not have to install them.

# Gemfile
gem "dspy"
gem "dspy-miprov2"
require "dspy"
require "dspy/miprov2"

Compile A Classifier

The signature declares the task. The metric defines acceptable behavior.

class SentimentClassifier < DSPy::Signature
  description "Classify the sentiment of customer feedback"

  input do
    const :text, String
  end

  output do
    const :sentiment, SentimentLabel
    const :confidence, Float
  end
end

program = DSPy::Predict.new(SentimentClassifier)

metric = lambda do |example, prediction|
  prediction.sentiment == example.expected_values[:sentiment]
end

optimizer = DSPy::Teleprompt::MIPROv2.new(metric: metric)
optimizer.configure do |config|
  config.auto_preset = DSPy::Teleprompt::AutoPreset::Medium
end

result = optimizer.compile(
  program,
  trainset: train,
  valset: validation
)

optimized_program = result.optimized_program
puts "Best validation score: #{result.best_score_value}"

Keep a test set outside the compile loop. The validation score selected the candidate. Estimating generalization requires independent data.

Presets Are Budgets, Not Quality Levels

Current DSPy.rb presets allocate candidate budgets and configure instruction candidates, bootstrap sets, example limits, search strategy, and early stopping.

Preset Candidate budget Instruction candidates Search strategy
light 6 3 greedy
medium 12 5 adaptive
heavy 18 8 Bayesian

For programs with multiple predictors, DSPy.rb derives the number of trials from the candidate budget and number of tunable variables. The budget is therefore more stable to document than a fixed trial count for every program.

Use the smallest preset that can answer the current question. A larger budget spends more model calls and explores more combinations. Production readiness depends on the held-out result and the application’s release criteria.

Inspect What Changed

MIPROv2Result exposes the optimized program, best score, evaluated candidates, and optimization trace.

puts result.best_score_value

result.optimization_trace[:trial_logs].each do |trial_id, trial|
  puts "Trial #{trial_id}: #{trial[:score]}"
  puts trial[:instruction]
end

The exact keys inside each serialized trial depend on the candidate and program shape. Inspect the trace your run produced before building reporting code around nested fields. DSPy.rb does not currently publish a metadata[:predictor_contributions] score that attributes improvement to individual predictors.

Evaluate optimized_program on the held-out test set and compare it with the baseline under the same metric. Persist the optimized program and enough run metadata to reproduce the decision: dataset version, model configuration, metric version, random seed, and budget.

What The Paper Establishes

The paper evaluates MIPROv2 across several language-model programs and tasks, comparing optimized programs with baselines under defined metrics.1 Those experiments motivate instruction and demonstration search. They do not promise a fixed improvement for a new Ruby application.

Your result depends on the task, model, examples, metric, candidate budget, and variance in model calls. Report the baseline, held-out score, cost, and run configuration rather than carrying a benchmark percentage into another domain.

When To Use It

MIPROv2 is a reasonable fit when:

  • You have enough representative examples to separate training, validation, and test data.
  • The metric reflects behavior you are willing to optimize.
  • The program has instructions or demonstrations worth compiling.
  • The expected gain can justify repeated model calls.

For a small single-predictor program, GEPA may provide a more direct reflective optimization loop. Choose between optimizers by their supported parameters, evidence requirements, and budget, then verify the compiled program on held-out data.

Further Reading

  1. Opsahl-Ong, Krista, et al. Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs. arXiv:2406.11695v2, 2024. Read the paper  2