Schema reference for search_history.json — the on-disk trajectory log of an AIPerf adaptive Bayesian-Optimization (BO) run. The file is produced by src/aiperf/exporters/search_history.py (write_search_history) and is rewritten in place after every BO iteration, so a partial trajectory survives a crash or cancellation. Each entry captures what the planner proposed, what the resulting benchmark measured, and (on terminal calls) why the loop stopped. For algorithm semantics see Bayesian Optimization.
search_history.json is the canonical artifact for post-run BO audit and dashboarding. It complements (it does not replace) sweep_aggregate/profile_export_aiperf_sweep.{json,csv}, which carries the post-hoc grouping of all iterations by variation_values. The trajectory log is unique in that it preserves iteration order and convergence-reason metadata.
Use it to:
len(config.objectives) > 1) best_trials is the Pareto front rather than a single argmax/argmin.The exporter writes to <base_dir>/search_history.json where base_dir is the controlling artifact directory. The companion sweep aggregate is under <base_dir>/sweep_aggregate/ for single-trial and independent multi-run layouts, and under <base_dir>/aggregate/sweep_aggregate/ for repeated multi-run layouts.
In-process (aiperf profile --search-space ...):
Top-Level Fields:
config SectionA snapshot of the adaptive-search configuration fields that the v1 writer persists from AdaptiveSearchSweep (src/aiperf/config/sweep/config.py). It includes the planner name, objectives, outcome constraints, iteration budget, initial-point count, random seed, convergence knobs, search-space dimensions, and SLA filters. It does not serialize every planner knob yet (for example optuna_sampler, optuna_acquisition, optuna_terminator, objective_pooling, and smooth-isotonic replicate/warmup settings are omitted), so use it as an audit trail for the trajectory rather than a complete round-trip config. The optimization target is recorded as a list under objectives (length-1 for single-objective runs, length-N for Pareto BO); outcome_constraints is the parallel list of feasibility gates that BoTorch’s acquisition masks against.
Fields:
search_space Element Fields:iterations SectionOne entry per BO iteration, in submission order. iteration_idx is dense and zero-based. Mid-run writes leave the array open-ended; readers must tolerate any non-negative length, including zero.
A multi-objective iteration carries one entry per config.objectives[i], in the same order:
Fields:
Note:
objective_values[i]is one aggregate vector per search point/iteration: by default the mean of finite trial-level objective values, or the pooled percentile when percentile pooling is configured. The GP/Optuna planner observes that aggregate vector, not every per-trial value separately. TheSearchIteration.resultsper-trial list held in memory by the planner is intentionally NOT serialized — read the per-trialprofile_export_aiperf.jsonfiles under each iteration’s variation directory if you need the spread.
best_trialsbest_trials is the post-hoc winner set over iterations whose objective_values is non-null. The shape adapts to the number of objectives:
len(config.objectives) == 1). best_trials is a length-1 list containing the global argmax (when direction == "MAXIMIZE") or argmin (when "MINIMIZE"). Single-objective is treated as the length-1 special case of the multi-objective shape — there is no separate scalar-best block.len(config.objectives) > 1). best_trials is the Pareto front: the set of iterations that are not dominated by any other iteration on every objective simultaneously. A trial A dominates B iff A is at least as good on every objective and strictly better on at least one. The front itself is unranked; if you want a tie-breaking order, sort by pareto_rank (always 0 for trials on the front) then by hypervolume contribution (not persisted here — recompute downstream if needed).best_trials is null until at least one iteration has produced a usable objective. Readers MUST tolerate the null state during early-run reads (and any read where every scored iteration’s objective_values is None).
A multi-objective Pareto front:
Fields:
Caveat:
best_trialsis “best of observed iterations,” not “true Pareto front of the search space.” Early termination (anyconvergence_reason) means the planner stopped before exhausting the budget; better trade-offs may exist outside the explored region.
convergence_reason takes one of the values below. The shared BO-set (everything except the monotonic_* and smooth_isotonic_* strings) is defined on OptunaSearchPlanner.convergence_reason() in src/aiperf/orchestrator/search_planner/optuna_planner.py; BayesianSearchPlanner inherits this implementation without override. The Optuna-terminator reasons (posterior_regret_bound, emmr) fire only when --optuna-terminator is set. The 1D-SLA planners (MonotonicSLASearchPlanner, SmoothIsotonicSLAPlanner) emit their own algorithm-specific strings — see the table below and the Bayesian Optimization — 1D SLA saturation guide.
The first signal to fire wins; later iterations are not run. See the BO guide’s convergence section for tuning advice and the Bayesian Optimization — 1D SLA saturation guide for the SLA-planner termination semantics.
boundary_summaryTop-level block. Emitted (non-null) when the search has exactly one dimension AND at least one iteration was recorded; null for multi-dim searches or empty history. Records the empirical feasibility boundary along the swept axis — most meaningful when at least one SLAFilter was configured (the max-concurrency-under-sla recipe is the canonical user), but the exporter does NOT gate on filter presence: with no filters every iteration’s feasible flag defaults to true, so feasible_max tracks the highest swept value and infeasible_min is null.
Base fields (written by MonotonicSLASearchPlanner, SmoothIsotonicSLAPlanner, and the BO post-hoc derivation):
Smooth-isotonic-only optional fields (written by SmoothIsotonicSLAPlanner when applicable; absent — not null — when produced by other planners or when the relevant phase did not run):
For full algorithm context (when each phase runs, the cliff-detection threshold, how the binding constraint is selected) see Bayesian Optimization — 1D SLA saturation (smooth_isotonic).
write_search_history(...) after each successful tell() AND once more on terminal exit (when ask() returns None). Readers MUST tolerate the partial state — the file is valid JSON at every observable instant only because each write is a single Path.write_bytes(...).Path.write_bytes call without a temp-file-then-rename. Concurrent readers may observe a torn write (zero bytes, partial JSON) on a slow filesystem; in practice the payload is small (a few KB up to ~100 KB for a 200-iteration run) and the race window is short. Treat a parse failure as “retry in a moment,” not as a corrupted run.iterations[i].iteration_idx == i (dense, zero-based). The planner-internal _iter counter increments on every tell(), regardless of trial success.convergence_reason. All earlier (mid-loop) writes carry convergence_reason: null. After a clean terminal exit (i.e. planner.ask() returned None), the orchestrator rewrites the file with planner.convergence_reason() or "unknown" — so a clean terminal exit always lands a non-null string, even when the planner did not record a structured reason. null in a finalized-looking file therefore implies abnormal termination (cancellation, crash, or hard process kill).iterations is the most recently-completed iteration, and convergence_reason will be null. The BO loop does NOT resume from the file in v1 — a restarted run begins with iteration 0.To compute summary statistics across the trajectory (e.g. learning curves), iterate history["iterations"] and skip entries where objective_values is None. For multi-objective hypervolume tracking, fold over [ov[i] for ov in iter['objective_values'] if ov is not None] paired with config['objectives'][i].direction.
aiperf version when building dashboards or downstream tooling against this artifact.objective_values[i] is the arithmetic mean across trials. It is the GP/Optuna planner’s observed aggregate vector for the point: the mean of finite trial values by default, or a pooled percentile when percentile pooling is enabled. If you need per-trial spread, read the per-trial profile_export_aiperf.json files at <base_dir>/search_iter_NNNN/profile_runs/run_NNNN/ — adaptive-search runs use a flat search_iter_NNNN per BO iteration (each holding profile_runs/run_NNNN/ for that iteration’s trials), distinct from grid sweeps’ {leaf}_{value} layout. See Sweep Aggregate API Reference for the full layout table.convergence_reason: "plateau_cv" can fire as early as iteration plateau_window. When the random-Sobol initial points happen to land in a flat region of the (scalar or hypervolume) objective, the coefficient-of-variation test trips immediately. This is correct, not a bug — increase plateau_window or tighten plateau_threshold if the run terminates too eagerly.config.search_space is the original spec, not what the planner sampled. The planner may explore the dimension’s range non-uniformly (Sobol initial points, then GP-driven exploitation). Use iterations[i].variation_values to see the actual samples; use config.search_space only to reproduce the original CLI/CRD invocation.best_trials is orthogonal to sweep_aggregate/’s best_configurations and pareto_optimal. Those belong to the SweepAnalyzer exporter, are computed across the whole RunResult set (including failed iterations), and may include points the BO planner never saw a finite objective for. Use best_trials for “what the BO loop converged on”; use sweep_aggregate/profile_export_aiperf_sweep.json for “what the post-hoc analyzer thinks is best across every cell.”sweep_aggregate/ companion artifact emitted alongside search_history.json.