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

Refactor SchedulerState from Scheduler #4365

Merged
merged 44 commits into from
Jan 22, 2021
Merged

Conversation

jakirkham
Copy link
Member

Requires PR ( #4343 )

This refactors out the SchedulerState as @cclass including info about endpoints and tasks. Also includes the transition_* functions. This should make it easier to more thoroughly Cythonize these components.

@jakirkham
Copy link
Member Author

cc @mrocklin @quasiben

@jakirkham jakirkham force-pushed the ref_trans2 branch 2 times, most recently from 6b7882d to fc807ca Compare December 15, 2020 19:25
@jakirkham
Copy link
Member Author

jakirkham commented Dec 16, 2020

Should add that after doing this, I can see a path to typing all attributes of the Scheduler and using them throughout. This would speed up attribute access and would also give Cython the opportunity to use targeted Python C APIs with those attributes (assuming they have a builtin type more specific than object). This would involve refactoring these into a base class, say _Scheduler, and decorating it with @cclass, then going through and casting all usages of self to a new variable, say parent, typed as _Scheduler much as we have done here with SchedulerState.

@jakirkham jakirkham requested a review from mrocklin December 16, 2020 19:00
@jakirkham jakirkham force-pushed the ref_trans2 branch 2 times, most recently from a71b4d3 to 4e5b41c Compare December 16, 2020 20:50
@mrocklin
Copy link
Member

mrocklin commented Dec 16, 2020 via email

@jakirkham
Copy link
Member Author

No worries. Just wanted to make sure you are aware of this. Mostly just interested in your general high-level thoughts as opposed to a detailed review at this stage.

@jakirkham
Copy link
Member Author

Some of the transition and supporting methods take **kwargs, but don't seem to do anything with them. As this hinders optimizing these methods, I've just dropped them. Please let me know if that is issue and they are needed by something else somewhere so we can devise alternative strategies (if needed).

@jakirkham
Copy link
Member Author

Hmm...seems those **kwargs exist to just ignore some arguments (see traceback below)? Confused as to why they are showing up in the first place.

distributed.core - ERROR - transition_processing_memory() got an unexpected keyword argument 'status'
Traceback (most recent call last):
  File "/Users/jkirkham/Developer/distributed/distributed/core.py", line 592, in handle_stream
    handler(**merge(extra, msg))
  File "distributed/scheduler.py", line 4707, in distributed.scheduler.Scheduler.handle_task_finished
  File "distributed/scheduler.py", line 4115, in distributed.scheduler.Scheduler.stimulus_task_finished
  File "distributed/scheduler.py", line 6079, in distributed.scheduler.Scheduler.transition
  File "distributed/scheduler.py", line 6015, in distributed.scheduler.Scheduler.transition
  File "distributed/scheduler.py", line 2022, in distributed.scheduler.SchedulerState.transition_processing_memory
TypeError: transition_processing_memory() got an unexpected keyword argument 'status'

@jakirkham
Copy link
Member Author

jakirkham commented Dec 16, 2020

After some toying around, I think these are coming from here for finished tasks:

d = {
"op": "task-finished",
"status": "OK",
"key": ts.key,
"nbytes": ts.nbytes,
"thread": self.threads.get(ts.key),
"type": typ_serialized,
"typename": typename(typ),
"metadata": ts.metadata,
}

Also from errored tasks this appears to come from here:

d = {
"op": "task-erred",
"status": "error",
"key": ts.key,
"thread": self.threads.get(ts.key),
"exception": ts.exception,
"traceback": ts.traceback,
}

Perhaps the stimulus_* functions should be grabbing these extra arguments and handling them? Alternatively the transition_* functions getting these could list them explicitly (though they still are not used there). Or better still worker messages could skip passing info that is simply not needed. For now am just grabbing them in transition until we come to some agreement here on what to do.

@jakirkham
Copy link
Member Author

jakirkham commented Dec 17, 2020

Here's the Scheduler call graph with these changes so far. Some functions (like _add_to_memory) are being handled with a virtual table, which we don't need as they won't be inherited. So we might break them out into separate functions to make them independent of the class (and thus the virtual table).

Scheduler call graph

Edit: We are spending less time in transition_waiting_processing (even relative to other transitions), which seems like a good thing. Also we are not spending as much time queueing stealable tasks. I'm wondering if part of the issue we saw with the Scheduler was due to it getting "antsy" and so trying things like letting tasks get stolen, which just created churn in turn slowing things down. Maybe we are starting to see a resolution to this behavior now?

@jakirkham jakirkham force-pushed the ref_trans2 branch 3 times, most recently from a109dbb to fc628fe Compare December 18, 2020 03:07
@jakirkham
Copy link
Member Author

Yeah messages including unused arguments and extra arguments being passed to transition_ functions seems like the main issue we need to address here (details in this comment #4365 (comment) and above it). Once we figure that out it should be more straightforward to optimize these.

@jakirkham
Copy link
Member Author

There's other odd things like nbytes being set to None (-1 would be better for typing). worker being optional where a function absolutely needs it. And so on. Though these are second order problems we can fix if we fix the first order ones (above).

Copy link
Contributor

@madsbk madsbk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I like the refactoring; encapsulate the state of the scheduler in a different class makes a lot of sense. Actually, I think we should go even further and encapsulate all states of the scheduler and tasks with clearly defined invariants but that is future work :)

I have read through all of the changes and as far as I can see, it looks good but because of the amount of changes I am not confident that it is bug free :)

As a side note I agree, we should get rid of all the unused **kwargs in a future PR. It makes a harder to follow variables through the code.

@@ -230,6 +231,8 @@ def set_thread_ident():

self.__stopped = False

super().__init__(**kwargs)
Copy link
Contributor

@madsbk madsbk Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused, why do we need kwargs? And call to super?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fair. It's a little confusing.

The call to super is needed to call the parent constructors of Scheduler. These are ServerNode and SchedulerState. We also want to affect when these get called relative to other things in this constructor (as this constructor needs some of the attributes of the parent classes to be setup in later steps).

As Scheduler inherits from two base classes, we need to make sure that arguments for those constructors get passed through and picked up by the right parent class. There are some more details in this SO answer that may help clarify this further.

Comment on lines 1780 to 1786
except Exception as e:
logger.exception(e)
if LOG_PDB:
import pdb

##################
# Administration #
##################
pdb.set_trace()
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we implement all the exception logging using a context manager as described in https://stackoverflow.com/a/28158006

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree that something like that makes sense. Alternatively we might want to push this into transition where all of these methods are called. Held off on that for now as I didn't want to disrupt the code too much, but agree this is something we should follow up on.

@jakirkham
Copy link
Member Author

Thanks for looking through this Mads! 😄

Overall I like the refactoring; encapsulate the state of the scheduler in a different class makes a lot of sense. Actually, I think we should go even further and encapsulate all states of the scheduler and tasks with clearly defined invariants but that is future work :)

Thanks Mads! 😄 Yeah for sure. I think this opens a lot of doors for us in terms of future optimizations. Just tried to find the right balance where we do enough to get some benefit without pushing everything into one PR.

I have read through all of the changes and as far as I can see, it looks good but because of the amount of changes I am not confident that it is bug free :)

Completely understand that. Have really been relying on the CI and tests to catch any issues. Also have been doing some profiling locally, which has helped as well. Plus regex has been my friend (most changes represent a common pattern).

As a side note I agree, we should get rid of all the unused **kwargs in a future PR. It makes a harder to follow variables through the code.

Agreed. I tried to get rid of them in this PR, but it wound up being a little too complicated and touching too much other code. Also as we are still calling the transition_* functions with their Python APIs, we don't get much benefit yet of adding C APIs. Though this may change with future PRs.

@mrocklin
Copy link
Member

Actually, I think we should go even further and encapsulate all states of the scheduler and tasks with clearly defined invariants but that is future work :)

Thanks Mads! smile Yeah for sure. I think this opens a lot of doors for us in terms of future optimizations. Just tried to find the right balance where we do enough to get some benefit without pushing everything into one PR.

Yeah, I have the same question. What do we include in SchedulerState and what do we include in Scheduler? I'm totally fine doing this in the future, but some methods, like new_task or transition or remove_worker I expected to be primarily state-focused while methods like stimulus_task_finished or handle_worker I would expect to remain in the Scheduler.

@jakirkham
Copy link
Member Author

jakirkham commented Jan 14, 2021

Yeah that makes sense. Generally have been thinking anything that relates to the management of the task graph or transformations of it seem like good candidates for SchedulerState. Methods involved in communication or general upkeep/maintenance of the Scheduler seem better there. IOW I think we have the same idea in mind that's just broadly what I've been thinking. Agree that these things can be tackled in future work. Am mainly trying to provide a framework and initial first step here to allow us to dive into that work (possibly from multiple directions 😉).

@jakirkham
Copy link
Member Author

Refactored out a few more minor utility functions used by the transition_*s. AFAICT that's all of them. Please let us know if there's anything else needed here 🙂

@jakirkham
Copy link
Member Author

FWIW here's the profile using Ben's shuffle benchmark with these changes.

prof_476 pstat dot

@lr4d
Copy link
Contributor

lr4d commented Jan 21, 2021

Just wondering, is it necessary that Scheduler inherits from SchedulerState? From a logical level, it seems like it would make more sense if Scheduler had one or mulitple attribute/property provided by SchedulerState

@jakirkham
Copy link
Member Author

After having done the work it's actually not that clear to me that it would be that doable. SchedulerState and Scheduler overlap on a fair number of attributes that they both need. Subclassing makes more sense as they both access these as opposed to letting the Scheduler muck with SchedulerState's internal state, which may actually make it harder to refactor out more functionality later. As the goal was to turn some of the Scheduler's methods into C functions using Cython, this also was easier to do with minimal duplication or disruption via subclassing. Also some of the methods like worker_objective seem to be designed to be overridden in subclasses, which we continue to allow with this structure. Maybe over time as the boundaries between these two become more clearly defined we can revisit, but at least today I'm not sure this is doable.

Comment on lines +5861 to +5864
for worker, msg in worker_msgs.items():
self.worker_send(worker, msg)
for client, msg in client_msgs.items():
self.client_send(client, msg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not help any, but any thoughts on pulling these out of the transition function into transitions ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like they're a decent chunk of the current time spent

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an interesting suggestion and agree it's worth trying. That said, I feel like this PR has gotten quite long and am worried that has slowed down the reviewing of it. Instead would propose we focus on getting this in. That would make it easier to make smaller more focused PRs on moving communication further out and consolidating messages further as well as allowing us to more effectively Cythonize any remaining functions we still see popping up in the profiles. Thoughts? 🙂

(Should add if we want to do this after the patch release tomorrow that also makes sense to me)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was mostly done in PR ( #4451 ). Handling the remaining ones in PR ( #4526 )

@mrocklin mrocklin merged commit a3b1ab1 into dask:master Jan 22, 2021
@mrocklin
Copy link
Member

OK, we're good to go here. I expect that over time we'll move more things over, but I agree with @jakirkham that what is here now is a good firm base.

@jakirkham jakirkham deleted the ref_trans2 branch January 22, 2021 21:12
@jakirkham
Copy link
Member Author

Thanks Matt! 😄

@jakirkham jakirkham mentioned this pull request Feb 20, 2021
3 tasks
@jakirkham
Copy link
Member Author

Yeah, I have the same question. What do we include in SchedulerState and what do we include in Scheduler? I'm totally fine doing this in the future, but some methods, like new_task or transition or remove_worker I expected to be primarily state-focused while methods like stimulus_task_finished or handle_worker I would expect to remain in the Scheduler.

FWIW moving new_task over in PR ( #4527 ). This was pretty easy to do.

I think transition should eventually be doable. Would be good to more strongly type the arguments to this function first though.

Looked at remove_worker and some pieces may be amenable to moving, but it is also an async method with some IO bits. So we might need to split it into a helper method that lives in SchedulerState and leave the rest in Scheduler. Will need to think about this more.

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.

5 participants