Skip to content

Initial GPU support #1967

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

Merged
merged 51 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
22a5807
Initial implementation of a GPU version of Buffer and NDBuffer
akshaysubr Jun 14, 2024
d8cc79f
Adding cupy as an optional dependency
akshaysubr Jun 14, 2024
4d2b8c7
Adding GPU prototype test
akshaysubr Jun 14, 2024
36b1cb2
Adding GPU memory store implementation
akshaysubr Jun 14, 2024
04001b4
Addressing comments
akshaysubr Jun 17, 2024
74a13c4
Making GpuMemoryStore tests conditional on cupy being available
akshaysubr Jun 17, 2024
bdc0a24
Adding test checking that existing host memory codecs use the gpu_buf…
akshaysubr Jun 18, 2024
d900aa3
Reducing code and docs duplication
akshaysubr Jun 28, 2024
0eca795
Formatting
akshaysubr Jun 28, 2024
d9ed6c4
Fixing silent rebase conflicts
akshaysubr Jun 28, 2024
5405e38
Reducing code duplication in GpuMemoryStore
akshaysubr Jun 28, 2024
2858701
Refactoring to an abstract Buffer class and concrete CPU and GPU impl…
akshaysubr Jul 8, 2024
4e18098
Templating store tests on Buffer type
akshaysubr Jul 8, 2024
35948d4
Changing imports to prevent circular dependencies
akshaysubr Jul 8, 2024
bd2a20b
Fixing unsafe calls to Buffer abstract methods in metadata.py and gro…
akshaysubr Jul 15, 2024
828401f
Preventing calls to abstract classmethods of Buffer and NDBuffer
akshaysubr Jul 15, 2024
02a6e9d
Fixing some more unsafe usage of Buffer abstract class
akshaysubr Aug 9, 2024
ff40d3c
Initial testing with cirun based GPU CI
akshaysubr Aug 9, 2024
e5cfd2f
Reverting to basic ubuntu machine image on GCP
akshaysubr Aug 9, 2024
d473a3d
Switching to cuda image from the docker registry
akshaysubr Aug 9, 2024
2a2e399
Revert "Switching to cuda image from the docker registry"
akshaysubr Aug 9, 2024
b89ab9a
Revert "Reverting to basic ubuntu machine image on GCP"
akshaysubr Aug 9, 2024
c5a387d
Revert "Initial testing with cirun based GPU CI"
akshaysubr Aug 9, 2024
72d172d
Adding pytest mark for GPU tests
akshaysubr Aug 9, 2024
3db61bd
Updating GPU memory store test with gpu mark
akshaysubr Aug 9, 2024
425c3f8
Adding GPU workflow that only runs GPU tests
akshaysubr Aug 9, 2024
75b0ad7
First pass at fixing merge conflicts, still many changes needed
akshaysubr Aug 20, 2024
c8c7e6d
Formatting
akshaysubr Aug 21, 2024
25a67ca
Fixing mypy errors in buffer code
akshaysubr Aug 23, 2024
ce7f5e2
Merging again with v3
akshaysubr Aug 23, 2024
ac061d9
Fixing errors in test_buffer.py
akshaysubr Aug 23, 2024
523d8d5
Fixing errors in test_buffer.py
akshaysubr Aug 23, 2024
b559ee4
Fixing store test errors
akshaysubr Aug 23, 2024
26a74f4
Fixing stateful store test
akshaysubr Aug 23, 2024
7307833
Fixing config test
akshaysubr Aug 23, 2024
f6fddd9
Fixing group tests
akshaysubr Aug 23, 2024
2b1fe14
Fixing indexing tests
akshaysubr Aug 23, 2024
abd135f
Manually installing cupy in the GPU workflow
akshaysubr Aug 23, 2024
1db58e7
Ablating GPU test matrix and adding gpu optional dependencies to the …
akshaysubr Aug 24, 2024
296bd02
Adding some more logging to debug GPU test failures
akshaysubr Aug 26, 2024
b33c887
Adding GA step to install the CUDA toolkit
akshaysubr Aug 26, 2024
c894f60
Merging with v3
akshaysubr Aug 26, 2024
e0da0fb
Adding a separate gputest hatch environment to simplify GPU testing
akshaysubr Aug 27, 2024
07277af
Fixing error in cuda-toolkit step
akshaysubr Aug 28, 2024
6e49e85
Downgrading to CUDA 12.4.1 in cuda-toolkit GA
akshaysubr Aug 28, 2024
02c319c
Trying manual install of the CUDA toolkit
akshaysubr Aug 29, 2024
e82ddc1
Updating environment variables with CUDA installation
akshaysubr Aug 29, 2024
7854ce9
Removing PATH env and setting it only through GITHUB_PATH
akshaysubr Aug 29, 2024
9688ad6
Merge branch 'v3' into gpu-buffer-implementation
akshaysubr Aug 29, 2024
3852c9f
Fixing issue from merge conflict
akshaysubr Aug 29, 2024
2e8069c
Merge branch 'v3' into gpu-buffer-implementation
d-v-b Aug 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jupyter = [
'ipytree>=0.2.2',
'ipywidgets>=8.0.0',
]
gpu = [
"cupy>=13.0.0",
]
docs = [
'sphinx',
'sphinx-autobuild>=2021.3.14',
Expand Down Expand Up @@ -220,4 +223,5 @@ filterwarnings = [
"error:::zarr.*",
"ignore:PY_SSIZE_T_CLEAN will be required.*:DeprecationWarning",
"ignore:The loop argument is deprecated since Python 3.8.*:DeprecationWarning",
"ignore:Creating a zarr.buffer.gpu.*:UserWarning",
]
28 changes: 23 additions & 5 deletions src/zarr/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
from zarr.attributes import Attributes
from zarr.buffer import BufferPrototype, NDArrayLike, NDBuffer, default_buffer_prototype
from zarr.chunk_grids import RegularChunkGrid
from zarr.chunk_key_encodings import ChunkKeyEncoding, DefaultChunkKeyEncoding, V2ChunkKeyEncoding
from zarr.chunk_key_encodings import (
ChunkKeyEncoding,
DefaultChunkKeyEncoding,
V2ChunkKeyEncoding,
)
from zarr.codecs import BytesCodec
from zarr.codecs._v2 import V2Compressor, V2Filters
from zarr.codecs.pipeline import BatchedCodecPipeline
Expand Down Expand Up @@ -76,7 +80,9 @@ def parse_array_metadata(data: Any) -> ArrayV2Metadata | ArrayV3Metadata:
raise TypeError


def create_codec_pipeline(metadata: ArrayV2Metadata | ArrayV3Metadata) -> BatchedCodecPipeline:
def create_codec_pipeline(
metadata: ArrayV2Metadata | ArrayV3Metadata,
) -> BatchedCodecPipeline:
if isinstance(metadata, ArrayV3Metadata):
return BatchedCodecPipeline.from_list(metadata.codecs)
elif isinstance(metadata, ArrayV2Metadata):
Expand Down Expand Up @@ -474,7 +480,10 @@ async def _get_selection(
return out_buffer.as_ndarray_like()

async def getitem(
self, selection: BasicSelection, *, prototype: BufferPrototype = default_buffer_prototype
self,
selection: BasicSelection,
*,
prototype: BufferPrototype = default_buffer_prototype,
) -> NDArrayLike:
indexer = BasicIndexer(
selection,
Expand Down Expand Up @@ -502,15 +511,24 @@ async def _set_selection(

# check value shape
if np.isscalar(value):
value = np.asanyarray(value, dtype=self.metadata.dtype)
array_like = prototype.buffer.create_zero_length().as_array_like()
if isinstance(array_like, np._typing._SupportsArrayFunc):
# TODO: need to handle array types that don't support __array_function__
# like PyTorch and JAX
array_like_ = cast(np._typing._SupportsArrayFunc, array_like)
value = np.asanyarray(value, dtype=self.metadata.dtype, like=array_like_)
else:
if not hasattr(value, "shape"):
value = np.asarray(value, self.metadata.dtype)
# assert (
# value.shape == indexer.shape
# ), f"shape of value doesn't match indexer shape. Expected {indexer.shape}, got {value.shape}"
if not hasattr(value, "dtype") or value.dtype.name != self.metadata.dtype.name:
value = np.array(value, dtype=self.metadata.dtype, order="A")
if hasattr(value, "astype"):
# Handle things that are already NDArrayLike more efficiently
value = value.astype(dtype=self.metadata.dtype, order="A")
else:
value = np.array(value, dtype=self.metadata.dtype, order="A")
value = cast(NDArrayLike, value)
# We accept any ndarray like object from the user and convert it
# to a NDBuffer (or subclass). From this point onwards, we only pass
Expand Down
17 changes: 17 additions & 0 deletions src/zarr/buffer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from zarr.buffer.core import (
ArrayLike,
Buffer,
BufferPrototype,
NDArrayLike,
NDBuffer,
)
from zarr.buffer.cpu import default_buffer_prototype

__all__ = [
"ArrayLike",
"Buffer",
"NDArrayLike",
"NDBuffer",
"BufferPrototype",
"default_buffer_prototype",
]
84 changes: 28 additions & 56 deletions src/zarr/buffer.py → src/zarr/buffer/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import sys
from collections.abc import Callable, Iterable, Sequence
from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -106,7 +107,7 @@ def check_item_key_is_1d_contiguous(key: Any) -> None:
raise ValueError("slice must be contiguous")


class Buffer:
class Buffer(ABC):
"""A flat contiguous memory block

We use Buffer throughout Zarr to represent a contiguous block of memory.
Expand Down Expand Up @@ -135,14 +136,15 @@ def __init__(self, array_like: ArrayLike):
self._data = array_like

@classmethod
@abstractmethod
def create_zero_length(cls) -> Self:
"""Create an empty buffer with length zero

Returns
-------
New empty 0-length buffer
"""
return cls(np.array([], dtype="b"))
...

@classmethod
def from_array_like(cls, array_like: ArrayLike) -> Self:
Expand All @@ -160,6 +162,7 @@ def from_array_like(cls, array_like: ArrayLike) -> Self:
return cls(array_like)

@classmethod
@abstractmethod
def from_buffer(cls, buffer: Buffer) -> Self:
"""Create a new buffer of an existing Buffer

Expand All @@ -179,10 +182,16 @@ def from_buffer(cls, buffer: Buffer) -> Self:
Returns
-------
A new buffer representing the content of the input buffer

Note
----
Subclasses of `Buffer` must override this method to implement
more optimal conversions that avoid copies where possible
"""
return cls.from_array_like(buffer.as_array_like())
...

@classmethod
@abstractmethod
Copy link
Contributor

Choose a reason for hiding this comment

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

test failures are due to this method becoming abstract, while it still gets called in to_buffer_dict in metadata.py. Two things should change to fix this: first, I think maybe the order of the decorators here should be flipped to ensure that Buffer.from_bytes can't be called without an exception, and second metadata.py needs to not be calling any Buffer methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure we can flip the order of decorators here based on this snippet from the abstractmethod docs:

When abstractmethod() is applied in combination with other method descriptors, it should be applied as the innermost decorator

I agree though that metadata.py shouldn't be calling any Buffer methods and should instead be calling prototype.buffer methods or default_buffer_prototype.buffer methods. Is there a preference between propagating a prototype argument up the call stack or just using default_buffer_prototype since these to_bytes calls are not in the critical performance path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved the CI issues by tracking all calls to abstract classmethods of Buffer and NDBuffer. Here are the main ones and the current solution:

  • metadata.py: switch to using default_buffer_prototype.buffer
  • group.py: switch to using default_buffer_prototype.buffer
  • sharding.py: switch to using default_buffer_prototype.buffer
  • codecs/_v2.py: switch to using cpu.Buffer and cpu.NDBuffer since these are all explicitly CPU only.

def from_bytes(cls, bytes_like: BytesLike) -> Self:
"""Create a new buffer of a bytes-like object (host memory)

Expand All @@ -195,7 +204,7 @@ def from_bytes(cls, bytes_like: BytesLike) -> Self:
-------
New buffer representing `bytes_like`
"""
return cls.from_array_like(np.frombuffer(bytes_like, dtype="b"))
...

def as_array_like(self) -> ArrayLike:
"""Returns the underlying array (host or device memory) of this buffer
Expand All @@ -208,6 +217,7 @@ def as_array_like(self) -> ArrayLike:
"""
return self._data

@abstractmethod
def as_numpy_array(self) -> npt.NDArray[Any]:
"""Returns the buffer as a NumPy array (host memory).

Expand All @@ -219,7 +229,7 @@ def as_numpy_array(self) -> npt.NDArray[Any]:
-------
NumPy array of this buffer (might be a data copy)
"""
return np.asanyarray(self._data)
...

def to_bytes(self) -> bytes:
"""Returns the buffer as `bytes` (host memory).
Expand All @@ -246,14 +256,10 @@ def __setitem__(self, key: slice, value: Any) -> None:
def __len__(self) -> int:
return self._data.size

@abstractmethod
def __add__(self, other: Buffer) -> Self:
"""Concatenate two buffers"""

other_array = other.as_array_like()
assert other_array.dtype == np.dtype("b")
return self.__class__(
np.concatenate((np.asanyarray(self._data), np.asanyarray(other_array)))
)
...


class NDBuffer:
Expand Down Expand Up @@ -287,6 +293,7 @@ def __init__(self, array: NDArrayLike):
self._data = array

@classmethod
@abstractmethod
def create(
cls,
*,
Expand Down Expand Up @@ -318,10 +325,7 @@ def create(
A subclass can overwrite this method to create a ndarray-like object
other then the default Numpy array.
"""
ret = cls(np.empty(shape=tuple(shape), dtype=dtype, order=order))
if fill_value is not None:
ret.fill(fill_value)
return ret
...

@classmethod
def from_ndarray_like(cls, ndarray_like: NDArrayLike) -> Self:
Expand All @@ -339,6 +343,7 @@ def from_ndarray_like(cls, ndarray_like: NDArrayLike) -> Self:
return cls(ndarray_like)

@classmethod
@abstractmethod
def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self:
"""Create a new buffer of Numpy array-like object

Expand All @@ -351,7 +356,7 @@ def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self:
-------
New buffer representing `array_like`
"""
return cls.from_ndarray_like(np.asanyarray(array_like))
...

def as_ndarray_like(self) -> NDArrayLike:
"""Returns the underlying array (host or device memory) of this buffer
Expand All @@ -364,6 +369,7 @@ def as_ndarray_like(self) -> NDArrayLike:
"""
return self._data

@abstractmethod
def as_numpy_array(self) -> npt.NDArray[Any]:
"""Returns the buffer as a NumPy array (host memory).

Expand All @@ -375,7 +381,7 @@ def as_numpy_array(self) -> npt.NDArray[Any]:
-------
NumPy array of this buffer (might be a data copy)
"""
return np.asanyarray(self._data)
...

@property
def dtype(self) -> np.dtype[Any]:
Expand Down Expand Up @@ -406,13 +412,11 @@ def squeeze(self, axis: tuple[int, ...]) -> Self:
def astype(self, dtype: npt.DTypeLike, order: Literal["K", "A", "C", "F"] = "K") -> Self:
return self.__class__(self._data.astype(dtype=dtype, order=order))

def __getitem__(self, key: Any) -> Self:
return self.__class__(np.asanyarray(self._data.__getitem__(key)))
@abstractmethod
def __getitem__(self, key: Any) -> Self: ...

def __setitem__(self, key: Any, value: Any) -> None:
if isinstance(value, NDBuffer):
value = value._data
self._data.__setitem__(key, value)
@abstractmethod
def __setitem__(self, key: Any, value: Any) -> None: ...

def __len__(self) -> int:
return self._data.__len__()
Expand All @@ -433,34 +437,6 @@ def transpose(self, axes: SupportsIndex | Sequence[SupportsIndex] | None) -> Sel
return self.__class__(self._data.transpose(axes))


def as_numpy_array_wrapper(
func: Callable[[npt.NDArray[Any]], bytes], buf: Buffer, prototype: BufferPrototype
) -> Buffer:
"""Converts the input of `func` to a numpy array and the output back to `Buffer`.

This function is useful when calling a `func` that only support host memory such
as `GZip.decode` and `Blosc.decode`. In this case, use this wrapper to convert
the input `buf` to a Numpy array and convert the result back into a `Buffer`.

Parameters
----------
func
The callable that will be called with the converted `buf` as input.
`func` must return bytes, which will be converted into a `Buffer`
before returned.
buf
The buffer that will be converted to a Numpy array before given as
input to `func`.
prototype
The prototype of the output buffer.

Returns
-------
The result of `func` converted to a `Buffer`
"""
return prototype.buffer.from_bytes(func(buf.as_numpy_array()))


class BufferPrototype(NamedTuple):
"""Prototype of the Buffer and NDBuffer class

Expand All @@ -476,7 +452,3 @@ class BufferPrototype(NamedTuple):

buffer: type[Buffer]
nd_buffer: type[NDBuffer]


# The default buffer prototype used throughout the Zarr codebase.
default_buffer_prototype = BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer)
Loading
Loading