Configurable functions

The AC module allows the automatic configuration of configurable functions that are called inside other functions.

These configurable functions can be global or local configurable functions, and can also be the entrypoint for the automatic configuration process.

Global configurable functions

Consider the following configurable function:

>>> from optilog.tuning import ac, Bool, Int, Real, Categorical
>>>
>>> @ac
>>> def func1(
>>>     x, data,
>>>     p1: Bool() = True, p2: Real(-1.3, 2) = 0,
>>>     p3: Int(-5, 5) = 0, p4: Categorical("A", "B", "C") = "A"):
>>>     ...
>>>     # Does some computations and returns a float

This function is used inside another configurable function, as it is shown below.

>>> import random
>>>
>>> @ac
>>> def func2(data, seed, n: Int(0, 5) = 3):
>>>     random.seed(seed)
>>>     val1 = func1(random.randint(10, 20), data)
>>>     val2 = func1(random.randint(10, 20), data)
>>>     return val1 * val2 * n

For this case, func1 is called using the same value for its configurable parameters. The tuning module automatically handles this case.

When a function is decorated with the @ac decorator it becomes a global configurable function. By default, all the configurable functions will be called using the same values for their configurable parameters.

Local configurable functions

Consider the previous example where func1 behaves as a global configurable function inside func2:

>>> import random
>>>
>>> @ac
>>> def func2(data, seed, n: Int(0, 5) = 3):
>>>     random.seed(seed)
>>>     val1 = func1(random.randint(10, 20), data)
>>>     val2 = func1(random.randint(10, 20), data)
>>>     return val1 * val2 * n

For this example, we will assume that the performance of func2 may be improved if both calls to func1 receive different values for its configurable parameters. Then, func1 should behave as a local configurable function. We can achieve that using the CfgCall parameter type:

>>> import random
>>> from optilog.tuning import ac, Int, CfgCall
>>>
>>> @ac
>>> def func2(
>>>     data, seed,
>>>     func1_call1: CfgCall(func1),
>>>     func1_call2: CfgCall(func1),
>>>     n: Int(0, 5) = 3
>>> ):
>>>     random.seed(seed)
>>>     val1 = func1_call1(random.randint(10, 20), data)
>>>     val2 = func1_call2(random.randint(10, 20), data)
>>>     return val1 * val2 * n

The CfgCall parameter type receives a configurable function and allows its local configuration.

For the example, two parameters are added to func2: func1_call1 and func1_call2. These are two different instances of func1 that can receive different values for the configurable parameters of each of them.

Notice that CfgCall does not need a default value, because the configurable parameters are already annotated with default values. The configured callable gets injected by the ac decorator. The user doesn’t need to provide any value for the parameter.

Entrypoint functions

An entrypoint function is a function that the AC tool will call to perform its configuration process. Any global configurable function can be an entrypoint to the AC tool.

Consider the example function that we have discussed in the Local configurable functions section:

>>> import random
>>> from optilog.tuning import ac, Int, CfgCall
>>>
>>> @ac
>>> def func2(
>>>     data, seed,
>>>     func1_call1: CfgCall(func1),
>>>     func1_call2: CfgCall(func1),
>>>     n: Int(0, 5) = 3
>>> ):
>>>     random.seed(seed)
>>>     val1 = func1_call1(random.randint(10, 20), data)
>>>     val2 = func1_call2(random.randint(10, 20), data)
>>>     return val1 * val2 * n

We can try to minimize the result returned by the func2 function. To do that, we can make this function an entrypoint and configure the values for the n parameter and for the two calls to func1 (which in this case is a local configurable function). We only have to print the value we want to optimize using any format:

>>> import random
>>> from optilog.tuning import ac, Int, CfgCall
>>>
>>> @ac
>>> def func2(
>>>     data, seed,
>>>     func1_call1: CfgCall(func1),
>>>     func1_call2: CfgCall(func1),
>>>     n: Int(0, 5) = 3
>>> ):
>>>     random.seed(seed)
>>>     val1 = func1_call1(random.randint(10, 20), data)
>>>     val2 = func1_call2(random.randint(10, 20), data)
>>>     res = val1 * val2 * n
>>>     print("Result:", res)
>>>     return res

Later, the user will have to provide the regular expression that is needed to capture the printed result when initializing the Configurator class (for the example we could use the regular expression ^Result: ([+-]?\d+(?:\.\d+)?)$ to match for floats). During the execution of some functions, we might find and report intermediate results. Optilog is aware of this, and will only capture the last printed line that matches the regular expression.

Finally, the user must know the list of global configurable functions that the entrypoint is calling (either directly or though another function), as we also have to specify them during the initialization of the Configurator class for Optilog to configure them.