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

Implements prototype of hydra_zen.like #219

Closed
wants to merge 1 commit into from
Closed

Conversation

rsokl
Copy link
Contributor

@rsokl rsokl commented Feb 4, 2022

Potentially addresses: #205

#219 prototypes a potential solution to the above, that is much more powerful than what I had initially proposed. This is inspired by @jgbos ' out of left field idea 😄

Let's see hydra_zen.like in action:

>>> from hydra_zen import just, instantiate, like, to_yaml
>>> import torch

>>> GenLike = like(torch.Generator)  # 'memorizes' sequence of interactions with `torch.Generator`
>>> seeded = GenLike().manual_seed(42)
>>> seeded   
Like(<class 'torch._C.Generator'>)().manual_seed(42)

>>> Conf = just(seeded)  # creates Hydra-compatible config for performing interactions via instantiation

>>> print(to_yaml(Conf))
_target_: hydra_zen.structured_configs._implementations.make_like
_args_:
- torch._C.Generator
- - - __call__
    - []
    - {}
  - manual_seed
  - - __call__
    - - 42
    - {}

>>> generator = instantiate(Conf)  # calls torch.Generator().manual_seed(42)
<torch._C.Generator at 0x14efb92c770>

what is going on here?

hydra_zen.like(<target>) returns a _Tracker instance that records all subsequent interactions with <target>. Right now, "interactions" are restricted to accessing attributes and calling the target. hydra_zen.just then knows how to convert a _Tracker instance into a valid Hydra config.

Let's use this to make a config that actually returns the manual_seed method of our Generator-instance:

>>> manual_seed_fn = GenLike().manual_seed

>>> instantiate(just(manual_seed_fn))
<function Generator.manual_seed>

Here is a silly example, just to help give more of a feel for this:

>>> listy = like(list)
>>> x = listy([1, 2, 3])
>>> y = x.pop()

# note that `x` *does not* reflect any mutation
>>> instantiate(just(x)), instantiate(just(y))
([1, 2, 3], 3)

Current thoughts on this prototype

One nice thing about like is that looks like a simple pass-through to IDEs, so you get auto-completes, static type checks, etc. as you build your "like expression" prior to creating the config.

Some obvious to-dos would include:

  • Adding as much validation as possible. E.g. GenLike().manual_seedling(42) should raise a runtime error because manual_seedling is not an attribute of Generator.
  • Making the yaml-representation of the just(like(<...>).<...>) expression as clean as possible
    • doing just(like(<target>)(*args, **kwargs)) should reduce down to builds(<target>, *args, **kwargs)
  • Enable users to directly provide like'd objects in their configs without them needing to call just first.

I would definitely like to get feedback on this. I only just thought of it, and will have to think carefully about potential pitfalls as well as about possible use cases that I haven't anticipated yet.

Additional functionality

like could also fulfill the role of the proposed hydra_zen.hydrate

import torch as tr

tr = like(tr)

tr.tensor([1., 2.])  # is equivalent to `like(tr.tensor)([1.0, 2.0])`

@addisonklinke
Copy link

This sounds intriguing, but I think I need a bit more time to wrap my head around exactly what's going on (it probably doesn't help that I haven't played around with just either). Based on this part of your example

generator = instantiate(Conf)  # calls torch.Generator().manual_seed(42)

I'm understanding that the combination of like and just enables instantiate to both initialize a torch.Generator instance and call the instance method manual_seed in a single line. Whereas previously this pattern would've required multiple steps

>>> GenConf = builds(torch.Generator)
>>> print(to_yaml(GenConf))
_target_: torch.Generator

>>> generator = instantiate(GenConf)
>>> generator.manual_seed(42)  # Can't happen until the above line runs

So in a way, it's like the recursive instantiation added in Hydra 1.1 except instead of recursing into the init parameters of the target object, we're recursing into the target object's methods and attributes once it's already instantiated. Is that the advantage you're looking to demonstrate, or am I missing something?

Could you elaborate on how this fulfills the role of hydrate? In reading your original proposal, I thought that hydrate would take an imported module and return its corresponding Hydra config node (which then could be stored in the ConfigStore and exposed with the application's CLI interface). The torch example above makes it seem like there's a different design goal - that we should first hydrate the objects we'll use and then subsequent interactions are memorized. If that is the intent, is it supposed to tie into the CLI interface eventually?

Thanks in advance for the clarification! I'll be interested to understand better

@rsokl
Copy link
Contributor Author

rsokl commented Apr 10, 2022

I'm understanding that the combination of like and just enables instantiate to both initialize a torch.Generator instance and call the instance method manual_seed in a single line. Whereas previously this pattern would've required multiple steps.

That is correct. Without like, there is no easy way to write a config that, when instantiated, initializes a class and then calls a method.

In general, given

conf = hydra_zen.like(obj)

like will record all subsequent "interactions" with obj, and will replay those interactions upon instantiation. E.g.

conf = conf("hello")
conf = conf.a
conf = conf.b

out = instantiate(conf)  # performs: `obj("hello").a.b`

So the advantage that I am demonstrating is that like would enable us to express configs that are otherwise impossible / very cumbersome to write. And this could also be used to "record interactions" with a module object.

Could you elaborate on how this fulfills the role of hydrate? In reading your #93 (comment), I thought that hydrate would take an imported module and return its corresponding Hydra config node (which then could be stored in the ConfigStore and exposed with the application's CLI interface).

I'm glad you asked this; the way you posed the question helped me realize that there is indeed a significant discrepancy between the proposed hydrate vs using like on a module object. You are correct that hydrate could return a Hydra config node for an entire module. like, on the other hand, would only help to make config-creation code look more like native Python.

I see now how hydrate is closer to a direct / generalized replacement to projects like hydra-torch, and that like would not facilitate this. I'll open an issue dedicated to hydrate to see if we can move forward with it.

@rsokl
Copy link
Contributor Author

rsokl commented Apr 10, 2022

Closing this for now. I think it is a neat idea, but if the main application of like is "instantiate + method call", then it is a bit overkill.

@rsokl rsokl closed this Apr 10, 2022
@rsokl
Copy link
Contributor Author

rsokl commented Apr 10, 2022

@addisonklinke see #256 for a new discussion of hydrate

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

Successfully merging this pull request may close these issues.

2 participants