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

Replace dictionary (instead of merging) from command line #2091

Open
slerman12 opened this issue Mar 14, 2022 · 11 comments
Open

Replace dictionary (instead of merging) from command line #2091

slerman12 opened this issue Mar 14, 2022 · 11 comments
Labels
question Hydra usage question

Comments

@slerman12
Copy link

slerman12 commented Mar 14, 2022

I define a default value for an argument, let's call it argument, as a dict in my Config.cfg file:

argument: {aaa: {bbb: 1, ccc: 2}}

Then I try to pass a different dict as an argument via the command line:

argument="{aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}"

And. I get this error:

Could not override 'argument'. To append to your config use +argument={aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}

I have two questions:

  1. How do I define a default dict in my config and still allow non-default ones to work as expected as command-line arguments?
  2. Is there a way to also enable passing in variable-references to those dicts, for example: argument="{aaa: {bbb: ${variable1}, ccc: 2}, ddd: {eee: 3}}"?
@slerman12 slerman12 added the enhancement Enhanvement request label Mar 14, 2022
@Jasha10 Jasha10 added question Hydra usage question and removed enhancement Enhanvement request labels Mar 14, 2022
@Jasha10
Copy link
Collaborator

Jasha10 commented Mar 14, 2022

@slerman12 usually writing argument=... at the CLI should work if argument is a normal key in your config.
If argument appears in the defaults list, the situation is more complicated.

@slerman12
Copy link
Author

slerman12 commented Mar 14, 2022

I thought I did, but I will try to be more specific for you.

Create a run.py file:

import hydra

@hydra.main(config_path='./', config_name='Config')
def main(args):
    pass

if __name__ == '__main__':
    main()

Then create a Config.yaml file:

argument: {aaa: {bbb: 1, ccc: 2}}

Now run the following:

python run.py argument="{aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}"

And the issue I am having is that I do not know how to standardly pass in a dict argument and have a default one defined in my config yaml at the same time, as above. I get the following error:

Could not override 'argument'. To append to your config use +argument={aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}

Hope that's clearer.

@Jasha10
Copy link
Collaborator

Jasha10 commented Mar 14, 2022

Got it, thanks for the additional details. I am having success with the following:

python run.py +argument="{aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}"

The difference here is that I've used a plus symbol before argument.

To see why this is necessary, take a look at the error message that results from not using a plus:

$ python run.py argument="{aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}"
Could not override 'argument'.
To append to your config use +argument={aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}
Key 'ddd' is not in struct
    full_key: argument.ddd
    object_type=dict

Hydra has been designed to prevent accidentally adding new keys to the config via CLI overrides. Since the key argument.ddd did not appear in the original config, the CLI override above triggers an error. Using the plus symbol + is the prefered way to add a new key to the config using the CLI.

See the Hydra docs on quoted values for more tips on CLI overrides.

@Jasha10
Copy link
Collaborator

Jasha10 commented Mar 14, 2022

Note also that in this case, all three of the following are equivalent:

$ python run.py '+argument.ddd={eee: 3}'
$ python run.py '+argument={aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}'
$ python run.py '+argument={ddd: {eee: 3}}'

The last two are equivalent because the CLI override gets merged into the contents of Config.yaml (using OmegaConf.merge) rather than overwriting Config.yaml.

@Jasha10 Jasha10 closed this as completed Mar 14, 2022
@slerman12
Copy link
Author

Oh... this is not the desired behavior though. I need to be able to override the default argument completely. I don't want to have to ask the user to redeclare +argument={aaa: null} anytime they want a new dictionary altogether. Is there a way to pass in dictionaries like one would other arguments? The above would be fine if not for the equivalence of the last two commands that you pointed out.

I also have a second question, as listed in my original post, but for now I'm mostly concerned about just this basic use case.

@slerman12
Copy link
Author

Can you tell me how Hydra or OnegaConf processes the argument in the case where the Config.yaml file is as follows:

argument: null

In this case I get the desired behavior from running:

python run.py argument="{aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}"

@Jasha10
Copy link
Collaborator

Jasha10 commented Mar 17, 2022

Essentially, Hydra works by calling OmegaConf.merge some number of times.
For the case where initially argument is null, Hydra's config composition process goes like this:

>>> config_yaml = OmegaConf.create("argument: null")
>>> cli_overrides = OmegaConf.create("argument: {aaa: {bbb: 1, ccc: 2}, ddd: {eee: 3}}")
>>> composed_config = OmegaConf.merge(config_yaml, cli_overrides)
>>> print(composed_config)
{'argument': {'aaa': {'bbb': 1, 'ccc': 2}, 'ddd': {'eee': 3}}}

To give another example, here is the case where initially argument (as defined in Config.yaml) is {aaa: {bbb: 1, ccc: 2}} and where '+argument={ddd: {eee: 3}}' is passed at the command line:

>>> config_yaml = OmegaConf.create("argument: {aaa: {bbb: 1, ccc: 2}}")
>>> cli_overrides = OmegaConf.create("argument: {ddd: {eee: 3}}")
>>> composed_config = OmegaConf.merge(config_yaml, cli_overrides)
>>> print(composed_config)
{'argument': {'aaa': {'bbb': 1, 'ccc': 2}, 'ddd': {'eee': 3}}}

I don't want to have to ask the user to redeclare +argument={aaa: null} anytime they want a new dictionary altogether.

Hmm, I see. So you want Config.yaml to specify a default value for argument, but you want this default value only to apply if no modification to argument is made at the command line.

I don't think it's possible to achieve this exact user experience with Hydra presently, though a few things do come to mind which might bring you closer to that target:

  • You could use the config group pattern as in Selecting default configs
  • You could delete the unwanted argument dictionary or the unwanted argument.aaa key using a CLI override. Building on the example from my previous comment:
    Deleting argument.aaa:
$ python run.py '+argument={ddd: {eee: 3}}' '~argument.aaa'
{'argument': {'ddd': {'eee': 3}}}

Deleting argument:

$ python run.py '~argument' '+argument={ddd: {eee: 3}}'
{'argument': {'ddd': {'eee': 3}}}

Note that order of CLI args matters:

$ # Note that order of CLI args matters:
$ python run.py '+argument={ddd: {eee: 3}}' '~argument'
{}

@Jasha10
Copy link
Collaborator

Jasha10 commented Mar 17, 2022

I also have a second question, as listed in my original post, but for now I'm mostly concerned about just this basic use case.

Sorry I missed the second question. Yes, you can pass variable interpolations via the command line. Here is a working example:

# run.py
import hydra

@hydra.main(config_path='./', config_name='Config')
def main(args):
    print(args)
    print(f"{args.argument.aaa=}")

if __name__ == '__main__':
    main()
# Config.yaml
variable: "foo"
argument: null
hydra:
  job:
    chdir: True
$ python run.py '+argument.aaa=${variable}'
{'variable': 'foo', 'argument': {'aaa': '${variable}'}}
args.argument.aaa='foo

@slerman12
Copy link
Author

I just made the default a string

argument: "{aaa: {bbb: 2}}"

and do the following in-code:

if isinstance(argument, str):
       argument = OmegaConf.create(argument)

That way, the user can provide a dict as argument and override the default, or otherwise the default gets translated into a dict anyway.

@greaber
Copy link

greaber commented May 19, 2022

Hi, I'm also trying to figure out how to replace a dict rather than merge. A typical use case is that you have, say, two learning rate schedulers that take different arguments, and you want to override one with another. I guess one solution could be to put the different schedulers into different subconfigs and use the defaults list, and maybe that is fine as a solution, but some people in our organization were wanting to do an override when they hadn't used the defaults list this way. If it seems like a valid use case to you, a possible way to implement it would be that if a dict has the _replace_: true in it then this special _replace_ key is not included in the config but instead is just a directive to the compositor to use replacement rather than merging.

From the command line, deleting and then adding a new dict works, as you suggest @Jasha10. It's just a question of how to do it from YAML.

@Jasha10 Jasha10 reopened this May 26, 2022
@Jasha10
Copy link
Collaborator

Jasha10 commented May 26, 2022

Related is issue #2227.

@Jasha10 Jasha10 changed the title Cannot pass in Dict... Replace dictionary (instead of merging) from command line May 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Hydra usage question
Projects
None yet
Development

No branches or pull requests

3 participants