Skip to content

Add NDBuffer.empty #3191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3191.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `NDBuffer.empty` method for faster ndbuffer initialization.
35 changes: 35 additions & 0 deletions src/zarr/core/buffer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,41 @@
cast("NDArrayLike", None)
) # This line will never be reached, but it satisfies the type checker

@classmethod
def empty(
cls, shape: ChunkCoords, dtype: npt.DTypeLike, order: Literal["C", "F"] = "C"
) -> Self:
"""
Create an empty buffer with the given shape, dtype, and order.

This method can be faster than ``NDBuffer.create`` because it doesn't
have to initialize the memory used by the underlying ndarray-like
object.

Parameters
----------
shape
The shape of the buffer and its underlying ndarray-like object
dtype
The datatype of the buffer and its underlying ndarray-like object
order
Whether to store multi-dimensional data in row-major (C-style) or
column-major (Fortran-style) order in memory.

Returns
-------
buffer
New buffer representing a new ndarray_like object with empty data.

See Also
--------
NDBuffer.create
Create a new buffer with some initial fill value.
"""
# Implementations should override this method if they have a faster way
# to allocate an empty buffer.
return cls.create(shape=shape, dtype=dtype, order=order)

Check warning on line 410 in src/zarr/core/buffer/core.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/core/buffer/core.py#L410

Added line #L410 was not covered by tests

@classmethod
def from_ndarray_like(cls, ndarray_like: NDArrayLike) -> Self:
"""Create a new buffer of a ndarray-like object
Expand Down
8 changes: 7 additions & 1 deletion src/zarr/core/buffer/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from typing import Self

from zarr.core.buffer.core import ArrayLike, NDArrayLike
from zarr.core.common import BytesLike
from zarr.core.common import BytesLike, ChunkCoords


class Buffer(core.Buffer):
Expand Down Expand Up @@ -160,6 +160,12 @@
else:
return cls(np.full(shape=tuple(shape), fill_value=fill_value, dtype=dtype, order=order))

@classmethod
def empty(
cls, shape: ChunkCoords, dtype: npt.DTypeLike, order: Literal["C", "F"] = "C"
) -> Self:
return cls(np.empty(shape=shape, dtype=dtype, order=order))

Check warning on line 167 in src/zarr/core/buffer/cpu.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/core/buffer/cpu.py#L167

Added line #L167 was not covered by tests

@classmethod
def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self:
return cls.from_ndarray_like(np.asanyarray(array_like))
Expand Down
8 changes: 7 additions & 1 deletion src/zarr/core/buffer/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from collections.abc import Iterable
from typing import Self

from zarr.core.common import BytesLike
from zarr.core.common import BytesLike, ChunkCoords

try:
import cupy as cp
Expand Down Expand Up @@ -178,6 +178,12 @@ def create(
ret.fill(fill_value)
return ret

@classmethod
def empty(
cls, shape: ChunkCoords, dtype: npt.DTypeLike, order: Literal["C", "F"] = "C"
) -> Self:
return cls(cp.empty(shape=shape, dtype=dtype, order=order))

@classmethod
def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self:
"""Create a new buffer of Numpy array-like object
Expand Down
11 changes: 11 additions & 0 deletions src/zarr/testing/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from collections.abc import Iterable
from typing import Self

from zarr.core.common import ChunkCoords


__all__ = [
"NDBufferUsingTestNDArrayLike",
Expand Down Expand Up @@ -51,6 +53,15 @@
ret.fill(fill_value)
return ret

@classmethod
def empty(
cls,
shape: ChunkCoords,
dtype: npt.DTypeLike,
order: Literal["C", "F"] = "C",
) -> Self:
return super(cpu.NDBuffer, cls).empty(shape=shape, dtype=dtype, order=order)

Check warning on line 63 in src/zarr/testing/buffer.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/buffer.py#L63

Added line #L63 was not covered by tests
Copy link
Contributor Author

@TomAugspurger TomAugspurger Jul 1, 2025

Choose a reason for hiding this comment

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

This is to test the base core.NDBuffer implementation. This class inherits from cpu.NDBuffer so we use super(cpu.NDBuffer, cls) to get its parent's impelentation (the base class).



class StoreExpectingTestBuffer(MemoryStore):
"""Example of a custom Store that expect MyBuffer for all its non-metadata
Expand Down
13 changes: 5 additions & 8 deletions src/zarr/testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,10 @@ def has_cupy() -> bool:
T = TypeVar("T")


gpu_mark = pytest.mark.gpu
skip_if_no_gpu = pytest.mark.skipif(not has_cupy(), reason="CuPy not installed or no GPU available")


# Decorator for GPU tests
def gpu_test(func: T) -> T:
return cast(
"T",
pytest.mark.gpu(
pytest.mark.skipif(not has_cupy(), reason="CuPy not installed or no GPU available")(
func
)
),
)
return cast("T", gpu_mark(skip_if_no_gpu(func)))
40 changes: 38 additions & 2 deletions tests/test_buffer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

import numpy as np
import pytest
Expand All @@ -20,7 +20,7 @@
TestBuffer,
TestNDArrayLike,
)
from zarr.testing.utils import gpu_test
from zarr.testing.utils import gpu_mark, gpu_test, skip_if_no_gpu

if TYPE_CHECKING:
import types
Expand Down Expand Up @@ -200,3 +200,39 @@ def test_gpu_buffer_prototype() -> None:
def test_cpu_buffer_as_scalar() -> None:
buf = cpu.buffer_prototype.nd_buffer.create(shape=(), dtype="int64")
assert buf.as_scalar() == buf.as_ndarray_like()[()] # type: ignore[index]


@pytest.mark.parametrize(
"prototype",
[
cpu.buffer_prototype,
pytest.param(
gpu.buffer_prototype,
marks=[gpu_mark, skip_if_no_gpu],
),
BufferPrototype(
buffer=cpu.Buffer,
nd_buffer=NDBufferUsingTestNDArrayLike,
),
],
)
@pytest.mark.parametrize(
"shape",
[
(1, 2),
(1, 2, 3),
],
)
@pytest.mark.parametrize("dtype", ["int32", "float64"])
@pytest.mark.parametrize("order", ["C", "F"])
def test_empty(
prototype: BufferPrototype, shape: tuple[int, ...], dtype: str, order: Literal["C", "F"]
) -> None:
buf = prototype.nd_buffer.empty(shape=shape, dtype=dtype, order=order)
result = buf.as_ndarray_like()
assert result.shape == shape
assert result.dtype == dtype
if order == "C":
assert result.flags.c_contiguous # type: ignore[attr-defined]
else:
assert result.flags.f_contiguous # type: ignore[attr-defined]