DSPy GEPA silently resumes a stale checkpoint from a shared log_dir after you change the metric: optimization returns the previous run's best program after 0 new rollouts. GEPA's log_dir checkpointing stores optimizer state (candidate pool, iteration counter, scores) keyed only by directory — nothing in the checkpoint encodes which metric produced those scores. If you reuse the same log_dir across runs with different metric functions (e.g. switching from symmetric accuracy to an asymmetric/weighted metric), GEPA loads the old run's final iteration, sees the budget as already consumed, and immediately returns the prior program. The result looks like a completed optimization but performed zero rollouts under the new objective, and all reported scores are from the old metric.
Treat GEPA checkpoint state as metric-specific: give every (metric, dataset-split) objective its own log_dir, never a shared/global one. Simplest robust layout is to nest the checkpoint dir under each run's unique output directory so a new objective can never inherit a foreign lineage:
# out_path is unique per run/objective
gepa_log_dir = Path(out_path) / "gepa_logs"
gepa_log_dir.mkdir(parents=True, exist_ok=True)
optimizer = dspy.GEPA(
metric=quality_metric,
auto=budget,
reflection_lm=reflection_lm,
log_dir=str(gepa_log_dir), # NOT a shared/global dir
)There is no error message — the failure is silent. Detection signs that you resumed a stale checkpoint: the run finishes near-instantly, reports 0 (or far fewer than budgeted) rollouts, and the returned program is byte-identical to a previous run's output. Checkpoint resume within the SAME objective remains safe and is worth keeping — it makes interrupted runs cheap to restart — the invariant is one checkpoint lineage per objective, not 'never resume'.