Includes basic np dlpack function that is tested to work across jax a… - #1067
Conversation
|
@kushalkolar so one issue I've been seeing is that the cuda array interface execution path seems to produce spatially transposed data. See below for a reproducible example. The reason for this is because at the following line, the transpose does different things if the input is a tensor vs. if it is a numpy array. For a tensor, my_tensor.transpose(0, 1) will swap the axes 0 and 1. Otoh, my_numpy_array.transpose(0, 1) will keep the desired shape. Is there any reason we are doing this final transpose step? For an individual ndprocessor, it does not seem necessary. I think there are two options: |
kushalkolar
left a comment
There was a problem hiding this comment.
Where does it say that the from_dlpack requires numpy >= 2.1? Anyways that's fine, v2.1 is 2 years old at this point. pygfx requires min 2.1 anyways too https://github.com/pygfx/pygfx/blob/main/pyproject.toml#L28C1-L28C55
|
|
||
| return cupy.asnumpy(arr) | ||
|
|
||
| data = np.from_dlpack(arr, device='cpu') #This requires numpy >= 2.1 |
There was a problem hiding this comment.
| data = np.from_dlpack(arr, device='cpu') #This requires numpy >= 2.1 | |
| data = np.from_dlpack(arr, device='cpu') |
Annoying that torch behavior is different from numpy. The transpose is necessary by definition, that's how the NDWidget spec is. The specified spatial dims order defines the dim order for the graphic data. Doing the transpose after it's converted to numpy sounds like the way to go. |
|
@kushalkolar Updated this -- so I've delayed the permutation operation until the sliced data is guaranteed to be a numpy array. nd vectors processor, nd image processor, and nd positions processor all subclass ndprocessor, so those are the files where i made this change. One thing to note: with this setup, the spatial functions must be written so they are applied data that has not yet been permuted. for example, if i specify data like this: dims = ['time', 'a', 'b'] The spatial window function received the spatial dimensions in order (a, b) with these changes. I actually think this is ok -- this forces the user to write all their window/spatial function logic using the dimension order specified by dims, but the final order is transposed based on what's in spatial_dims. So we would ofc need to update the specs. I think so long as the user can write their whole pipeline (raw data --> displayed graphic) with a fixed dimension ordering it will be clear. Let me know what you think! |
I think that makes sense. The whole point is that window_funcs and spatial_funcs should be fast and async, and things are naturally async when they remain in torch as long as possible. The Maybe we should rename the arg to |
|
ordered_spatial_dims might be a good one, specifying the two roles of this parameter: (1) it specifies what the spatial dims are and (2) it gives an order to output the spatial dims. thoughts? |
|
@kushalkolar I'll also flag the following potential issue: At this line, a function "squeeze" is used. On a first read, I expected it to fail with torch tensors, since the documented parameters are tensor.squeeze(dim = ...), whereas the code is doing .squeeze(axis =...). Turns out at some point in pytorch the developers allowed axis to make it interoperable with numpy. Anyways it's not clear with other libraries that are compatible with dlpack whether this functionality will break. |
|
where is it documented that torch also accepts |
|
@kushalkolar Example: Looks like under the hood there is some code that parses keywords. |
wow that's hidden deep |
|
@kushalkolar I'm thinking from the standpoint of documentation. We ideally want to be able to tell someone "your dlpack compliant array can work here". Maybe there is not a workaround with the axis squeeze thing though |
|
If it works with torch, cupy and Jax that covers most use cases I'd think? |
|
@kushalkolar Fair enough, we can just make sure to note this in the documentation. Ok so what's left here - just the rename of the spatial_dims param to something else? If you're ok with renaming is ordered_spatial_dims, we'll have to update many of the function signatures in ndwidget, so let's make sure we're both happy with the naming here and I can go ahead and do it. |
|
I added it to the list of renames to do later in bulk on #971 |
|
what's left is the suggestion and updating pyproject.toml and merge latest |
|
@kushalkolar Added the suggested change. Re: pyproject.toml, we are now using np.from_dlpack exclusively to bring gpu arrays to cpu and from there do zero-cost conversion to numpy. So it looks like we don't need to change the pyproject.toml at all. |
For the min version pin? |
…sion across devices
|
@kushalkolar sorry I thought I did that earlier haha. |
| # Axis order of the spatial dimensions to display | ||
| self._spatial_dims_int = tuple( | ||
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | ||
| ) | ||
|
|
There was a problem hiding this comment.
spatial_dims is a mutable property, so this should be a read only property or set in the spatial_dims setter as a private attribute (probably preferable, it doesn't have to be public) and not created once in the constructor
There was a problem hiding this comment.
@kushalkolar good point, updated in latest commit. I also renamed spatial_dims_int to spatial_dims_indices for clarity
| """ | ||
| The ordered sequence of data indices that will be displayed | ||
| """ | ||
| return self._spatial_dims_indices |
There was a problem hiding this comment.
| return self._spatial_dims_indices | |
| return tuple( | |
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | |
| ) |
| @property | ||
| def spatial_dims_indices(self) -> tuple[int, ...]: | ||
| """ | ||
| The ordered sequence of data indices that will be displayed |
There was a problem hiding this comment.
| The ordered sequence of data indices that will be displayed | |
| ordered spatial dim indices that correspond to the named spatial dims |
| ## This is the ordered sequence of data indices that will be displayed | ||
| self._spatial_dims_indices = tuple( | ||
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | ||
| ) | ||
|
|
There was a problem hiding this comment.
| ## This is the ordered sequence of data indices that will be displayed | |
| self._spatial_dims_indices = tuple( | |
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | |
| ) |
| ## This is the ordered sequence of data indices that will be displayed | ||
| self._spatial_dims_indices = tuple( | ||
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | ||
| ) | ||
|
|
||
| @property | ||
| def spatial_dims_indices(self) -> tuple[int, ...]: | ||
| """ | ||
| The ordered sequence of data indices that will be displayed | ||
| """ | ||
| return self._spatial_dims_indices | ||
|
|
There was a problem hiding this comment.
| ## This is the ordered sequence of data indices that will be displayed | |
| self._spatial_dims_indices = tuple( | |
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | |
| ) | |
| @property | |
| def spatial_dims_indices(self) -> tuple[int, ...]: | |
| """ | |
| The ordered sequence of data indices that will be displayed | |
| """ | |
| return self._spatial_dims_indices |
| window_output = await run_in_thread_pool(self._executor, cuda_to_numpy, window_output) | ||
|
|
||
| return window_output | ||
|
|
| ## This is the ordered sequence of data indices that will be displayed | ||
| self._spatial_dims_indices = tuple( | ||
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | ||
| ) | ||
|
|
||
| @property | ||
| def spatial_dims_indices(self) -> tuple[int, ...]: | ||
| """ | ||
| The ordered sequence of data indices that will be displayed | ||
| """ | ||
| return self._spatial_dims_indices | ||
|
|
There was a problem hiding this comment.
| ## This is the ordered sequence of data indices that will be displayed | |
| self._spatial_dims_indices = tuple( | |
| self.spatial_dims.index(d) for d in self.dims if d in self.spatial_dims | |
| ) | |
| @property | |
| def spatial_dims_indices(self) -> tuple[int, ...]: | |
| """ | |
| The ordered sequence of data indices that will be displayed | |
| """ | |
| return self._spatial_dims_indices |
| window_output = await run_in_thread_pool(self._executor, cuda_to_numpy, window_output) | ||
|
|
||
| return window_output | ||
|
|
|
|
||
| return window_output | ||
|
|
||
| return window_output.transpose(*self._spatial_dims_indices) |
There was a problem hiding this comment.
| return window_output.transpose(*self._spatial_dims_indices) | |
| return window_output.transpose(*self.spatial_dims_indices) |
it's a public property use it
…in the base class, updates docs, uses public property in the ndprocessor subclasses
|
@kushalkolar thanks -- all makes sense. made the changes, spatial_dims_indices is computed and set in the public property of the base class now. i was hesitant to do it this way at the start because the computation to define spatial dims indices will now be run at every single getitem. but if the number of dimensions is small, the overhead should never be a problem. |
|
It's probably a nanosecond scale operation. |
Fixes #1063
The numpy dlpack fully sidesteps the need to rely on torch, but it requires numpy > 2.1. I think this is ok, since numpy is now at 2.5, and only users who care about high-performance (i.e. computing on GPUs) will use this execution path anyways.