Skip to content

NAP 4: asynchronous slicing#4892

Merged
sofroniewn merged 29 commits intonapari:mainfrom
andy-sweet:async-slice-nap
Aug 3, 2022
Merged

NAP 4: asynchronous slicing#4892
sofroniewn merged 29 commits intonapari:mainfrom
andy-sweet:async-slice-nap

Conversation

@andy-sweet
Copy link
Copy Markdown
Member

Description

This opens NAP 4, which proposes to make slicing in napari asynchronous. Thanks to for @junxini, @perlman, and @kcpevey for helping to write this! And others for any preliminary feedback!

I should have probably opened this PR before the document grew quite so large, so apologies for that! Some people may have seen the Motivation and scope section before (which hasn't changed much at all in a few months), but now there is a description of how slicing currently works in napari in Related work and an actual technical proposal (in Detailed description) about how to achieve some of those goals.

I think the proposal here is coherent, but it is not intended to be complete or final. I definitely expect some feedback, criticism, and alternative ideas. Happy to incorporate those before or after this gets merged.

I added some diagrams, so it's probably easiest to read the rendered document on this PR: https://github.com/andy-sweet/napari/blob/55b025a6013f90914f15bca7030aca98ed542b40/docs/naps/4-async-slicing.md

If you want to see what some of the code may look like, how this works, and how/where it breaks, check out the prototype PR: andy-sweet#21

References

Tracking issue: #4795

andy-sweet and others added 29 commits June 23, 2022 09:03
* Add some notes on shapes layer state.

* Update docs/naps/4-async-slicing.md

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* Update docs/naps/4-async-slicing.md

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* Update docs/naps/4-async-slicing.md

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* Update docs/naps/4-async-slicing.md

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* Update docs/naps/4-async-slicing.md

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* Cleanup of notes on Shapes layer.

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>
* add_vector_info

* Apply suggestions from code review

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>

* add author

Co-authored-by: Kim Pevey <kpevey@quansight.com>
Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>
This reverts commit e92077c.
Co-authored-by: Nicholas Sofroniew <sofroniewn@gmail.com>
@github-actions github-actions bot added the documentation Documentation content and tooling label Aug 1, 2022
Copy link
Copy Markdown
Collaborator

@Czaki Czaki left a comment

Choose a reason for hiding this comment

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

I like this. I will go back after some time after a rethink.

Comment on lines +485 to +488
The main goal of this project is to perform slicing asynchronously, so it's
natural that we might break anyone that was depending on slicing being synchronous.
At a minimum, we must provide a public way to achieve the same fundamental goals,
such as connecting to the `slice_ready` event.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Writing a project that uses napari to create animation having only slice_ready will be much harder than for the current state as it will require a switch from some form of only loop to object keeping state to pass around waiting for the event. It may be nice to have fully sync mode.

@alisterburt I think that it may touch your work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

napari animation is a great call out, we'd have to make that respond to some event after rendering is done to make sure we captured the screenshot at the right time. we might need an additional event then or a force sync mode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great point @Czaki !

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you mentioned this verbally, but thanks for writing it down. I think it would be a useful exercise to try to see how hard it is to make napari-animation work with the current prototype in: andy-sweet#21

I'm planning to do that, but won't complain if someone else gets to it before me!

Copy link
Copy Markdown
Contributor

@alisterburt alisterburt left a comment

Choose a reason for hiding this comment

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

Absolutely incredible work @andy-sweet

I'm approving this - I think it should be merged ASAP and discussion should follow in a subsequent PR

Comment on lines +485 to +488
The main goal of this project is to perform slicing asynchronously, so it's
natural that we might break anyone that was depending on slicing being synchronous.
At a minimum, we must provide a public way to achieve the same fundamental goals,
such as connecting to the `slice_ready` event.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great point @Czaki !

Copy link
Copy Markdown
Contributor

@brisvag brisvag left a comment

Choose a reason for hiding this comment

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

Amazing work @andy-sweet et Al.! :D This is super thorough, I'm sure it will be a breeze to implement with all these details already ironed out ;)

I have a few comments below, but as @alisterburt said, we should merge this ASAP, so feel free to ignore them for now and address them in a second PR.

Comment on lines +223 to +224
- `_mesh_vertices`, output from `generate_vector_meshes`.
- `_mesh_triangles`, output from `generate_vector_meshes`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note that this is getting moved to the vispy side in #4846.

- The implementation details for some layer/data types might be complex (e.g. multi-scale image), but the high level logic should be simple.
- P1. Move the slice state off the layer, so that its attributes only represent the whole data.
- Layer may still have a function to get a slice.
- May need alternatives to access currently private state, though doesn't necessarily need to be in the Layer (e.g. a plugin with an ND layer, that gets interaction data from 3D visualization , needs some way to get that data back to ND).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you clarify what you mean byt this? That a LayerSliceResponse should know where it lives in the world coordinates?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I think that's right. I believe this originally came up in a discussion with @alisterburt.

The response knows where it lives in the scene (i.e. the subspace of the world that is being visualized), but slicing is a lossy operation that means the output loses the info to get back to ND world coordinate system.

I guess if the response also stores at least some parts of the request (e.g. the bounding box that defines which region which are slicing), then that might be enough because the non-visualized dimensions are the sliced dimensions (either at an exact coordinate or spanning a range like thick slices).

Comment on lines +305 to +307
At this point `data` should be fast to access either from RAM (e.g. as a numpy
array) or VRAM (e.g. as a CuPy array), so that it can be consumed on the main
thread without blocking other operations for long.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this requires some extra thought; I'm trying to imagine how this all would work out in a possible future where we have DataSources that maintain data loaded onto the GPU so that we can for example share visualizations between canvases, or between layers, or allow faster slicing in the shader. Indeed, I'm working towards this in PRs such as vispy/vispy#2350 and vispy/vispy#2351. Is there a way we can take advantage of this async machinery, while also allowing "pre-loading" data onto the GPU and do the slicing in the shader?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Slicing in the shader is super interesting. I suspect that will need to be in addition and complementary to this PR, so you could say have a 4D timeseries dataset that you sliced to get a smaller single 3D timepoint that you "pre-loaded" (as you say above) and then sliced in the GPU to get a specific 2D plane. Does it sound like we're on the same page here @brisvag?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yup :) But I think the biggest advantage would be for multicanvas in order to display the same data multiple times using the same texture; imagine having a sizeable 3D volume on which you performed feature picking in 10 different ways, and you'd like to see them side by side. You could have a grid of 10 canvases with linked cameras and zoom in onto details and compare, but without sharing texture you'd be decuplicating the vram usage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This might be an oversimplification, but as long as the thing that's rendering the slice (e.g. vispy) has enough to be able to do that "quickly", then I think the job of slicing as described here is done. In the case described in the comments above (i.e. 4D data, same underlying 3D slice with different features presented in multiple canvases), I'd imagine that slicing just wouldn't do much after the 3D sliced data has been read for the first time. Not quite sure what data would be in that case, which is one motivation to keep the typing of that as vague as possible for now (e.g. Any) and maybe keep things temporarily private.

Comment on lines +508 to +509
In this proposal, the slicing thread waits for slices of all layers to be ready before
it emits the `slice_ready` event. There are a few reasons for that.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had another comment about this, but I see now you addressed it already. I think this would be a big improvement, especially because some slicing operations are CPU-intensive (Points) rather than I/O. But if it makes it easier to keep this for a later data, that's ok!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Points slicing could be seedup (by the cost of memory), outside this NAP as we use suboptimal way of storage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, definitely! some thought already went into this from @kevinyamauchi :)

Comment on lines +576 to +579
- Should `Dims.current_step` represent the last slice position request or the last slice response?
- With sync slicing, there is no distinction.
- If it represents the last slice response, then what should we connect the sliders to?
- Similar question for `corner_pixels` and others.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it should be the last request. Maybe a way of preventing issues is to have a temporary NotLoaded sentinel value in places where things are out of date and users might access them. Or introduce a lock so that if one tries to access anything out-of-sync, they'll have to wait.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 to last request

Copy link
Copy Markdown
Member Author

@andy-sweet andy-sweet Aug 3, 2022

Choose a reason for hiding this comment

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

There seems to be some general consensus around last request. Some other related comments about this are that we consider storing the last response equivalent of this and other similar state, though maybe not publicly.


- Should we maintain the synchronous behavior of `refresh` and `set_view_slice`?

- Should we invert the current design and submit async tasks within each vispy layer?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would prevent us from easily re-using this logic with another backend in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 vispy independent

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This would prevent us from easily re-using this logic with another backend in the future.

Not necessarily. Most of the slicing logic proposed here is defined in the layers (in particular Layer._get_slice) and in LayerSlicer and this idea wouldn't change that. I haven't done a great job of explaining what the alternative is, so I'll probably update this text with some pseudocode and list it as an alternative considered.

Copy link
Copy Markdown
Contributor

@sofroniewn sofroniewn left a comment

Choose a reason for hiding this comment

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

Looking great - I added a couple of my most major comments. Happy to have this merge and discussion continue elsewhere too

Comment on lines +280 to +282
point: Tuple[float, ...]
dims_displayed: Tuple[int, ...]
dims_not_displayed: Tuple[int, ...]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if we should create a dedicated Slice object that includes all the includes all the encapsulated dims and camera info to determine what part of the data to show. Slice could then be an additional argument to a slicing function. Thoughts @jni - I know you don't want a proliferation of classes, but to me that seems like a helpful one

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I like this idea and I think @nclack has a similar one: #4892 (comment)

Would be great to fold a lot of base layer's request state into a world bounding box.


Second, we introduce a response to encapsulate the output of slicing.

```python
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we not have one LayerSlice object instead of both LayerSliceRequest and LayerSliceResponse? Especially if we have encapsulated all the dims/ camera info into a single Slice object then I think all the other attributes of the two objects are the same - though I guess there could be times when they are not.

I'm trying to think from the perspective of someone adding a new layer and trying to make sure that the number of new objects added is as small as possible - ideally two, one for the full layer and then one for the slice data.

I tried exploring something like that here with the datasource idea from @d-v-b and quite liked it's feel https://github.com/napari/napari/pull/1908/files#diff-22adcfbe4884971c3fdb13eae4a24a4683d3365fb437b02df0e9f147f2c854dbR8-R29 I called it PointsData and had a "full" version of it actually live on the layer too.

Curious what @jni @tlambert03 @nclack think

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something just feels a bit off to me with having the request & response in a single object. I'd like both objects to be immutable, but I don't have a strong case for it.

Separately, what would it look like to to send LayerSliceResponse to something other than vispy visual, e.g., a web-based widget?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Separately, what would it look like to to send LayerSliceResponse to something other than vispy visual, e.g., a web-based widget?

That's a great question! Would be awesome to try as a proof of concept

Something just feels a bit off to me with having the request & response in a single object. I'd like both objects to be immutable, but I don't have a strong case for it.

That's fair - I just want to minimize burden on folks contributing new layers. I could be they just have to add one LayerDataSource object and we turn it into request and response

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Definitely want to avoid any kind of class explosion, but I also don't love the idea of defining one type for two different purposes. I wonder if frozen dataclasses are less of a concern than more complex classes (i.e. with methods that have side effects). I'd also be curious to know if unpacking the request into keyword arguments for Layer.get_slice is another option that might make things easier to understand.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd also be curious to know if unpacking the request into keyword arguments for Layer.get_slice is another option that might make things easier to understand.

i actually liked this less, to me it became too many kwargs

I also don't love the idea of defining one type for two different purposes

ok all this is fair - we can start with two classes - one request & one response. always possible to refactor later and at least being explicit has advantage of being clear

I still like the idea of a dedicated Slice class that contains all the Camera & Dims info needed to make a slice.

Comment on lines +288 to +289
In the future, the point may become a bounding box that includes the current
field of view.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like bbox much better. point could just get encoded in the (affine) transform, and the extent information from bbox would be very valuable.

I guess the proposed design is there because that fits the current use, hence "in the future". But imo the future is now :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I would love to collapse a few of the existing request fields into something bounding_box_world. The main reason I didn't do that yet is laziness - partly because some of the existing slicing code uses point by name and type. But I agree with you that it is probably the right time to make the switch, so I think this would be a great idea when we start writing code to be merged into napari.

Copy link
Copy Markdown
Member Author

@andy-sweet andy-sweet Aug 3, 2022

Choose a reason for hiding this comment

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

One thing that makes me hesitant is around dimension correspondence. Ideally, the bounding box would have named dimensions and we could do the correspondence between its and a layer's dimensions by name. I think we should still be able to work with the numpy broadcasting correspondence we have now, but I haven't thought that through yet.

Copy link
Copy Markdown
Contributor

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

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

classic Andy here :) Just a beautiful document and a great, clear proposal. I'm all for merging this whenever. I'd like a little more time to digest the specifics in the proposal, but after a first pass, I didn't see anything I didn't like. Thanks @andy-sweet!

@sofroniewn
Copy link
Copy Markdown
Contributor

Given the consensus here around merging the NAP sooner rather than later, I will go ahead and merge this. We can continue discussion in a variety of forums though. Great work @andy-sweet on submitting your first NAP 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation content and tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants