Overriding Configurations#

../_images/overrides_cut.jpg

Image generated with DALL-E#

Whether a soleil described object is loaded from a Python script or resolved following a CLI command, under the hood, the processing is carried out by a call to load_solconf() of the form

resolved_obj = load_solconf('./root_config.solconf', overrides=[...])

The value provided to the overrides parameter is always a list that admits target variable name path/value pairs in multiple possible formats (see OverrideSpec).

For example, entries of overrides can be strings in various Python-syntax-compatible forms, possibly specifying more than one override in each entry:

overrides=['a=3', 'b=[1,2]', 'c={1,2}', 'd=(1,2)']
overrides=['a="xyz"; b=1e-5']
overrides=["""
a=b'xyxy'
b={1:-4,'a':5}
"""]

Note

Overrides provided from the CLI are always in string format – they will be evaluated using a special Python parser.

Another useful type admitted as an entry of overrides is a dictionary of Python objects:

overrides=[{'a':3, 'b':-4}]

All override syntaxes can further be combined in a single list:

overrides=['a=1', {'b':2}, "c=3;d=4", {'e':5, 'f':6}]

Variable name paths#

The target variable name path of an override specifies the position of the target relative to the root configuration. Consider, as an example, the following part of the train.solconf configuration introduced in the Getting Started section:

18class optimizer:
19    type: as_type = "torch.optim:SGD"
20    params = resolved(net).parameters()
21    lr = 0.001
22    momentum = 0.9

Since the optimizer class is defined at the highest level in the root config, the learning rate parameter can be modified using an override of the form:

'optimizer.lr=0.002'
{'optimizer.lr': 0.002}

In general, keys for members of a (nested) class in the root config will be the dot-separated class qualified name and variable name. So, given the following class with members that are also object descriptors (assumed defined in the root config)

class model:
   type: as_type = "soleil_examples.cifar.model:Net"

   class backbone:
      type: astype = 'torch.nn:Linear'
      in_features = 10
      out_features = 20

one can modify the number of output features using

'model.backbone.out_features=30'

Variable name paths for loaded members are formed accordingly by dot-joining the variable name path of the load target variable, and that of the override target (as if it were in the root config). For example, given the following line from root config train.solconf

28data: hidden = load(".data.default")

the member root of class dataset (defined in loaded file data/default.solconf) can be overriden with the following overrides:

"data.dataset.root='/my/new/root'"
{'data.dataset.root': '/my/new/root'}

Evaluation time and context#

All overrides make use of AST magic to ensure that the override is applied at the moment of the target variable’s definition. This ensures that any other variables that depend on the value of the overriden member themselves see the overriden value. See Mechanism below for the approach used to do this.

Currently, the globals dictionary used when evaluating the value of the override is empty. This means that only constants can be assigned as override values. Some extra flexibility is added by means of the special overrideables submodule and choices.

Todo

  1. Inject from soleil.solconf import * `` into to overrides evaluation globals. 2) Add a ``ref(<var path>) method that resolves to the description of a given path. Resolved attributes can be accessed using resolved(ref(<var path>)).

Mechanism#

In order to support overrides, the Soleil pre-processor converts every variable assignment such as

a = 1

class B:
     b = 2

into a call to a special function _soleil_override(), as follows:

a = _soleil_override('a', 1)

class B:
     b = _soleil_override('b', 2)

When the module executes during a call to load_solconf(), the call to _soleil_override() first checks whether an override was specified for that variable and returns that override value if so, or the original value otherwise. To do so, _soleil_override() matches a variable name path computed for each variable to the names specified in the CLI override strings or keys. Note that these variable name paths specify the position of each variable relative to root configuration loaded with load_solconf().

Variable name paths are computed using the first argument to _soleil_override() and the name a given module was loaded to, which is contained in module-level variable __soleil_qualname__.

For example, the variable paths for all variables are given in the comments below when calling load_solconf('<path>/main.solconf')

# main.solconf
####################
# The root configuration has `__soleil_qualname__ = None`
a = 1               # 'a'
class B:            # 'B'
    b = 2           # 'B.b'
C = load('.submod') # 'C', pre-proc converts to `load('.submod', _target='c')`

# submod.solconf
####################
# The module has `__soleil_qualname__ = 'C'`
c = 3               # 'C.c'
d = 4               # 'C.d'

In order to maintain the __soleil_qualname__ module variables, the Soleil pre-processor injects _target keywords into all simple load() statements (see the example above).

String overrides parser#

Overrides provided as strings are parsed with a special parser that limits the permissible syntax constructs to variable assignments and constants. This offers some protection against erroneous CLI input.