Repository navigation
Zstd Codec on the GPU #2863
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
base: main
Are you sure you want to change the base?
Zstd Codec on the GPU #2863
Changes from all commits
49d5ee8
d548adc
a8c0db3
69aa274
10e1bc9
771c0c1
ec07100
d1c37a3
69ea74e
1b85fdc
f5c7814
7671274
d558ef8
f16d730
f0db57d
048ad48
2282cb9
c6460b5
3b5e294
dd825dc
f89b232
7a4b037
398b4d1
dd69543
76f7560
8b5b3f1
7af3a16
996fbc0
d24d027
090349c
83c53b0
eb50521
de3b577
ac14838
9c673ce
594ae65
202efae
d55f541
fda4350
d5b14af
6c76a6c
39789d5
c7360e2
a71b5ab
7075e3c
9262999
a153f0c
82f6ffe
aeab833
486b2bf
99ed0af
1de24ff
abf7695
d63fd21
056a602
2646c82
b0a1848
281548d
8a801c7
ee1ec92
3264865
bd34083
1b024fb
576ba5f
93d6010
314bc54
56b9e00
ef2e2f7
166c011
fbefa5e
21dea73
1ad457d
58cee0e
81e49b0
1acd153
54a8430
4d4463f
9fd0064
f2722ab
8287247
fba64c2
0f04526
7ca5cd4
b6dc00d
f87dce1
05d97a1
5f9fe39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| Added GPU-accelerated Zstd Codec | ||
|
|
||
| This adds support for decoding with the Zstd Codec on NVIDIA GPUs using the | ||
| nvidia-nvcomp library. | ||
|
|
||
| With `zarr.config.enable_gpu()`, buffers will be decoded using the GPU | ||
| and the output will reside in device memory. | ||
|
|
||
| The optional-dependency extra for GPU support is now named `cuda` (``pip install "zarr[cuda]"``) | ||
| instead of `gpu`, since it installs CUDA-specific packages (CuPy and nvCOMP). The `gpu` extra | ||
| has been removed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| .. _user-guide-gpu: | ||
|
|
||
| Using GPUs with Zarr | ||
| ==================== | ||
|
|
||
| Zarr can use GPUs to accelerate your workload by running | ||
| :meth:`zarr.config.enable_gpu`. | ||
|
|
||
| Reading data into device memory | ||
| ------------------------------- | ||
|
|
||
| :meth:`zarr.config.enable_gpu` configures Zarr to use GPU memory for the data | ||
| buffers used internally by Zarr. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| >>> import zarr | ||
| >>> import cupy as cp # doctest: +SKIP | ||
| >>> zarr.config.enable_gpu() # doctest: +SKIP | ||
| >>> store = zarr.storage.MemoryStore() # doctest: +SKIP | ||
| >>> z = zarr.create_array( # doctest: +SKIP | ||
| ... store=store, shape=(100, 100), chunks=(10, 10), dtype="float32", | ||
| ... ) | ||
| >>> type(z[:10, :10]) # doctest: +SKIP | ||
| cupy.ndarray | ||
|
|
||
| Note that the output type is a ``cupy.ndarray`` rather than a NumPy array. | ||
|
|
||
| For supported codecs, data will be decoded using the GPU via the `nvcomp`_ | ||
| library. See :ref:`user-guide-config` for more. Isseus and feature requests | ||
| for NVIDIA nvCOMP can be reported in the `nvcomp issue tracker`_. | ||
|
|
||
| .. _nvcomp: https://docs.nvidia.com/cuda/nvcomp/samples/python_samples.html | ||
| .. _nvcomp issue tracker: https://github.com/NVIDIA/CUDALibrarySamples/issues |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| from dataclasses import dataclass | ||
| from functools import cached_property | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| import numpy as np | ||
|
|
||
| from zarr.abc.codec import BytesBytesCodec | ||
| from zarr.core.common import JSON, parse_named_configuration | ||
| from zarr.registry import register_codec | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Iterable | ||
| from typing import Self | ||
|
|
||
| from zarr.core.array_spec import ArraySpec | ||
| from zarr.core.buffer import Buffer | ||
|
|
||
| try: | ||
| import cupy as cp | ||
| except ImportError: # pragma: no cover | ||
| cp = None | ||
|
|
||
| try: | ||
| from nvidia import nvcomp | ||
| except ImportError: # pragma: no cover | ||
| nvcomp = None | ||
|
|
||
|
|
||
| def _parse_zstd_level(data: JSON) -> int: | ||
| if isinstance(data, int): | ||
| if data >= 23: | ||
| raise ValueError(f"Value must be less than or equal to 22. Got {data} instead.") | ||
| return data | ||
| raise TypeError(f"Got value with type {type(data)}, but expected an int.") | ||
|
|
||
|
|
||
| def _parse_checksum(data: JSON) -> bool: | ||
| if isinstance(data, bool): | ||
| return data | ||
| raise TypeError(f"Expected bool. Got {type(data)}.") | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class NvcompZstdCodec(BytesBytesCodec): | ||
| is_fixed_size = True | ||
|
|
||
| level: int = 0 | ||
| checksum: bool = False | ||
|
|
||
| def __init__(self, *, level: int = 0, checksum: bool = False) -> None: | ||
| # TODO: Set CUDA device appropriately here and also set CUDA stream | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed with leaving devices / streams as a TODO for now. I want to enable users to overlap host-to-device memcpys with compute operations (like decode, but their own compute operations as well), but I'm not sure yet what that API will look like. If you have any thoughts on how best to do this I'd love to hear them, and write them up as an issue.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #3271 for planning on devices and streams. |
||
|
|
||
| level_parsed = _parse_zstd_level(level) | ||
| checksum_parsed = _parse_checksum(checksum) | ||
|
|
||
| object.__setattr__(self, "level", level_parsed) | ||
| object.__setattr__(self, "checksum", checksum_parsed) | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data: dict[str, JSON]) -> Self: | ||
| _, configuration_parsed = parse_named_configuration(data, "zstd") | ||
| return cls(**configuration_parsed) # type: ignore[arg-type] | ||
|
|
||
| def to_dict(self) -> dict[str, JSON]: | ||
| return { | ||
| "name": "zstd", | ||
| "configuration": {"level": self.level, "checksum": self.checksum}, | ||
| } | ||
|
|
||
| @cached_property | ||
| def _zstd_codec(self) -> nvcomp.Codec: | ||
| device = cp.cuda.Device() # Select the current default device | ||
| stream = cp.cuda.get_current_stream() # Use the current default stream | ||
| # Note: this returns an array with dtype=np.dtype("int8") | ||
| return nvcomp.Codec( | ||
| algorithm="Zstd", | ||
| bitstream_kind=nvcomp.BitstreamKind.RAW, | ||
| device_id=device.id, | ||
| cuda_stream=stream.ptr, | ||
| ) | ||
|
|
||
| def _convert_to_nvcomp_arrays( | ||
| self, | ||
| chunks_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], | ||
| ) -> tuple[list[nvcomp.Array], list[int]]: | ||
| none_indices = [i for i, (b, _) in enumerate(chunks_and_specs) if b is None] | ||
| filtered_inputs = [b.as_array_like() for b, _ in chunks_and_specs if b is not None] | ||
| # TODO: add CUDA stream here | ||
| return nvcomp.as_arrays(filtered_inputs), none_indices | ||
|
|
||
| def _convert_from_nvcomp_arrays( | ||
| self, | ||
| arrays: Iterable[nvcomp.Array], | ||
| chunks_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], | ||
| ) -> Iterable[Buffer | None]: | ||
| result: list[Buffer | None] = [] | ||
|
|
||
| for a, (_, spec) in zip(arrays, chunks_and_specs, strict=True): | ||
| if a is None: | ||
| result.append(None) | ||
| else: | ||
| a2 = cp.array(a, dtype=a.dtype, copy=False) | ||
| if a2.dtype != np.dtype("B"): | ||
| a2 = a2.view(dtype=np.dtype("B")) | ||
| result.append(spec.prototype.buffer.from_array_like(a2)) | ||
|
Comment on lines
+105
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously we just had I think this mismatch comes in the first place from nvcomp using |
||
|
|
||
| return result | ||
|
|
||
| async def decode( | ||
| self, | ||
| chunks_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], | ||
| ) -> Iterable[Buffer | None]: | ||
| """Decodes a batch of chunks. | ||
| Chunks can be None in which case they are ignored by the codec. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| chunks_and_specs : Iterable[tuple[Buffer | None, ArraySpec]] | ||
| Ordered set of encoded chunks with their accompanying chunk spec. | ||
|
|
||
| Returns | ||
| ------- | ||
| Iterable[Buffer | None] | ||
| """ | ||
| chunks_and_specs = list(chunks_and_specs) | ||
|
|
||
| # Convert to nvcomp arrays | ||
| filtered_inputs, none_indices = self._convert_to_nvcomp_arrays(chunks_and_specs) | ||
|
|
||
| outputs = self._zstd_codec.decode(filtered_inputs) if len(filtered_inputs) > 0 else [] | ||
|
TomAugspurger marked this conversation as resolved.
TomAugspurger marked this conversation as resolved.
TomAugspurger marked this conversation as resolved.
|
||
|
|
||
| # Record event for synchronization | ||
| event = cp.cuda.Event() | ||
| event.record(cp.cuda.get_current_stream()) | ||
| # Wait for decode to complete in a separate async thread | ||
| await asyncio.to_thread(event.synchronize) | ||
|
Comment on lines
+135
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of what I learned as part of https://tomaugspurger.net/posts/gpu-accelerated-zarr/ was the importance of avoiding unnecessary synchronizations being important for performance. At the moment, it doesn't really matter since |
||
|
|
||
| for index in none_indices: | ||
| outputs.insert(index, None) | ||
|
|
||
| return self._convert_from_nvcomp_arrays(outputs, chunks_and_specs) | ||
|
|
||
| async def encode( | ||
| self, | ||
| chunks_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], | ||
| ) -> Iterable[Buffer | None]: | ||
| """Encodes a batch of chunks. | ||
| Chunks can be None in which case they are ignored by the codec. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| chunks_and_specs : Iterable[tuple[Buffer | None, ArraySpec]] | ||
| Ordered set of to-be-encoded chunks with their accompanying chunk spec. | ||
|
|
||
| Returns | ||
| ------- | ||
| Iterable[Buffer | None] | ||
| """ | ||
| # TODO: Make this actually async | ||
| chunks_and_specs = list(chunks_and_specs) | ||
|
|
||
| # Convert to nvcomp arrays | ||
| filtered_inputs, none_indices = self._convert_to_nvcomp_arrays(chunks_and_specs) | ||
|
|
||
| outputs = self._zstd_codec.encode(filtered_inputs) if len(filtered_inputs) > 0 else [] | ||
|
|
||
| # Record event for synchronization | ||
| event = cp.cuda.Event() | ||
| event.record(cp.cuda.get_current_stream()) | ||
| # Wait for encode to complete in a separate async thread | ||
| await asyncio.to_thread(event.synchronize) | ||
|
|
||
| for index in none_indices: | ||
| outputs.insert(index, None) | ||
|
TomAugspurger marked this conversation as resolved.
|
||
|
|
||
| return self._convert_from_nvcomp_arrays(outputs, chunks_and_specs) | ||
|
|
||
| def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| register_codec("zstd", NvcompZstdCodec) | ||
Uh oh!
There was an error while loading. Please reload this page.