-
Notifications
You must be signed in to change notification settings - Fork 47
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
"Hooks" for ShootFromSnapshotsSimulation (i.e., committor) #755
"Hooks" for ShootFromSnapshotsSimulation (i.e., committor) #755
Conversation
This will enable analysis, even with parallel hooks
I have a question concerning the PS: I have a working implementation of PathSampling with hooks @ https://github.com/hejung/openpathsampling/tree/pathsamp_hooks , I'll still have to test it extensively, I have however used it without problems in the past month. |
Found it, it happens in the |
Some of the design of the hooks is based on the idea that eventually we'll use them as part of our parallelization schemes, which will use a task-based approach (probably using Hopefully clearer example: imagine the analysis (after_step) is a histogram, but that adding to the histogram takes a long time (so you do the analysis in parallel with the simulation). You can add step As an aside, If you're interested in the broader ideas around task-based parallelization, I'd encourage you to apply to an upcoming E-CAM ESDW, which will focus on that topic: https://www.cecam.org/workshop-1650.html |
This is (FINALLY!) ready for review. @hejung, obviously you've already worked with the code, so I'd especially appreciate if you can check that the description in the docs is an accurate representation. |
I will give it a thorough look this weekend. :) |
LGTM! The only thing I still have trouble wrapping my head around is the hook_state dictionary, the one that is returned by the class HookWithState(PathSimulatorHook):
def after_step(self, sim, step_number, step_info, state, results,
hook_state):
if hook_state is not None:
own_state = hook_state[self.after_step]
else:
own_state = None
# here we would do stuff and update own_state
# to finally return it
return own_state Edit: Check for None in the code snippet as I realized that the hook_state is None if no hook has returned a state 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.
The main idea here is great! I don't agree fully with some of the design choices/restrictions (thinking of what I might need for #989), but this looks great overall
One general point:
what is the difference between beta
and experimental
again (I have the feeling this has been explained before, but I can't remember)
So first a warning, I have not looked at #911 so I don't know if my concerns are caught/fixed in there. Also, this is done with:
If you can give an eye to #755 and #911 with a mental focus on "will this work if the hooks use Dask?"
I do not have a lot of recent experience with Dask but I assumed this is going to either use delayed
or futures
.
The two main points:
run_hooks
should have the option to carry it's state around.- all hook functions should accept **kwargs for them to be mixed with hooks that might require more/different kwargs
A couple assumptions:
My comments in run_hook
assumes that hook_name_state
will be a dict with {hookfunc_object: future}
and that dask
is smart enough that it does not require the whole dictionary before calculating a hook
that does not need it. If the second case is not true, it can be easily fixed by pop
ing the dict and only feed a hook it's own return
Other **kwargs
than the default: In the future, some hooks might need a client
to fire_and_forget
, while others (classical ones) don't. Another option to solve this issue is to do some fancy inspection, but that is more work with little in extra return, I guess.
I now made a couple comments that are hook_state = self.run_hooks...
, if you don't agree with that, at least catch the (possible) return with an underscore _ = self.run
self.output_stream.write("\n") | ||
n_snapshots = len(self.initial_snapshots) | ||
hook_state = None | ||
self.run_hooks('before_simulation', sim=self) |
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.
hook_states should be carried around
self.run_hooks('before_simulation', sim=self) | |
hook_state = self.run_hooks('before_simulation', sim=self, hook_name_state=hook_state) |
|
||
def run_hooks(self, hook_name, **kwargs): | ||
"""Run the hooks for the given ``hook_name``""" | ||
hook_name_state = {} |
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 it might be good to (have the option) keep a constant state here, so change this into
hook_name_state = {} | |
hook_name_state = kwargs.get('hook_name_state') or {} |
if hook_name_state != {}: | ||
return hook_name_state |
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 should always return, as each of the hook states should be stored with hook.func
as key, so these should never update hook keys that shouldn't. (It also helps if you one hook state needs the result from another hook, such as before_step
and after_step
for step timings)
if hook_name_state != {}: | |
return hook_name_state | |
return hook_name_state |
if result is not None: | ||
hook_name_state.update({hook: result}) |
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 should always update, None
is a decent return as well
if result is not None: | |
hook_name_state.update({hook: result}) | |
hook_name_state.update({hook: result}) |
openpathsampling/beta/hooks.py
Outdated
implemented_for = ['before_simulation', 'before_step', 'after_step', | ||
'after_simulation'] | ||
|
||
def before_simulation(self, sim): |
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 another hook implements more kwargs here it should not error out
def before_simulation(self, sim): | |
def before_simulation(self, sim, **kwargs): |
openpathsampling/beta/hooks.py
Outdated
self.output_stream = output_stream | ||
self.allow_refresh = allow_refresh | ||
|
||
def before_simulation(self, sim): |
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.
def before_simulation(self, sim): | |
def before_simulation(self, sim, **kwargs): |
if self.allow_refresh is None: | ||
self.allow_refresh = sim.allow_refresh | ||
|
||
def before_step(self, sim, step_number, step_info, state): |
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.
def before_step(self, sim, step_number, step_info, state): | |
def before_step(self, sim, step_number, step_info, state, **kwargs): |
self.run_hooks('before_step', sim=self, | ||
step_number=step_number, step_info=step_info, | ||
state=start_snap) |
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.
hook_states should be carried around
self.run_hooks('before_step', sim=self, | |
step_number=step_number, step_info=step_info, | |
state=start_snap) | |
hook_state = self.run_hooks('before_step', sim=self, | |
step_number=step_number, | |
step_info=step_info, | |
state=start_snap, | |
hook_name_state=hook_state) |
hook_state=hook_state | ||
) |
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.
hook_state=hook_state | |
) | |
hook_state=hook_state, | |
hook_name_state = hook_state | |
) |
snap_num += 1 | ||
|
||
self.run_hooks('after_simulation', sim=self) |
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.
self.run_hooks('after_simulation', sim=self) | |
hook_state = self.run_hooks('after_simulation', sim=self, hook_name_state) |
Thanks a lot for taking a look at this one; it definitely helps to have another pair of eyes on it, especially with trying to plan this to be compatible with Dask. I'm going to take some time to think about the bigger points, but I'll answer a couple easy things now:
I'm curious why this would make it harder? I think that in #911 @hejung made it so that all the timing info is within the hook that reports that info. In principle, you could even use a totally different hook for estimating time remaining -- say you just wanted to use something like tqdm instead of our more verbose output. You could create a (I mean, it does mean that the work in #989 needs to be redone in this context, but the hope is that it's easier to do this way than it was the other way!)
The fundamental difference is that
The premise was that |
main point if you want to use hooks with dask: from dask.distributed import Client
client = Client()
class A():
def __init__(self, a):
self.a = a
def set_a(self, b):
self.a = b
#return self
def get_a(self):
return self.a
a = A(12)
b = client.submit(a.set_a, 13)
c = client.submit(a.get_a)
c.result() this returns class A():
def __init__(self, a):
self.a = a
def set_a(self, b):
self.a = b
return self
def get_a(self):
return self.a
a = A(12)
a = client.submit(a.set_a, 13)
c = client.submit(a.get_a)
c.result() errors out with AttributeError Traceback (most recent call last)
<ipython-input-15-19e9324dce45> in <module>
13
14 a = client.submit(a.set_a, 13)
---> 15 c = client.submit(a.get_a)
16 c.result()
AttributeError: 'Future' object has no attribute 'get_a' Via import dask
class A():
def __init__(self, a):
self.a = a
@dask.delayed
def set_a(self, b):
self.a = b
#return self
@dask.delayed
def get_a(self):
return self.a
a = A(12)
_ = a.set_a(13)
c = a.get_a()
c.compute() but this does (but requires a double import dask
class A():
def __init__(self, a):
self.a = a
@dask.delayed
def set_a(self, b):
self.a = b
return self
@dask.delayed
def get_a(self):
return self.a
a = A(12)
a = a.set_a(13)
c = a.get_a()
# Needs 2 computes
c.compute().compute() |
I agree with @sroet that the hook state is confusing (I think I already wrote that somewhere). @sroet : Concerning the dask example: I am not sure I get the issue. How would dask know that you want to use the result of the first operation on the class if you do not pass the result of the first call to the second one (as in the last example that works)? But I also have not thought about dask for a while... |
It doesn't, which was the point I was trying to make.
So the info is stored inside the hook (as an attribute to Now it might help to share the 'dummy' hook that I think about for most of these discussions:
The point that I was trying to make in my review and the second comment is that this data needs to be returned by the |
First, note that the def after_step(*args, **kwargs):
hook_state = kwargs['hook_state']
a = self.client.submit(self.calculate_a, hook_state)
hook_state = self.client.submit(self.do_something_with, a, self.const)
return hook_state Running the hook itself will stay in the local process, but the hook can certainly launch things with Dask (note: we'll probably actually be launching with a wrapper around Dask, not actual As to the specific case, this is the purpose of The I can tell that not having everything return There's certainly no reason for I'm definitely concerned about how allowing more stages (especially |
Aha... thanks for the clear example case. So here's the thing about this example: it relies on a hidden variable that is dependent on the environment (i.e., the time). This is part of why is doesn't actually make sense in parallel (we know nothing about the machines each task will be executed on -- even aside from questions of execution timing, they may not have synchronized clocks!) I suspect this is always the case for any example where you need to pass information from |
Yeah, you are correct in that it is not a good example to be done in parallel
thanks for the more detailed idea here. Would it then still not matter that the data is shuttled around in hook_state? As the hooks themself would have to make sure to grab only their own blocking data from the hook state dictionary? Say if you have 2 independent
I do understand that |
I made a few small changes to the API here based on some comments from @sroet and @hejung: 1.
For now, I'm going to stick to @hejung and @sroet, please take another look and let me know if there's anything that really needs to be addressed before a beta version of this enters a release. @hejung : Your work in #911 and after will probably require changes based on the API changes listed above (but you can see from d59b306 that it isn't much work to change those). |
I don't see anything particularly pressing. |
I'll leave this open for another 48 hours to provide time for any other comments (particularly from @hejung?), although I'm looking forward to getting this into the code. I will merge this no earlier than Fri 25 Jun 18:00 GMT (14:00 my local). |
Sorry for the late reply! |
Thanks @hejung! I expect that you'll need to update your PRs to reflect changes here once this is merged (merging it now). Then I'll try to review those so that they can be included in the 1.5 release!
Also would require changes to the
I'm not entirely sure what you mean here -- for the specific issue of timing, a few things:
In any case, if anyone else has interest in mixing this with Dask, please do play with it and let me know of any issues! After this is merged, I'll do work on that in #1001. |
Sorry for beeing unlcear about the timing I meant. |
The challenge is that this kind of output doesn't actually make sense when running in parallel. This was one of the main reasons I started on these hooks -- if the main task is run by Dask, then the timing we were using would report that the entire simulation was completed as soon as the tasks had been scheduled! (So, in a fraction of a second, regardless of how large the simulation.) The original goal of this PR was to separate progress reporting from the actual loop for running the simulation. The default hooks to use for progress reporting will depend on the "scheduler" being used. The current behavior will be wrapped by a TLDR: what you have there is fine! 😊 The end goal is that the user would have to specifically request those hooks if running a parallel simulation (and yes, the output would be nonsense, but if the user explicitly asks for nonsense....)
Thanks! No huge rush, but as soon as we finish those I'll start working on the 1.5.0 release (which will require a little care because very old PRs, like this one and like the spring shooting PR, are sometimes overlooked by my tool to automate writing release notes). |
This begins to introduce the idea of "hooks" (which @jhprinz has previously called "reporters") to our main simulation objects. The goals here are to:
In this PR, I add the basic functions for hooks, and replace code in the
ShootFromSnapshotsSimulation
with hook-based versions. The default behavior will be indistinguishable from the original, but now there are a lot more possibilities.This builds off of #754, and should only be merged after that.
ShootFromSnapshotsSimulation