Pin a strategy and seed#

suggest is a pure function of the experiment state, the config, and the ask’s explicit intents (none on this page), so a pinned config plus a pinned seed replays the same sequence of suggestions on the same machine and dependency set. The field catalogue is in Strategies and config; why determinism holds is in Provenance and determinism. Across CPU classes or torch builds the GP rungs can drift, so the numbers below come from one machine.

Pin the strategy, params, and seed#

  1. Build the config once: {"strategy": <name>, "params": {...}, "seed": <int>, "timebox_mode": <mode>}. strategy is one of random, sobol, gp_ei, ucb_cl, gp_nehvi, or "auto" (the default when omitted: the policy’s own routing with your seed carried along; it takes no params).

  2. Pass the same dict to every suggest. An explicit seed is used verbatim; without one the engine derives a seed from the state, which is still deterministic.

  3. Read suggestion.provenance: strategy is the rung that answered, fallback_from the rung you asked for when the ladder had to step down, and config_hash the config that produced the answer. Pinning a GP rung before any evidence exists ladders to sobol on the first ask, and the receipt says so.

import json

from tauopt import Client

CONFIG = {
    "strategy": "gp_ei",
    "params": {"num_restarts": 4, "raw_samples": 32},
    "seed": 7,
    "timebox_mode": "caps",
}


def run(seed):
    client = Client(driver="local")
    exp = client.create_experiment(
        name=f"pinned-seed-{seed}",
        parameters=[
            {"name": "x1", "type": "double", "bounds": (0.0, 1.0)},
            {"name": "x2", "type": "double", "bounds": (0.0, 1.0)},
        ],
        metric={"name": "y", "objective": "minimize"},
    )
    rows = []
    for _ in range(6):
        sug = client.suggest(exp.id, config={**CONFIG, "seed": seed})
        a = sug.assignments
        client.observe(exp.id, suggestion_id=sug.id, value=(a["x1"] - 0.3) ** 2 + (a["x2"] - 0.6) ** 2)
        p = sug.provenance
        rows.append([p.strategy, p.fallback_from, p.seed, p.config_hash, a])
    return rows


first, second, other = run(7), run(7), run(8)
for row in first:
    print(json.dumps(row))
print("same seed, identical sequence:", first == second)
print("different seed, identical sequence:", first == other)
["sobol", "gp_ei", 7, "blake2b_0c33c580abb4ef52", {"x1": 0.5792599907144904, "x2": 0.7402846803888679}]
["gp_ei", null, 7, "blake2b_5a8dcaac58aafacc", {"x1": 0.0, "x2": 0.0}]
["gp_ei", null, 7, "blake2b_5a8dcaac58aafacc", {"x1": 0.7783309004894556, "x2": 0.9680527482321146}]
["gp_ei", null, 7, "blake2b_5a8dcaac58aafacc", {"x1": 0.49411697609275884, "x2": 0.6428542374832091}]
["gp_ei", null, 7, "blake2b_5a8dcaac58aafacc", {"x1": 0.28465737967191573, "x2": 0.747232579756825}]
["gp_ei", null, 7, "blake2b_5a8dcaac58aafacc", {"x1": 0.14225492258083972, "x2": 0.6457468785976386}]
same seed, identical sequence: True
different seed, identical sequence: False

The first row’s config_hash differs from the rest because a fallback rung runs with its own default params, and the receipt hashes the config that ran.

Choose a timebox mode for replay#

  • "budget" (the default): each GP rung gets a wall-clock timebox (params.timebox_s, 10 seconds by default). Pure as long as the timebox does not bind; on a loaded machine a binding timebox ladders down and records it.

  • "off": no wall budget; fits and acquisition run to convergence.

  • "caps": clock-free like "off", with deterministic work ceilings (fit_maxiter, acq_maxiter), so the answer never depends on load.

Pin "caps" or "off" whenever you need load-independent replay.

Bound the wait#

max_wait_s (a number in (0, 300]) deadlines the whole ask. What happens when it expires depends on the mode and on on_timeout:

  1. Default on_timeout="ladder": the GP rungs trip and the instant rungs answer, with fallback_from recorded.

  2. on_timeout="fail": the ask raises 503 suggest_timeout instead of degrading; hosted responses carry Retry-After, exposed as e.retry_after.

  3. timebox_mode="caps" under a deadline: the engine refuses with AskDeadlineExceeded (importable from tauopt and from tauopt.engine; a 503 ask_deadline_exceeded ApiError on both drivers, with no Retry-After because a caps re-ask repeats deterministic work) at a rung boundary or at delivery, never substituting another rung’s answer, and no suggestion opens; the refusal receipt rides e.point, e.max_wait_s, e.elapsed_s, e.rung, and e.details. Catch the class next to OptimizationComplete in caps-mode loops, or let an except ApiError handler see it (both drivers raise the same class).

  4. On the local driver, in every timebox mode, whenever the process supervisor is engaged (the default "auto" or "process"; not supervisor="off", and not "auto" on Windows, where the ask runs inline with no backstop): a deadline-bearing ask is also backstopped above the engine. If the worker process running it has not answered within max(1.25 * max_wait_s, max_wait_s + 3 s) (a kernel that never returns, not the engine’s own clocks), the ask is refused with BackstopExceeded (the same 503 ask_deadline_exceeded ApiError, with point backstop and a stage), the worker killed and replaced behind the refusal, and no suggestion opens; Client(driver="local", supervisor="off") runs the ask inline as before. The hosted plane answers the same on its queue lane (provenance.lane worker).

A misspelled strategy name never raises: the ladder answers and fallback_from carries the typo. The run forces each path with an unreachable budget.

from tauopt import ApiError, Client
from tauopt.engine import AskDeadlineExceeded

client = Client(driver="local")
exp = client.create_experiment(
    name="wait-budget",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
)
for _ in range(4):  # past the space-filling bootstrap, so the GP rung is routed
    sug = client.suggest(exp.id, config={"strategy": "auto", "seed": 1})
    client.observe(exp.id, suggestion_id=sug.id, value=(sug.assignments["x"] - 0.4) ** 2)

# default on_timeout="ladder": an unreachable budget trips the GP rungs and the
# instant rungs answer, with the fallback recorded
sug = client.suggest(exp.id, max_wait_s=1e-6)
print("ladder:", sug.provenance.strategy, "fallback_from", sug.provenance.fallback_from)
client.close_suggestion(exp.id, sug.id)

# on_timeout="fail": a timeout error instead of a degraded answer
try:
    client.suggest(exp.id, max_wait_s=1e-6, on_timeout="fail")
except ApiError as e:
    print("fail:", e.status_code, e.error_type, "-", e.message)

# caps mode under a deadline: the engine's own refusal escapes the local driver as is
try:
    client.suggest(exp.id, config={"strategy": "auto", "seed": 1, "timebox_mode": "caps"}, max_wait_s=1e-6)
except AskDeadlineExceeded as e:
    print("caps:", type(e).__name__, "point", e.point, "rung", e.rung)

# a misspelled strategy never raises: the ladder answers and provenance names the typo
sug = client.suggest(exp.id, config={"strategy": "gp_ie", "seed": 1})
print("typo:", sug.provenance.strategy, "fallback_from", sug.provenance.fallback_from)
ladder: sobol fallback_from gp_ei
fail: 503 suggest_timeout - ask exceeded max_wait_s=1e-06 before gp_ei could run
caps: AskDeadlineExceeded point rung_boundary rung gp_ei
typo: gp_ei fallback_from gp_ie