Limits and when not to use TauOpt#

Observation count against the default timebox#

Every ask on a Gaussian-process (GP) rung refits the model from scratch on all scored observations: make_and_fit_gp builds a SingleTaskGP and maximizes its exact marginal likelihood, which works through an n-by-n covariance matrix, so the cost of one ask grows with the number of training rows after duplicate-assignment aggregation (n). The fit is pinned to one thread, so more cores do not shorten it. The default posture puts a wall-clock budget on that work: timebox_mode budget, with a timebox_s of 10 seconds per GP rung. The budget binds in two ways. The hyperparameter fit receives a share of the remaining budget as a scipy timeout (60 percent on gp_ei, split evenly across the objective and constraint components; 50 percent on ucb_cl; 50 percent on gp_nehvi, split across its components), and a fit that hits it returns its best-so-far hyperparameters rather than raising, so a GP rung can answer from a truncated fit with no fallback_from; no fit record carries a convergence status (the receipt’s fits entries hold the fitted hyperparameters and the training-set shape), so the one tell is timings.fit_s approaching the fit’s share (6 seconds of the default 10 on an unconstrained gp_ei ask). The share is cooperative: a long linear-algebra call inside a phase cannot be interrupted, so the budget is a target the rung checks between steps, not a hard cap. Above the engine, a deadline-bearing ask (max_wait_s set) on the local driver is backstopped by a supervisor: the ask runs on one warm worker process, and if the worker has not answered within max(1.25 * max_wait_s, max_wait_s + 3 s) the ask is refused with BackstopExceeded (a 503 ask_deadline_exceeded with point backstop), the worker killed and replaced behind the refusal, the state untouched – a kernel that never returns costs one typed refusal, never a hung process (Client(driver="local", supervisor=...) picks the shape; the hosted plane answers the same from its queue lane’s worker pool, provenance.lane worker). The backstop withholds and never shapes: a delivered answer is the engine’s answer, byte for byte. TimeboxExceeded fires when a GP rung overruns its own timebox_s (the clock starts with the rung; a laddered ask can spend one budget per GP rung; only max_wait_s bounds the whole ask), checked at phase boundaries and once after the answer is computed: then the ask descends the ladder (gp_ei to ucb_cl to sobol to random) and the receipt records the descent in fallback_from. A long study keeps answering either way, and its answers are space-filling once ucb_cl has overrun its own budget too. Watch fallback_from and timings.fit_s on every suggestion, raise timebox_s (up to 300), or pin timebox_mode caps or off when the answer must not depend on the clock.

Dimensionality#

The GP rungs carry no high-dimensional method: no trust region, no subspace embedding, no sparsity prior. The acquisition optimizer is BoTorch’s optimize_acqf over the unit cube: it scores DEFAULT_RAW_SAMPLES quasi-random points (128 for gp_ei and gp_nehvi, 64 for ucb_cl; Sobol draws, or polytope samples under linear constraints), draws DEFAULT_NUM_RESTARTS of them (8 and 4) as starts with probability rising in their acquisition value (the best-scoring point is always among them), and refines each start by gradient ascent (L-BFGS-B, or SLSQP when linear constraints are present), keeping the best endpoint. The start pool does not grow with dimension, so in a wider box the restarts begin from a thinner sample. The one dimension-aware element is the lengthscale prior of BoTorch’s default kernel, whose location grows with the logarithm of the dimension count. The default policy’s bootstrap lengthens with dimension too: max(4, 2 * dim) scored observations before the first GP ask, where dim is the parameter count (Space.dim) while the GP fits in the model dimension (Space.model_dim, one column per categorical value): a three-parameter space with one twelve-valued categorical bootstraps on six observations and then fits a fourteen-dimensional GP.

The model the engine builds#

gp_ei and ucb_cl fit one SingleTaskGP for the objective (gp_ei adds one per outcome-constraint metric) in double precision on model-view rows (unit-scaled numerics, one-hot categoricals) with BoTorch’s default kernel: an RBF kernel with one lengthscale per dimension, a dimension-scaled log-normal lengthscale prior, a lengthscale floor of 0.025, and no output-scale kernel; outcomes pass through a Standardize transform. The fit runs fit_gpytorch_mll with a single attempt (L-BFGS-B through scipy, no retries), bounded by the timebox. The acquisition is LogExpectedImprovement for one point, qLogExpectedImprovement with a 256-sample Sobol sampler for a batch, and the constrained variants when constraint metrics exist. gp_nehvi fits one such GP per optimized and constraint metric under qLogNoisyExpectedHypervolumeImprovement. An optimized metric without a threshold has its reference-point component recomputed from the current observations on every ask (ten percent of the observed span beyond the worst observed value, away from the optimum; the engine computes it in its maximization frame as the minimum minus a tenth of the span, and a zero span counts as one), so the hypervolume being improved moves as points arrive; declare a threshold on every optimized metric when the frontier target must stay fixed.

ucb_cl passes beta (default 2.0) straight to BoTorch’s analytic UpperConfidenceBound, whose score is mean + sqrt(beta) * sigma; the default therefore sits about 1.4 posterior standard deviations above the mean rather than 2.

Categorical and integer parameters#

A categorical parameter enters the GP as a one-hot block, one column per value, so a parameter with twelve values adds twelve model dimensions. The acquisition optimizer treats those columns as continuous coordinates in [0, 1], and the answer decodes by the largest column (Parameter.from_model_block). The optimizer’s endpoint is in general a fractional row across that block, so the suggested value is a point the acquisition never evaluated exactly, the same class of gap as the integer rounding below. The alternative embeddings, categorical_mode mixed (a CategoricalKernel over category indices) and mixed_capped (mixed under a capped restart and sample bundle), are experimental and refuse conditional spaces.

An integer parameter is optimized on its continuous relaxation and rounded on the way out, in Parameter.from_model_block on the GP rungs and Parameter.from_unit on the space-filling rungs: the model never sees the integer structure, and on a small range a whole interval of continuous optima rounds to one value. For a handful of admissible values, declare an integer grid or a categorical.

Noise#

A reported value_stddev enters the fit squared, as that point’s fixed variance (points without one are imputed at the median of the reported stddevs or, under gp_ei’s mll_impute noise mode, at the stage-1 inferred level; when none report, one noise level is inferred for all). On the inferred-noise path the fit’s noise level is constrained to at least the value resolve_floor_std returns, max(1e-4, base_noise, s2_pooled / stdvs**2) in standardized-variance units, with base_noise defaulting to 1e-3 and s2_pooled the disagreement among repeated assignments; there base_noise=0 lowers that floor to 1e-4 rather than removing it. On the fixed-noise path each reported variance is raised to at least base_noise times the standardized variance, with no 1e-4 minimum and no pooled term, and repeat disagreement enters per group of repeated assignments as max(v_rep, s2_g / n_g), where v_rep is the group’s mean reported variance divided by the group size and s2_g its sample variance; an explicit zero leaves the reported variances verbatim.

What the engine does not do#

  • It never calls your objective: you evaluate and report, and no built-in parallel evaluation exists, locally or hosted.

  • It has no multi-fidelity strategy: every observation is a full evaluation, and no strategy or metric field represents fidelity or evaluation cost.

  • It runs on the CPU only. Tensors are built in float64 on the CPU, and no code path moves them to a GPU.

  • The local driver is one process and is not thread-safe: LocalDriver does unsynchronized check-then-set on plain dictionaries, so run one thread per driver. Its state lives in process memory, so a killed process takes the experiment with it. A deadline-bearing ask runs on one worker process the driver supervises (a fresh interpreter with its own import of the math stack and its own GP lock; POSIX only in this version); the state stays in the parent, which hands the worker a pickled copy per ask. The worker leaves when its parent dies (it watches the parent’s pid); a worker wedged in a kernel that holds the GIL outlives a parent killed outright and is the OS’s to end. Sharing an experiment across threads, processes, or nodes is the hosted service’s job. GP asks also serialize process-wide: every GP rung takes GP_COMPUTE_LOCK (torch’s thread pinning and RNG forking are process-global), so concurrent GP asks on different experiments in one process queue behind one fit at a time.

  • Linear constraints cover plain double parameters only, and the space-filling rungs honor them by rejection: after 100,000 consecutive rejected draws the rung raises instead of looping.

  • At most four metrics may be optimized at once.