Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Reworked codec pipelines - #1670

Merged
normanrz merged 29 commits into
v3from
batched-codec-pipeline
May 17, 2024
Merged

Reworked codec pipelines#1670
normanrz merged 29 commits into
v3from
batched-codec-pipeline

Conversation

@normanrz

@normanrznormanrz commented Feb 16, 2024

Copy link
Copy Markdown
Member

This PR refactors the codec pipelines in the v3 codebase. There are now a new default implementation:

  • BatchedCodecPipeline, which divides the chunk batches into configurable "mini-batches". In a mini-batch all steps are run in lock step (e.g. fetching from store, decoding, encoding, writing to store). Multiple mini-batches are processed concurrently.

This PR moves a lot of code from the Array to the codec pipeline, which is an opportunity to share more code between the Array and the ShardingCodec. To make that work new ByteGetter and ByteSetter protocols to generalize the existing StorePath are introduced.

It also changes the Codec API by making decode and encode methods take chunk batches.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@normanrznormanrz self-assigned this Feb 16, 2024
@pep8speaks

pep8speaks commented Feb 16, 2024

Copy link
Copy Markdown

Hello @normanrz! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2024-05-10 11:48:21 UTC

@normanrznormanrz mentioned this pull request Feb 19, 2024
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 5d4d09a to 450bcc6CompareFebruary 20, 2024 10:28
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/codecs/batched_pipeline.py Outdated
Comment threadsrc/zarr/v3/metadata.py Outdated
@akshaysubr

Copy link
Copy Markdown
Contributor

@normanrz Overall, this looks quite good to me. A couple of questions I had:

  1. It makes sense for the default behavior to be just dispatching decoding to multiple threads in decode_batch and similarly for encoding. For codecs that want a different parallelization strategy, the idea would be that they just override decode_batch right?
  2. All of the array types are currently np.ndarray. Would it be possible to generalize this to something that is agnostic to the specific library and uses the buffer protocol, __array_interface__, __cuda_array_interface__ or DLPack? This would allow using numpy arrays as usual, but also cupy arrays, PyTorch tensors, JAX arrays, etc.

@normanrznormanrz added this to the 3.0.0.alpha milestone Apr 12, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I refactored the codec pipeline quite a bit in the last commit.

  • There are now a BatchedCodecPipeline and a InterleavedCodecPipeline building upon an abstract CodecPipeline
  • There are new protocol types for ByteGetter and ByteSetter. These are basically a generalization of StorePath. This allows the sharding codec to use the same codec pipeline abstraction as the array. That moves us one step closer to treating shards as sub-arrays.
  • The decode, decode_partial, encode, encode_partial now have the batched interface.
  • This still uses np.ndarray everywhere. I leave that to @akshaysubr to change to a generalized array interface.

Currently, the choice of codec pipeline is hard-coded. I am still looking for a way to specify that. Should that go into RuntimeConfiguration? @d-v-b@jhamman

@normanrznormanrz mentioned this pull request Apr 22, 2024
Comment threadsrc/zarr/v3/abc/codec.py Outdated
@normanrz
normanrzforce-pushed the batched-codec-pipeline branch from 6c8c706 to 019ecc8CompareApril 30, 2024 15:20
@normanrznormanrz changed the title Batched codec pipelineReworked codec pipelinesMay 8, 2024
@normanrz

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready for a final review. I updated the PR description with the major changes. The only thing missing from my pov is the user-configurable batch size and codecpipeline selection. I'll add that after #1855 lands.

@normanrz
normanrz requested review from d-v-b and jhammanMay 10, 2024 09:09
Comment threadsrc/zarr/abc/codec.py Outdated
Comment threadsrc/zarr/abc/codec.py Outdated

@dataclass(frozen=True)
class BytesCodec(ArrayBytesCodec):
class BytesCodec(ArrayBytesCodecBatchMixin):

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.

Will ArrayBytesCodec be used anywhere other than as the base class fro ArrayBytesCodecBatchMixin? If not, we might want to consider simplifying the inheritance structure a bit.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would anticipate that folks want to build a codec that implements their own batching, e.g. in rust or on the GPU. That is why, we should keep both classes.

Comment threadsrc/zarr/codecs/mixins.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
Comment threadsrc/zarr/codecs/pipeline/hybrid.py Outdated
@madsbk

Copy link
Copy Markdown
Contributor

I think it would be helpful with some docstrings. At least, I find it hard to follow the intention without any help :)

@normanrz

Copy link
Copy Markdown
MemberAuthor

I added a few doc strings and implemented the abstract Codec classes with Generics.

@normanrz
normanrz requested a review from d-v-bMay 15, 2024 13:32

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@normanrz -- this is an impressive piece of work. I should admit that the size of it made it hard to review so I just have a few comments. Overall, I think its a big step forward and I want to get my hands on it so I favor moving it into the v3 branch asap.

This batched codec pipeline divides the chunk batches into batches of a configurable
batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in
lock step for each mini-batch. Multiple mini-batches are processing concurrently.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can come later but we're going to want some additional documentation on the behavior here. Reading this, I'm not entirely sure I get it.

Comment threadsrc/zarr/codecs/batched_codec_pipeline.py Outdated
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/codecs/batched_codec_pipeline.py
Comment threadsrc/zarr/abc/codec.py
"""
return chunk_spec

def evolve(self, array_spec: ArraySpec) -> Self:

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 doesn't need to be addressed in this PR, but the docstring for this method doesn't describe the behavior I would expect from a method called "evolve" -- my intuition is that object.evolve(property=new_val) would return a copy of object, with property set to new_val, which I think is consistent with how it works in attrs. But codec.evolve here is rather different . Based on this docstring, I would think this method should be called "from_array_spec" or something, to make it clear that we are getting a new codec instance from an array spec (and it would make sense to use .evolve in this method of course).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What about naming the function evolve_from_array_spec?

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 that works

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.

alternatively, we could make from_array_spec a class method that takes keyword arguments to cover the attributes that the input array spec doesn't convey

CodecOutput = TypeVar("CodecOutput", bound=np.ndarray | BytesLike)


async def batching_helper(

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 is a very naive question, but given that this function exists, why do we need to implement batching by writing new methods for codecs classes? Unless I'm missing something, the new batching methods just wrap batching_helper around the base encode / decode functionality defined per-codec.

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.

to put the point differently, why can't the codec pipeline class implement batching by calling batching_helper on the encode / decode methods of the codecs it contains?

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 doesn't need to be addressed here, so feel free to ignore for now

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

How would you feel about this?

class_Codec(Generic[CodecInput, CodecOutput], Metadata):
...
asyncdef_decode_single(self, chunk_data: CodecOutput, chunk_spec: ArraySpec) ->CodecInput:
raiseNotImplementedErrorasyncdefdecode(
self, chunk_data_and_specs: Iterable[tuple[CodecOutput|None, ArraySpec]]
) ->Iterable[CodecInput|None]:
returnawaitbatching_helper(self.decode_single, chunk_data_and_specs)
# same for encode
...

Batch-aware codecs would then only override the decode method and ignore the _decode_single method. That would be fine because _decode_single is a protected method not intended to be part of the public interface. Single-chunk codecs could override _decode_single and won't have to care about the batching. We could drop the batch mixins, then.

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.

sounds good!

normanrzand others added 7 commits May 16, 2024 09:52
Co-authored-by: Joe Hamman <joe@earthmover.io>
* adds wrapper codecs for the v2 codec pipeline
* encode_chunk_key
* refactor ArrayV2 away
* empty zattrs
* Apply suggestions from code review
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
* unify ArrayMetadata
* abstract ArrayMetadata
* unified Array.create
* use zarr.config for batch_size
* __init__.py aktualisieren
Co-authored-by: Joe Hamman <joe@earthmover.io>
* ruff
---------
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
Co-authored-by: Joe Hamman <joe@earthmover.io>
Comment on lines +13 to +104
@dataclass(frozen=True)
class V2Compressor(ArrayBytesCodecBatchMixin):
compressor: dict[str, JSON] | None

is_fixed_size = False

async def decode_single(
self,
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
if chunk_bytes is None:
return None

if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
chunk_numpy_array = ensure_ndarray(
await to_thread(compressor.decode, chunk_bytes.as_array_like())
)
else:
chunk_numpy_array = ensure_ndarray(chunk_bytes.as_array_like())

# ensure correct dtype
if str(chunk_numpy_array.dtype) != chunk_spec.dtype:
chunk_numpy_array = chunk_numpy_array.view(chunk_spec.dtype)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
_chunk_spec: ArraySpec,
) -> Buffer | None:
chunk_numpy_array = chunk_array.as_numpy_array()
if self.compressor is not None:
compressor = numcodecs.get_codec(self.compressor)
if (
not chunk_numpy_array.flags.c_contiguous
and not chunk_numpy_array.flags.f_contiguous
):
chunk_numpy_array = chunk_numpy_array.copy(order="A")
encoded_chunk_bytes = ensure_bytes(
await to_thread(compressor.encode, chunk_numpy_array)
)
else:
encoded_chunk_bytes = ensure_bytes(chunk_numpy_array)

return Buffer.from_bytes(encoded_chunk_bytes)

def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int:
raise NotImplementedError


@dataclass(frozen=True)
class V2Filters(ArrayArrayCodecBatchMixin):
filters: list[dict[str, JSON]]

is_fixed_size = False

async def decode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
chunk_numpy_array = chunk_array.as_numpy_array()
# apply filters in reverse order
if self.filters is not None:
for filter_metadata in self.filters[::-1]:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.decode, chunk_numpy_array)

# ensure correct chunk shape
if chunk_numpy_array.shape != chunk_spec.shape:
chunk_numpy_array = chunk_numpy_array.reshape(
chunk_spec.shape,
order=chunk_spec.order,
)

return NDBuffer.from_numpy_array(chunk_numpy_array)

async def encode_single(
self,
chunk_array: NDBuffer,
chunk_spec: ArraySpec,
) -> NDBuffer | None:
chunk_numpy_array = chunk_array.as_numpy_array().ravel(order=chunk_spec.order)

for filter_metadata in self.filters:
filter = numcodecs.get_codec(filter_metadata)
chunk_numpy_array = await to_thread(filter.encode, chunk_numpy_array)

return NDBuffer.from_numpy_array(chunk_numpy_array)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@madsbk I added a very naive Buffer integration in these codecs. Could you please help me get that right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll merge this PR for now. We can work on a new PR to optimize this.

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.

Sounds good!

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@normanrz@pep8speaks@akshaysubr@madsbk@jhamman@d-v-b@sanketverma1704