|
| 1 | +# Adaptive Capping |
| 2 | + |
| 3 | +Adaptive capping is a feature that can be used to speedup the evaluation of candidate configurations when the objective |
| 4 | +is to minimize runtime of an algorithm across a set of instances. The basic idea is to terminate unpromising candidates |
| 5 | +early and adapting the timeout for solving a single instance dynamically based on the incumbent's runtime and the. |
| 6 | +runtime already used by the challenging configuration. |
| 7 | + |
| 8 | +## Theoretical Background |
| 9 | + |
| 10 | +When comparing a challenger configuration with the current incumbent for a (sub-)set of instances, we already know how |
| 11 | +much cost (in terms of runtime) was incurred by the incumbent to solve the set of instances. As soon as the challenger |
| 12 | +configuration exceeds the cost of the incumbent, it is evident that the challenger will not become the new incumbent |
| 13 | +since the costs accumulate over time and are strictly positive, i.e., solving an instance cannot have negative runtime. |
| 14 | + |
| 15 | +Example: |
| 16 | +*Let the incumbent be evaluated for two instances with observed runtimes 3s and 4s. When a challenger configuration is |
| 17 | +evaluated and compared against the incumbent, it is first evaluated on a first instance. For example, we observe a |
| 18 | +runtime of 2s. As the challenger appears to be a promising configuration, its evaluation is intensified and the budget |
| 19 | +is doubled, i.e., the budget is increased to 2. For solving the second instance, adaptive capping will allow a timeout |
| 20 | +of 5s since the sum of runtimes for the incumbent is 7s and the challenger used up 2s for solving the first instance so |
| 21 | +far so that 5s remain until the costs of the incumbent are exceeded. Even if the challenger configuration would need 10s |
| 22 | +to solve the second instance, its execution would be aborted. In this example, by adaptive capping we thus save 5s of |
| 23 | +evaluation costs for the challenger to notice that it will not replace the current incumbent.* |
| 24 | + |
| 25 | +In combination with random online aggressive racing, we can further speedup the evaluation of challenger configurations |
| 26 | +as we increase the horizon for adaptive capping step by step with every step of intensification. Note that |
| 27 | +intensification will double the number of instances to which the challenger configuration (and eventually also the |
| 28 | +incumbent configuration) are applied to. Furthermore, to increase the trust into the current incumbent, the incumbent is |
| 29 | +regularly subject to intensification. |
| 30 | + |
| 31 | + |
| 32 | +## Setting up Adaptive Capping |
| 33 | + |
| 34 | +To achieve this, the user must take active care in the termination of their target function. |
| 35 | +The capped problem.train will receive a budget keyword argument, detailing the seconds allocated to the configuration. |
| 36 | +Below is an example of a capped problem that will return the used budget if the computation exceeds the budget. |
| 37 | + |
| 38 | + |
| 39 | +```python |
| 40 | + |
| 41 | + class TimeoutException(Exception): |
| 42 | + pass |
| 43 | + |
| 44 | + |
| 45 | + @contextmanager |
| 46 | + def timeout(seconds): |
| 47 | + def handler(signum, frame): |
| 48 | + raise TimeoutException(f"Function call exceeded timeout of {seconds} seconds") |
| 49 | + |
| 50 | + # Set the signal handler for the alarm signal |
| 51 | + signal.signal(signal.SIGALRM, handler) |
| 52 | + signal.alarm(seconds) # Schedule an alarm after the given number of seconds |
| 53 | + |
| 54 | + try: |
| 55 | + yield |
| 56 | + finally: |
| 57 | + # Cancel the alarm if the block finishes before timeout |
| 58 | + signal.alarm(0) |
| 59 | + |
| 60 | + |
| 61 | + class CappedProblem: |
| 62 | + @property |
| 63 | + def configspace(self) -> ConfigurationSpace: |
| 64 | + ... |
| 65 | + |
| 66 | + def train(self, config: Configuration, instance:str, budget, seed: int = 0) -> float: |
| 67 | + |
| 68 | + try: |
| 69 | + with timeout(int(math.ceil(budget))): |
| 70 | + start_time = time.time() |
| 71 | + ... # heavy computation |
| 72 | + runtime = time.time() - start_time |
| 73 | + return runtime |
| 74 | + except TimeoutException as e: |
| 75 | + print(f"Timeout for configuration {config} with runtime budget {budget}") |
| 76 | + return budget # here the runtime is capped and we return the used budget. |
| 77 | +``` |
| 78 | + |
| 79 | +In order to enable adaptive capping in smac, we need to create [problem instances](4_instances.md) to optimize over and specify a |
| 80 | +global runtime cutoff in the intensifier. Then we optimize as usual. |
| 81 | + |
| 82 | + |
| 83 | +```python |
| 84 | +from smac.intensifier import Intensifier |
| 85 | +from smac.scenario.scenario import Scenario |
| 86 | + |
| 87 | +scenario = Scenario( |
| 88 | + capped_problem.configspace, |
| 89 | + ... |
| 90 | + instances=['1', '2', '3'], # add problem instances we want to solve |
| 91 | + instance_features={'1': [1], '2': [2], '3': [3]} # in the absence of actual features add dummy features for identification |
| 92 | +) |
| 93 | + |
| 94 | +intensifier = Intensifier( |
| 95 | +scenario, |
| 96 | +runtime_cutoff=10 # specify an absolute runtime cutoff (sum over instances) never to be exceeded |
| 97 | +) |
| 98 | + |
| 99 | +smac = HyperparameterOptimizationFacade( |
| 100 | + scenario, |
| 101 | + capped_problem.train, |
| 102 | + intensifier=intensifier, |
| 103 | + ... |
| 104 | +) |
| 105 | + |
| 106 | +incumbent = smac.optimize() |
| 107 | +``` |
0 commit comments