Search History API Reference
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.
Overview
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:
- Recover the order in which the planner proposed configurations.
- Identify the best observed point(s) and how many iterations it took to find them. For multi-objective runs (
len(config.objectives) > 1)best_trialsis the Pareto front rather than a single argmax/argmin. - Determine why the run terminated (budget exhaustion, no-improvement patience, plateau, or — for the Optuna terminator — posterior-regret bound).
- Reproduce the original search-space specification (including objectives and outcome constraints) for a follow-up run.
File Location
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 ...):
JSON Schema
Top-Level Structure
Top-Level Fields:
config Section
A 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 Section
One 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.
Interpreting best_trials
best_trials is the post-hoc winner set over iterations whose objective_values is non-null. The shape adapts to the number of objectives:
- Single-objective (
len(config.objectives) == 1).best_trialsis a length-1 list containing the global argmax (whendirection == "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-bestblock. - Multi-objective (
len(config.objectives) > 1).best_trialsis 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 bypareto_rank(always0for 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 Reasons
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_summary
Top-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).
Lifecycle and Consistency Guarantees
- Rewritten after every iteration. The orchestrator calls
write_search_history(...)after each successfultell()AND once more on terminal exit (whenask()returnsNone). Readers MUST tolerate the partial state — the file is valid JSON at every observable instant only because each write is a singlePath.write_bytes(...). - NOT atomic. The current writer issues one
Path.write_bytescall 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. - Iteration order is submission order.
iterations[i].iteration_idx == i(dense, zero-based). The planner-internal_itercounter increments on everytell(), regardless of trial success. - Final write carries
convergence_reason. All earlier (mid-loop) writes carryconvergence_reason: null. After a clean terminal exit (i.e.planner.ask()returnedNone), the orchestrator rewrites the file withplanner.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.nullin a finalized-looking file therefore implies abnormal termination (cancellation, crash, or hard process kill). - Crash semantics. On controller-pod restart, cancellation, or a hard process kill, the last entry in
iterationsis the most recently-completed iteration, andconvergence_reasonwill benull. The BO loop does NOT resume from the file in v1 — a restarted run begins with iteration 0.
Programmatic Consumption
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.
Caveats
- Schema is not yet stable across versions. v1 emits the subset above; future releases may add fields (e.g. per-iteration timestamps, GP posterior summaries, hypervolume time-series). Pin your
aiperfversion 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-trialprofile_export_aiperf.jsonfiles at<base_dir>/search_iter_NNNN/profile_runs/run_NNNN/— adaptive-search runs use a flatsearch_iter_NNNNper BO iteration (each holdingprofile_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 iterationplateau_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 — increaseplateau_windowor tightenplateau_thresholdif the run terminates too eagerly.config.search_spaceis 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). Useiterations[i].variation_valuesto see the actual samples; useconfig.search_spaceonly to reproduce the original CLI/CRD invocation.best_trialsis orthogonal tosweep_aggregate/’sbest_configurationsandpareto_optimal. Those belong to theSweepAnalyzerexporter, are computed across the wholeRunResultset (including failed iterations), and may include points the BO planner never saw a finite objective for. Usebest_trialsfor “what the BO loop converged on”; usesweep_aggregate/profile_export_aiperf_sweep.jsonfor “what the post-hoc analyzer thinks is best across every cell.”
See Also
- Bayesian Optimization — algorithm semantics, convergence tuning, objective definition.
- Sweep Aggregate API Reference — the
sweep_aggregate/companion artifact emitted alongsidesearch_history.json. - Parameter Sweeping Tutorial — user guide for grid sweeps and adaptive search.