-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
bugfix: Resolve interpolation bug with Hydra #5406
Conversation
@@ -0,0 +1,17 @@ | |||
# Copyright The PyTorch Lightning team. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hall it rather go to tests/data/conf
? (not important)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simpler as hydra need relative path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are multiple initialize methods you can use that might make more sense to your use case.
Take a look at the docs [here[(https://hydra.cc/docs/experimental/compose_api#initialization-methods).
# only collect variables that appear in the signature | ||
local_args = {k: local_vars[k] for k in init_parameters.keys()} | ||
local_args.update(local_args.get(kwargs_var, {})) | ||
local_args = {k: v for k, v in local_args.items() if k not in exclude_argnames} | ||
local_args = apply_to_collection(local_args, DictConfig, resolve_dict_config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this convert everything to Hydra even you pass pure dict?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be triggered only on DictConfig.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so you say whenever you have Hydra installed, you will convert any input to DictConfig
, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it will convert a DictConfig with interpolation to a resolved one.
Codecov Report
@@ Coverage Diff @@
## master #5406 +/- ##
======================================
Coverage 93% 93%
======================================
Files 134 135 +1
Lines 9996 10011 +15
======================================
+ Hits 9313 9328 +15
Misses 683 683 |
if OMEGACONF_AVAILABLE: | ||
local_args = apply_to_collection(local_args, DictConfig, resolve_dict_config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- if apply_to_collection is recursive, this is exponentially expensive because OmegaConf.to_container is also recursive.
- can you explain why you need resolve all interpolations eagerly? there are subtle implications to doing it which can be surprising.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interpolation is broken, and need to be resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apply_to_collection
is recursive. However, it will stop and apply resolve_dict_config
as soon as it encounter a DictConfig
object and return. In the example, it will apply it directly on cfg
as it is the only element in local_args
.
If you have a better solution, please share the idea or make a PR 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is subtle implications to doing it which can be surprising.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interpolation is broken, and need to be resolved.
Why is it broken? OmegaConf containers has a reference to their parent. The interpolation should work:
from omegaconf import *
cfg = OmegaConf.create({"foo": {"bar": "${zoo}"}, "zoo": 20})
assert cfg.foo.bar == 20
foo = cfg.foo
assert foo.bar == 20
Something was probably done along the way to break it.
I think it's better to identify what is the root cause before fixing this.
If you have a better fix, please submit a PR.
You seem a bit defensive, If you don't need my advice you are welcome to ignore it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @omry
ahaha I apologise, I didn't mean to sound defensive and didn't express myself properly.
I meant The interpolation feature is broken, and we need to resolve it
on the Lightning side. We are doing something wrong ! 👍
Your advices are always welcome !!!
And yes, you are entirely right. @SeanNaren spotted the root cause of the problem.
We were off in our loop. Surprising our tests didn't catch this ...
Just pushed @SeanNaren fix and test pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accepted, I think it was just a small misunderstanding.
When I asked "Why is it broken?" I did not think you meant that it's broken in OmegaConf, but rather that it's not working correctly. My question was more along the lines of "Why is it not working correctly? there has to be something going on here".
Anyway, water under the bridge. Great to see you improving Hydra support in PL!
As @omry said this flow works: from omegaconf import *
cfg = OmegaConf.create({"foo": {"bar": "${zoo}"}, "zoo": 20})
assert cfg.foo.bar == 20
foo = cfg.foo
assert foo.bar == 20
OmegaConf.save(cfg, 'output', resolve=True) So there is no issue with Hydra, I think this is the issue: for v in hparams.values():
if OmegaConf.is_config(v):
with fs.open(config_yaml, "w", encoding="utf-8") as fp:
OmegaConf.save(OmegaConf.create(hparams), fp, resolve=True) This should be: for v in hparams.values():
if OmegaConf.is_config(v):
with fs.open(config_yaml, "w", encoding="utf-8") as fp:
OmegaConf.save(OmegaConf.create(v), fp, resolve=True) #hparams should be v EDIT: let's keep the test though, this is a crucial test to have for saving! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not an expert on Hydra but LGTM 👍
pytorch_lightning/core/saving.py
Outdated
for v in hparams.values(): | ||
if OmegaConf.is_config(v): | ||
with fs.open(config_yaml, "w", encoding="utf-8") as fp: | ||
OmegaConf.save(OmegaConf.create(hparams), fp, resolve=True) | ||
OmegaConf.save(OmegaConf.create(v), fp, resolve=True) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This snippet does not make a whole lot of sense to me.
It seems to be iterating the hparams, and saving the first value that is an OmegConf config, and ignore the rest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Borda Any idea ?
@@ -0,0 +1,17 @@ | |||
# Copyright The PyTorch Lightning team. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are multiple initialize methods you can use that might make more sense to your use case.
Take a look at the docs [here[(https://hydra.cc/docs/experimental/compose_api#initialization-methods).
tests/models/test_hparams.py
Outdated
@pytest.mark.skipif(not HYDRA_AVAILABLE, reason="Hydra is not available") | ||
def test_model_save_hyper_parameters_interpolation_with_hydra(tmpdir): | ||
|
||
initialize(config_path="conf") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialize() is changing the global state.
Use this to avoid that.
with initialize():
...
Please go over the relevant page, there have been many changes in Hydra 1.0 you may not be aware of yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Omry. I will adress those comments tomorrow 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Omry. How can I save / load a collection of normal dict, list and DictConfig, ListConfig ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried to save without interpolation and reload with it, but I didn't manage to make it work ? Would you mind having a look ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Input:
args_0 = compose(config_name="config") # As expected, it works for this one.
args_1 = {"cfg": compose(config_name="config")}
args_2 = [compose(config_name="config")]
kwarg_1 = {"cfg": [compose(config_name="config")]}
model = TestHydraModel(args_0, args_1, args_2, kwarg_1=kwarg_1)
For saving:
if OMEGACONF_AVAILABLE:
with fs.open(config_yaml, "w", encoding="utf-8") as fp:
OmegaConf.save(hparams, fp)
args_0:
log: ${training.log}
training:
log: Something
args_1:
cfg:
log: ${training.log}
training:
log: Something
args_2:
- log: ${training.log}
training:
log: Something
kwarg_1:
cfg:
- log: ${training.log}
training:
log: Something
to reload (This won't work as it breaks the original structure of the DictConfigs)
if OMEGACONF_AVAILABLE:
with fs.open(config_yaml, "r") as fp:
hparams = yaml.full_load(fp)
for k, v in hparams.items():
hparams[k] = OmegaConf.create(v)
return hparams
How should I save and load to preserve interpolation ? Or should I resolve everything so I don't have pb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current solution for saving
if OMEGACONF_AVAILABLE:
def resolve_dict_config(data):
return OmegaConf.to_container(data, resolve=True)
hparams = apply_to_collection(hparams, DictConfig, resolve_dict_config)
with fs.open(config_yaml, "w", encoding="utf-8") as fp:
OmegaConf.save(hparams, fp)
return
args_0:
log: Something
training:
log: Something
args_1:
cfg:
log: Something
training:
log: Something
args_2:
- log: Something
training:
log: Something
kwarg_1:
cfg:
- log: Something
training:
log: Something
…PyTorchLightning/pytorch-lightning into bugfix/5384_hydra_interpolation
Hello @tchaton! Thanks for updating this PR. There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻 Comment last updated at 2021-01-09 13:12:48 UTC |
if OmegaConf.is_config(v): | ||
with fs.open(config_yaml, "w", encoding="utf-8") as fp: | ||
OmegaConf.save(OmegaConf.create(hparams), fp, resolve=True) | ||
# deepcopy: hparams from user shouldn't be resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit lost as to what the logic here is. Let's say I have multiple DictConfigs stored in hparams like below
{
'arg1': DictConfig(...),
'arg2': DictConfig(...)
}
With the logic you're proposing, we're going to resolve all DictConfig's eagerly (so that all interpolation's are resolved) and then save the hparams file.
The previous logic would only save the config if it found a DictConfig object, or in my logic the first conf that it finds, which is definitely unsuitable.
So to be clear, the issue is that hparams is a dictionary of DictConfigs and there is no save function that auto resolves them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@omry It was auto-merge, but let s come with a better solution.
@tchaton Here is a snippet showing the behavior: from omegaconf import *
cfg = OmegaConf.create(
{
"a": {"b": 10},
"aa": {
"bb": "${a.b}",
},
}
)
def foo(**kwargs):
print(type(kwargs))
for v in kwargs.values():
print(type(v))
OmegaConf.save(kwargs, "/tmp/foo.yaml")
foo(**cfg)
cfg2 = OmegaConf.load("/tmp/foo.yaml")
print("cfg1", cfg)
print("cfg2", cfg2)
assert cfg == cfg2
assert cfg.aa.bb == cfg2.aa.bb == 10
print(OmegaConf.to_yaml(cfg2, resolve=True)) Output:
OmegaConf.save can operate on plain dicts. (internally it converts them to a DictConfig prior to converting them to yaml Unless there is a compelling reason, I wouldn't resolve when saving. You are changing the semantics of the config. I think it's reasonable to expect users that have interpolations in the configs to also use OmegaConf/Hydra to load the config later. If you still want to resolve as you save, just add resolve=True to the save call as before. |
args_0 = compose(config_name="config") | ||
args_1 = {"cfg": compose(config_name="config")} | ||
args_2 = [compose(config_name="config")] | ||
kwarg_1 = {"cfg": [compose(config_name="config")]} | ||
model = TestHydraModel(args_0, args_1, args_2, kwarg_1=kwarg_1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this trying to emulate the following?
cfg = compose(config_name="config")
model = TestHydraModel(**cfg)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function call itself does this:
class MyModel(pl.LightningModule):
def __init__(self,
cfg_1: DictConfig,
cfg_2: Dictconfig):
self.save_hyperparameters() # Autosaves your parameters for loading your model in the future
# after training your model
MyModel.load_from_checkpoint('model.ckpt') # load's from checkpoint, uses the saved input args
In the test he's emulating the above I think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the logic flow that is currently implemented is:
- Loop through the hparams dictionary
- If DictConfig, resolve interpolation and create new dict container
- After the loop, save all the resolved DictConfigs
There is some weirdness however. I'm hoping we can do something a bit cleaner:
- If OmegaConf is available, we rely on OmegaConf entirely to save the hparams dictionary without resolving anything. @omry we should expect OmegaConf.save to work with primitive types/anything that can be pickle-able right? How about having two different DictConfigs within the dictionary structure?
- If not, default save functionality
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If OmegaConf is available, we rely on OmegaConf entirely to save the hparams dictionary without resolving anything. @omry we should expect OmegaConf.save to work with primitive types/anything that can be pickle-able right?
No, OmegaConf does not support arbitrary objects in the config.
How about having two different DictConfigs within the dictionary structure?
That should be fine.
@@ -23,36 +23,16 @@ | |||
|
|||
from pytorch_lightning.utilities.apply_func import move_data_to_device | |||
from pytorch_lightning.utilities.distributed import AllGatherGrad, rank_zero_info, rank_zero_only, rank_zero_warn | |||
from pytorch_lightning.utilities.package_utils import _module_available |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, this was moved already to #5256 along with all COnstants so we can import them anywhere without cyclic imports...
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
* resolve bug * Apply suggestions from code review * resolve package import * resolve import * update on comments * update on comments * hacky fix * update * exit * update * to_container * typo * resolve import * update * resolve pep8 Co-authored-by: Jirka Borovec <[email protected]> Co-authored-by: Sean Naren <[email protected]> (cherry picked from commit bb5031b)
What does this PR do?
Resolves DictConfig in
save_hyperparameters
function.Fixes #5384 <- this links related issue to this PR
Before submitting
PR review
Anyone in the community is free to review the PR once the tests have passed.
Before you start reviewing make sure you have read Review guidelines. In short, see the following bullet-list:
Did you have fun?
Make sure you had fun coding 🙃