Repository navigation
Zstd Codec on the GPU - #2863
akshaysubr wants to merge 87 commits into
Conversation
d6608e7 to
d548adc
Compare
|
Thanks for opening this PR! At the moment we do not have any codecs implemented in the |
|
@dstansby My understanding was that |
TomAugspurger
left a comment
There was a problem hiding this comment.
Looks nice overall. I think the async side of things ended up in a pretty good spot. The code itself is pretty easy to follow (a normal stream synchronize). Having to do that on another host thread is a bit unfortunate, but there's only one synchronize per batch so this should be fine.
I left comments on a few things to clean up that I can help out with if you want @akshaysubr.
| checksum: bool = False | ||
|
|
||
| def __init__(self, *, level: int = 0, checksum: bool = False) -> None: | ||
| # TODO: Set CUDA device appropriately here and also set CUDA stream |
There was a problem hiding this comment.
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.
| chunks_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], | ||
| ) -> Iterable[Buffer | None]: | ||
| return [ | ||
| spec.prototype.buffer.from_array_like(cp.array(a, dtype=np.dtype("b"), copy=False)) |
There was a problem hiding this comment.
This is one spot where @weiji14's idea to use dlpack in #2658 (comment) would help. If NDBuffer new how to consume objects implementing the dlpack protocol, we could (maybe) get rid of the cp.array
|
Fixed some merge conflicts and changed the |
| if array_like.ndim != 1: | ||
| raise ValueError("array_like: only 1-dim allowed") | ||
| if array_like.dtype != np.dtype("B"): | ||
| if array_like.dtype.itemsize != 1: |
There was a problem hiding this comment.
The new tests in test_nvcomp.py were failing without this change.
I'd like to get us to a point where we don't care as much about the details of the buffer passed in here. This is an OK start I think.
There was a problem hiding this comment.
what exactly does this check for? It's not clear to me why any numpy array that can be viewed as bytes wouldn't be allowed in here
There was a problem hiding this comment.
and same for the dimensionality check, since any N-dimensional numpy array can be viewed as a 1D array.
There was a problem hiding this comment.
Yeah, I'm not really sure...
I agree that the actual data we store internally here needs to be a byte dtype. Just doing cp.asarray(input).view("b") seems pretty reasonable to me.
There was a problem hiding this comment.
i'm not even convinced that we need Buffer / NDBuffer, when Buffer is just a special case of NDBuffer where there's 1 dimension and the data type is bytes
There was a problem hiding this comment.
we could even express this formally by:
- making
NDBuffergeneric with two type parameters (number of dimensions and dtype) - having APIs that insist on consuming a
Bufferinstead insist on consumingNDBuffer[Literal[1], np.uint8]
There was a problem hiding this comment.
(super out of scope for this PR ofc)
There was a problem hiding this comment.
It's not clear to me why any numpy array that can be viewed as bytes wouldn't be allowed in here
I think this is mainly because NDBuffer objects don't need to be contiguous, but Buffer objects must be contiguous in memory which might be important when we send those out to codecs that expect a contiguous memory slice.
But I agree that we can probably merge those two and make Buffer a specialization of NDBuffer.
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## main #2863 +/- ##
==========================================
- Coverage 92.91% 83.66% -9.25%
==========================================
Files 86 98 +12
Lines 10707 19415 +8708
==========================================
+ Hits 9948 16243 +6295
- Misses 759 3172 +2413 🚀 New features to boost your workflow:
|
…ed config - decode()/encode() created a cp.cuda.Event() but never called .record() before .synchronize(), so the "wait for GPU completion" was a no-op -- a race, not just wasted asyncio.to_thread overhead. Now records on the stream the nvcomp op was enqueued on before waiting. - _zstd_codec was a cached_property binding device/stream once, forever, even though codec/pipeline instances are cached and reused (e.g. by sharding's pipeline cache). Now built fresh per call from the caller's current device/stream. - level/checksum were parsed and round-tripped through to_dict/from_dict but never passed to nvcomp.Codec(...). checksum now maps to nvcomp's ChecksumPolicy; level has no nvCOMP equivalent, so a nonzero value now raises NotImplementedError instead of being silently dropped. - is_fixed_size was True for a compressor (encoded size is data-dependent, same as the CPU ZstdCodec's is_fixed_size = False); left is_fixed_size True made compute_encoded_size's NotImplementedError reachable by any caller that correctly trusts is_fixed_size before calling it.
# Conflicts: # docs/user-guide/gpu.md # pyproject.toml # src/zarr/codecs/__init__.py # src/zarr/codecs/gpu.py # src/zarr/core/buffer/gpu.py # tests/test_codecs/test_codecs.py # tests/test_codecs/test_nvcomp.py
# Conflicts: # src/zarr/core/buffer/gpu.py # tests/test_api.py
…of cast Answers review question on why the cast was needed: get_codec_class returns type[Codec]; the registry lookup exists so enable_gpu() can swap in NvcompZstdCodec. A runtime isinstance check replaces the static cast.
…n gpu.NDBuffer (S101)
…Buffer object-dtype error with main
|
@akshaysubr I've opened akshaysubr#4 against this PR's |
…ailable in CI" This reverts commit 4d4463f.
Per review discussion on zarr-developers#2863 (JeppeKlitgaard, d-v-b, TomAugspurger): the extra installs CUDA-specific packages (cupy-cuda12x, nvidia-nvcomp-cu12), so name it after CUDA rather than GPUs in general, leaving room for other accelerator backends. The 'gpu' extra is removed (extras can't be deprecated). enable_gpu() and zarr.buffer.gpu are unchanged: they predate this PR and are public API.
Raising NotImplementedError in __init__ also fired from from_dict, so any array written by the CPU ZstdCodec with level != 0 could not be opened under enable_gpu(). level is an encode-only parameter; accept and preserve it in metadata as the CPU codec does. Restore the device/stream TODO.
nvCOMP only allows NO_COMPUTE_NO_VERIFY with a RAW bitstream, so mapping checksum to ChecksumPolicy made checksum=True fail on both encode and decode. The CUDA event is still recorded before synchronizing.
Sync gpu-codecs with main, rename the gpu extra to cuda, fix nvCOMP Zstd sync and options
This PR adds a Zstd codec that runs on the GPU using the nvCOMP 4.2 python APIs.
TODO:
docs/user-guide/*.rstchanges/