Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access sub-ingredient's config #923

Open
mrzv opened this issue Nov 20, 2023 · 12 comments
Open

Access sub-ingredient's config #923

mrzv opened this issue Nov 20, 2023 · 12 comments

Comments

@mrzv
Copy link

mrzv commented Nov 20, 2023

I have two ingredients. One of them knows about the other one. When configuring the second one, I'd like to be able to access a config value from the first one, but I can't find a way to do this. Here's an example that sketches what I'm after.

data = Ingredient('data')
model = Ingredient('model', ingredients = [data])

@data.config
def data_config():
    a = 5

@model.config
def model_config(data):
    b = data['a'] / 2      # this doesn't work

Is there any way to do this?

@mrzv
Copy link
Author

mrzv commented Nov 20, 2023

Actually, this works as intended; not sure why I thought it didn't. I think I'm actually after "sibling" config. I.e.,

ex = Experiment()

@ex.config
def a_config():
    a = 5


@ex.config
def b_config():
    b = a / 2      # how to make this work?

How can I make something like this work?

@mrzv
Copy link
Author

mrzv commented Nov 20, 2023

The first version also doesn't work, if I switch model_config to a named_config, i.e.,

@model.named_config
def model_config(data):
    b = data['a'] / 2      # this doesn't work

Then if I invoked ... with model.model_config, I get

Traceback (most recent calls):
  File ".../lib/python3.11/site-packages/sacred/config/custom_containers.py", line 79, in __getitem__
    raise KeyError(item)
KeyError: 'a'

@mrzv
Copy link
Author

mrzv commented Nov 20, 2023

In the second example (sibling config), it should also be named_config:

ex = Experiment()

@ex.config
def a_config():
    a = 5

@ex.named_config
def b_config(a):
    b = a / 2      # how to make this work?

I get

Traceback (most recent calls):
  File ".../lib/python3.11/site-packages/sacred/config/config_scope.py", line 64, in __call__
    raise KeyError(
KeyError: "'a' not in preset for ConfigScope. Available options are: {'_log'}"

So this is really a question about named_configs.

@boeddeker
Copy link
Contributor

The problem is that b_config may overwrite values in a_config (Basic assumption in sacreds config system).
Hence, first b_config is executed and then a_config. So while b_config is executed, a is unknown.

Here, an example, that may give you an idea, how the sacred magic works:

ex = Experiment()

@ex.config
def a_config():
    b = 2
    a = 5 * b

@ex.named_config
def b_config(a):
    b = a / 2      # This cannot work, because a may depend on b
    b = 11      # This would work and a would then be 5*11

@mrzv
Copy link
Author

mrzv commented Nov 20, 2023

I guess I don't understand the precedence. Named configs are executed before regular configs, but their values take precedence over regular configs? I.e., regular configs don't over-write the already set values? Is that right?

What is the right way to deal with this, where I want to access some global config value in a named config, which I promise won't modify it? Is there some kind of read-only access? Or that's impossible because the value is just not there? Do I have to copy the "default" values that everybody needs access too to every config? That seems like substantial code duplication.

@mrzv
Copy link
Author

mrzv commented Nov 20, 2023

It seems having a global default_a variable works as intended. It does mean code duplication, but perhaps that's good enough for my purposes.

default_a = 5

@ex.config
def a_config():
    a = default_a

@ex.named_config
def b_config():
    a = default_a
    b = a / 2      # how to make this work?

@boeddeker
Copy link
Contributor

I guess I don't understand the precedence. Named configs are executed before regular configs, but their values take precedence over regular configs? I.e., regular configs don't over-write the already set values? Is that right?

Yes, that is right. Sacred has priorities, which config values are used, if they are multiple times set.
Additionally, there is some magic involved, when "config scopes" are used.
This magic works only in one direction: higher priority -> lower priority

What is the right way to deal with this, where I want to access some global config value in a named config, which I promise won't modify it? Is there some kind of read-only access? Or that's impossible because the value is just not there? Do I have to copy the "default" values that everybody needs access too to every config? That seems like substantial code duplication.

I have never required something like that. I overwrite values in a named config (For me it is an "shortcut" for the CLI). Then the default config resolves dependencies (e.g. if statements).
If you have precisely this use case, the global variable might be the best solution.

@mrzv
Copy link
Author

mrzv commented Nov 21, 2023

The global variable seems to do the trick. Thanks for your help.

@mrzv mrzv closed this as completed Nov 21, 2023
@mrzv
Copy link
Author

mrzv commented Nov 21, 2023

Sorry, there is one more thing I don't understand. What's the logic why this doesn't work for sub-ingredients? If I explicitly mark model as dependent on data, as above, and then I want to access some variable in the data scope from a named_config for a model, why doesn't that work? Does the dependency not imply precedence?

@mrzv mrzv reopened this Nov 21, 2023
@boeddeker
Copy link
Contributor

I don't use ingredients, so I don't know.

@thequilo
Copy link
Collaborator

Named configs are always evaluated before "normal" configs, no matter where they are defined.

This is the place where it happens:

sacred/sacred/initialize.py

Lines 420 to 442 in cd90ee1

# Phase 2: Named Configs
for ncfg in named_configs:
scaff, cfg_name = get_scaffolding_and_config_name(ncfg, scaffolding)
scaff.gather_fallbacks()
ncfg_updates = scaff.run_named_config(cfg_name)
distribute_presets(scaff.path, prefixes, scaffolding, ncfg_updates)
for ncfg_key, value in iterate_flattened(ncfg_updates):
set_by_dotted_path(config_updates, join_paths(scaff.path, ncfg_key), value)
distribute_config_updates(prefixes, scaffolding, config_updates)
# Phase 3: Normal config scopes
for scaffold in scaffolding.values():
scaffold.gather_fallbacks()
scaffold.set_up_config()
# update global config
config = get_configuration(scaffolding)
# run config hooks
config_hook_updates = scaffold.run_config_hooks(
config, command_name, run_logger
)
recursive_update(scaffold.config, config_hook_updates)

While I agree that this is confusing and it would make total sense to fully evaluate the sub-ingredient's config (named + normal) before the parent ingredient, this would be a substantial change in the core code.

@thequilo
Copy link
Collaborator

And currently you can overwrite a sub-ingredient's config from its parent. Run with named:

from sacred import Experiment, Ingredient

i = Ingredient('i')


@i.config
def defaults():
    a = 42


ex = Experiment(ingredients=[i])


@ex.named_config
def named():
    i = {'a': 123}


@ex.automain
def main(_config):
    print(_config)

gives

{'i': {'a': 123}, 'seed': 515843388}

This is incompatible with the other processing order. But I'm unsure which one is more intuitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants