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
float64on the CPU, and no code path moves them to a GPU.The local driver is one process and is not thread-safe:
LocalDriverdoes 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 takesGP_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
doubleparameters 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.