Skip to content

Conversation

@jakirkham
Copy link
Member

From profiling the Scheduler, we know that there are a series of transition_* functions that take the bulk of the time. These functions and functions they call primarily work with the various *State objects. In these functions they spend time accessing their attributes, hashing them, etc. At the Python level we have optimized these operations about as much as we can. Already we use __slots__ on the *State objects to speed up access. Also we pre-compute their hash and store it during construction to speed up later hashing of these objects. That said, working with the *State objects still takes a fair bit of time.

Thus it seems reasonable to try and optimize these objects with Cython. Analogous to PR ( #4283 ) and PR ( #4289 ) we try to leverage Cython's pure Python mode to minimize disruption. Just to get a taste for these kinds of changes, we pick the simplest *State type to start, ClientState. This only has a handful of attributes and while it does see a fair bit of usage it is not as heavily used as WorkerState or TaskState.

In order to benefit from annotating classes, we need to use one feature from Cython, cclass, to decorate the class in question. This has the same effect as using cdef class in Cython. We then define and type attributes using type annotations, which Cython converts into C level attributes in a struct with very fast access. By default these act like cdef attributes, so are only visible in C/Cython code when compiled (there is no affect in pure Python). We can make them visible in Python by either using Cython's declare, a .pxd file, or rolling our own @properties (which is also Cython's declare does for us). Have forgone this atm as it doesn't appear ClientState is used externally. This also helped with the next part. Additionally have typed a few method variables used by ClientState.

When trying to running with these changes to ClientState, we found any spots where a ClientState was assigned to a variable and annotated that variable accordingly. This was done by picking up on convention cs typically meant ClientState. Sometimes c meant ClientState, but those have been changed to cs for clarity. Finally in running the profile anything we might have missed was easy to pick up as there would be a ClientState attribute access error (since attributes are not visible to Python currently). So any other variable needing typing was caught and fixed that way.

After getting a sense of what kinds of issues would crop up and how to fix them, this went pretty smoothly. The code changes also remained fairly light. Some changes to ClientState. A few imports with fallbacks. Otherwise just typing a ClientState variable when it came up. This should help us prepare for the other *State objects as well. Would be interested to get others takes on these changes. 🙂

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020

From my perspective this looks pretty lightweight. I'm pleasantly surprised :)

@jakirkham
Copy link
Member Author

Thanks! This is basically done on my end if we are ok with it. Wasn't sure to what extent the *State objects (and their attributes specifically) are expected to be visible outside the Scheduler (if at all). If that's not needed, will just fill in the missing docstring attributes and mark it ready.

@jakirkham
Copy link
Member Author

cc @quasiben (for vis)

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020

Wasn't sure to what extent the *State objects (and their attributes specifically) are expected to be visible outside the Scheduler

When you say "outside the scheduler" do you mean

  • Outside of the scheduler process? Like "do client processes ever consume TaskState objects?" If so, the answer is "probably no"
  • Outside of the Scheduler class? Then yes, the dashboard is a good example of another system in Dask that uses internal Scheduler state

@jakirkham
Copy link
Member Author

Sorry I mean does anything outside of this scheduler.py file do from ... import ClientState and then do something with a ClientState's attributes. Like does cs.client_key (or any other attribute) get used outside of this file.

This applies to other *State objects as well. So am curious both in terms of this specific type and then generally the others.

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020 via email

@jakirkham
Copy link
Member Author

Ok thanks for that data point. We might want to adjust this approach for them.

To the second part, is ClientState used outside of this file (or even distributed)?

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020

In general scheduler state is used outside of scheduler.py. Scheduler logic is spread to other files like stealing.py or locks.py intentionally to try to keep the core somewhat lightweight. Is it possible to draw the Cython box a big bigger around multiple files?

It is probably doable to keep things contained within this repository though

@jakirkham
Copy link
Member Author

Ok thanks that helps. I'll keep that in mind going forward. Also will push some changes to address that here.

We could use Cython in more places. I don't think we need to yet, but it could happen. Stealing is a good example of where we may consider this eventually.

Independently we might want to start tracking what is intended to be Cythonized and how to enable it. Initially was thinking we could add an optional flag to setup.py to Cythonize a few files. If not passed, nothing would happen. If passed, it would Cythonize a few files (with a fallback if Cython isn't around).

Sure. Was more wondering whether we need to be cognizant of breakage. We are not entering that territory yet (and may not at all). Just trying to understand the constraints a bit more since we are discussing them.

@jakirkham
Copy link
Member Author

Have pushed a couple of commits to add propertys for these attributes. This is one way to make sure they are visible to Python when Cythonizing this file. Though they are not the only way as noted in the OP (quoted below)

We can make them visible in Python by either using Cython's declare, a .pxd file, or rolling our own @properties (which is also Cython's declare does for us).

The advantage of using propertys is the code still looks pretty Pythonic. Also it ensures if the _ attributes are used when Cythonized, we get an error if the variable using those attributes was not typed. IOW instead of potentially writing poor perf code, we just get an error. The downside is this a bit more boilerplate. Also there is a little more overhead experienced by anything using the propertys in pure Python vs. the _ attributes, which are in the slots.

If we prefer something more succinct while still remaining rather Pythonic, Cython's declare is a good option. The downside is it will mask inefficient attribute access in Cython as opposed to erroring as we have now with propertys.

Using .pxd would move the declarative bits of Cython (cdef class and cdef/cpdef) into a separate file. On the one hand, this might avoid adding some typing bits to the .py file. However it increases the maintenance burden as we now have 2 files to keep up-to-date and in-sync. So seems to be the most error prone in the long run. It also raises the question of whether we should just scrap pure Python mode and use a .pyx file instead of a .py file.

Out of all of the options slightly prefer the property approach, but could go with the declare approach if we prefer something succinct. Would like to avoid the .pxd approach as this seems hard to maintain and moves us in a direction I don't think we want to go.

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020

This seems like a sensible path forward. I'll be curious to see if this has an impact when we hit TaskState and WorkerState, which I suspect will be more significant and more work.

@jakirkham
Copy link
Member Author

For comparison have included both approaches in this PR's commits. Here is the @property approach ( a0e0c1c ) and here is the declare approach ( 9000361 ).

@jakirkham jakirkham changed the title [WIP] Annotate ClientState for Cythonization Annotate ClientState for Cythonization Dec 1, 2020
@jakirkham jakirkham marked this pull request as ready for review December 1, 2020 22:27
@jakirkham
Copy link
Member Author

Ok this is ready for review. Please let me know if you have any other questions 🙂

@mrocklin
Copy link
Member

mrocklin commented Dec 1, 2020

This looks good to me. Thanks @jakirkham

@jakirkham jakirkham merged commit a192b22 into dask:master Dec 2, 2020
@jakirkham jakirkham deleted the cy_cs branch December 2, 2020 01:46
except ImportError:
from ctypes import (
c_double as double,
c_ssize_t as Py_hash_t,
Copy link
Member Author

Choose a reason for hiding this comment

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

For context, this is reasonable as Python does the same thing itself. Though it shouldn't matter whether we get this right in the pure Python case as this doesn't have any effect on the implementation besides documentation. Just noting this to provide background as to how this was decided.

mrocklin pushed a commit that referenced this pull request Dec 4, 2020
Analogous to PR ( #4290 ) except instead of `ClientState`, this is annotating `WorkerState` and usages thereof. This is a bit longer simply because of how many attributes `WorkerState` has. That said, this just uses the same strategy here as was used for `ClientState`.

* Coerce `bandwidth` to `float`

Typically `bandwidth` seems to be a `float` elsewhere in the code. So
coerce it to a `float` here as well to align with that practice.

* Use `ws` variable name for `WorkerState` objects

* Name `WorkerState` variable distinctly in closure

Avoids confusion with the variable name used outside this closure. Also
bypasses any potential leakage from the external scope.

* Assign selected `WorkerState` to variable

* Annotate `WorkerState` for Cythonization

* Annotate all `WorkerState` variables

* Prefix `WorkerState` attributes with `_`

Since these are effectively private given they are only available in
Cython/C, go ahead and mark them as such. We can then add Python
`@property`s on top of them to handle Python access to the original
attribute names with minimal disruption.

* Add Python-level `property`s for attributes

* Run `black`

* Add some `property.setter`s

Includes `setter`s for `occupancy` and `nbytes`.

* Create `list` from generator

This ensures Cython still uses `WorkerState` to annotate the variable
iterated over. Otherwise it constructs a generator with its own scope
where this is ignored.

* Relax `_name` to `object`

Apparently `_name` is sometimes an `int`. So relax typing around it to
just an `object`.
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