From 2aee3deef0d55f89b77f967cc6faaa11f010ee7a Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 18:56:46 -0700 Subject: [PATCH 01/12] Export ArrayMetadata --- docs/api.rst | 4 ++++ docs/history.rst | 7 +++++++ grunnur/__init__.py | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index b31dc41..955bedc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -102,6 +102,10 @@ Buffers and arrays .. autoclass:: Buffer() :members: +.. autoclass:: ArrayMetadata + :members: + :special-members: __getitem__ + .. autoclass:: ArrayMetadataLike() :show-inheritance: :members: diff --git a/docs/history.rst b/docs/history.rst index f125324..98f2b37 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -11,7 +11,14 @@ Changed * ``local_mem`` keyword parameter of kernel calls renamed to ``cu_dynamic_local_mem``. (PR_17_) +Added +^^^^^ + +* Made ``ArrayMetadata`` public. (PR_18_) + + .. _PR_17: https://github.com/fjarri/grunnur/pull/17 +.. _PR_18: https://github.com/fjarri/grunnur/pull/18 diff --git a/grunnur/__init__.py b/grunnur/__init__.py index 9ccfe20..b18eec6 100644 --- a/grunnur/__init__.py +++ b/grunnur/__init__.py @@ -6,7 +6,7 @@ opencl_api_id, ) from .array import Array, ArrayLike, MultiArray -from .array_metadata import ArrayMetadataLike +from .array_metadata import ArrayMetadata, ArrayMetadataLike from .buffer import Buffer from .context import Context from .device import Device, DeviceFilter From 762c9cc859098f4efa3921edad0a0d92e55682c6 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 19:04:18 -0700 Subject: [PATCH 02/12] Sync `is_contiguous` attribute name with the docs --- grunnur/array.py | 2 +- grunnur/array_metadata.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grunnur/array.py b/grunnur/array.py index bef3725..6d7d571 100644 --- a/grunnur/array.py +++ b/grunnur/array.py @@ -135,7 +135,7 @@ def set( if isinstance(array, numpy.ndarray): array_data = array elif isinstance(array, Array): - if not array._metadata.contiguous: # noqa: SLF001 + if not array._metadata.is_contiguous: raise ValueError("Setting from a non-contiguous device array is not supported") array_data = array.data else: diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index dddff61..0bf42a5 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -67,12 +67,12 @@ def __init__( if strides is None: strides = default_strides - self.contiguous = True + self.is_contiguous = True else: strides = tuple(strides) # Technically, an array with non-default (e.g., overlapping) strides # can be contioguous, but that's too hard to determine. - self.contiguous = strides == default_strides + self.is_contiguous = strides == default_strides min_offset, max_offset = _get_range(shape, dtype.itemsize, strides) if buffer_size is None: From a5ce7d69caff9f638ea6996c6917471abe8cb7ef Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 19:00:06 -0700 Subject: [PATCH 03/12] Add `Array.metadata` --- docs/history.rst | 1 + grunnur/array.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index 98f2b37..07281a5 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -15,6 +15,7 @@ Added ^^^^^ * Made ``ArrayMetadata`` public. (PR_18_) +* Add ``metadata`` attribute to ``Array``. (PR_18_) .. _PR_17: https://github.com/fjarri/grunnur/pull/17 diff --git a/grunnur/array.py b/grunnur/array.py index 6d7d571..1a1debc 100644 --- a/grunnur/array.py +++ b/grunnur/array.py @@ -33,6 +33,9 @@ class Array: strides: tuple[int, ...] """Array strides.""" + metadata: ArrayMetadata + """Array metadata object.""" + @classmethod def from_host( cls, @@ -90,17 +93,17 @@ def empty( @classmethod def empty_like(cls, device: BoundDevice, array_like: ArrayMetadataLike) -> Array: """Creates an empty array with the same shape and dtype as ``array_like``.""" + # TODO: take other information like strides and offset return cls.empty(device, array_like.shape, array_like.dtype) def __init__(self, array_metadata: ArrayMetadata, data: Buffer): - self._metadata = array_metadata - + self.metadata = array_metadata self.device = data.device - self.shape = self._metadata.shape - self.dtype = self._metadata.dtype - self.strides = self._metadata.strides - self.first_element_offset = self._metadata.first_element_offset - self.buffer_size = self._metadata.buffer_size + self.shape = self.metadata.shape + self.dtype = self.metadata.dtype + self.strides = self.metadata.strides + self.first_element_offset = self.metadata.first_element_offset + self.buffer_size = self.metadata.buffer_size if data.size < self.buffer_size: raise ValueError( @@ -111,7 +114,7 @@ def __init__(self, array_metadata: ArrayMetadata, data: Buffer): self.data = data def _view(self, slices: slice | tuple[slice, ...]) -> Array: - new_metadata = self._metadata[slices] + new_metadata = self.metadata[slices] origin, size, new_metadata = new_metadata.minimal_subregion() data = self.data.get_sub_region(origin, size) @@ -135,7 +138,7 @@ def set( if isinstance(array, numpy.ndarray): array_data = array elif isinstance(array, Array): - if not array._metadata.is_contiguous: + if not array.metadata.is_contiguous: raise ValueError("Setting from a non-contiguous device array is not supported") array_data = array.data else: From 609ac68f3e41c00c05bcbd4ba75bb5e46aa713e3 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 20:12:43 -0700 Subject: [PATCH 04/12] Add `ArrayMetadata.__eq__` and `__hash__` --- grunnur/array_metadata.py | 13 +++++++++++++ tests/test_array_metadata.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 0bf42a5..7ac5777 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -100,6 +100,19 @@ def __init__( self._full_max_offset = full_max_offset self.buffer_size = buffer_size + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, ArrayMetadata) + and self.shape == other.shape + and self.dtype == other.dtype + and self.strides == other.strides + and self.first_element_offset == other.first_element_offset + and self.buffer_size == other.buffer_size + ) + + def __hash__(self) -> int: + return hash((type(self), self.dtype, self.shape, self.strides, self.first_element_offset)) + def minimal_subregion(self) -> tuple[int, int, ArrayMetadata]: """ Returns the metadata for the minimal subregion that fits all the data in this view, diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index 4ed4896..4a8eb1b 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -132,6 +132,22 @@ def test_metadata_constructor(): ) +def test_eq(): + meta1 = ArrayMetadata((5, 6), numpy.int32) + meta2 = ArrayMetadata((5, 6), numpy.int32) + meta3 = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) + assert meta1 == meta2 + assert meta1 != meta3 + + +def test_hash(): + meta1 = ArrayMetadata((5, 6), numpy.int32) + meta2 = ArrayMetadata((5, 6), numpy.int32) + meta3 = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) + assert hash(meta1) == hash(meta2) + assert hash(meta1) != hash(meta3) + + def test_view(): meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4, -1:-5:-2] From 53942d56b98ef4bd4346c4d271f2d7eb642e4ac8 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 19:00:59 -0700 Subject: [PATCH 05/12] Allow array shape to be given as a single integer --- grunnur/array.py | 2 +- grunnur/array_metadata.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/grunnur/array.py b/grunnur/array.py index 1a1debc..75c1928 100644 --- a/grunnur/array.py +++ b/grunnur/array.py @@ -63,7 +63,7 @@ def from_host( def empty( cls, device: BoundDevice, - shape: Sequence[int], + shape: Sequence[int] | int, dtype: DTypeLike, strides: Sequence[int] | None = None, first_element_offset: int = 0, diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 7ac5777..eb3e8eb 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -1,12 +1,11 @@ from __future__ import annotations +from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from .dtypes import _normalize_type if TYPE_CHECKING: # pragma: no cover - from collections.abc import Sequence - import numpy from numpy.typing import DTypeLike @@ -51,13 +50,13 @@ def from_arraylike(cls, array: ArrayMetadataLike) -> ArrayMetadata: def __init__( self, - shape: Sequence[int], + shape: Sequence[int] | int, dtype: DTypeLike, strides: Sequence[int] | None = None, first_element_offset: int = 0, buffer_size: int | None = None, ): - shape = tuple(shape) + shape = tuple(shape) if isinstance(shape, Sequence) else (shape,) dtype = _normalize_type(dtype) if len(shape) == 0: From 6ea5a300fc93adc6484ae5e560637a741bacebf0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 20:10:11 -0700 Subject: [PATCH 06/12] Add ArrayMetadata.__repr__ --- grunnur/array_metadata.py | 17 ++++++++++++++++- tests/test_array_metadata.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index eb3e8eb..0909e16 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -74,8 +74,9 @@ def __init__( self.is_contiguous = strides == default_strides min_offset, max_offset = _get_range(shape, dtype.itemsize, strides) + default_buffer_size = first_element_offset + max_offset if buffer_size is None: - buffer_size = first_element_offset + max_offset + buffer_size = default_buffer_size full_min_offset = first_element_offset + min_offset if full_min_offset < 0 or full_min_offset + dtype.itemsize > buffer_size: @@ -99,6 +100,9 @@ def __init__( self._full_max_offset = full_max_offset self.buffer_size = buffer_size + self._default_strides = strides == default_strides + self._default_buffer_size = buffer_size == default_buffer_size + def __eq__(self, other: object) -> bool: return ( isinstance(other, ArrayMetadata) @@ -138,6 +142,17 @@ def __getitem__(self, slices: slice | tuple[slice, ...]) -> ArrayMetadata: new_shape, self.dtype, strides=new_strides, first_element_offset=new_fe_offset ) + def __repr__(self) -> str: + args = [f"dtype={self.dtype}", f"shape={self.shape}"] + if self.first_element_offset != 0: + args.append(f"first_element_offset={self.first_element_offset}") + if not self._default_strides: + args.append(f"strides={self.strides}") + if not self._default_buffer_size: + args.append(f"buffer_size={self.buffer_size}") + args_str = ", ".join(args) + return f"ArrayMetadata({args_str})" + def _get_strides(shape: Sequence[int], itemsize: int) -> tuple[int, ...]: # Constructs strides for a contiguous array of shape ``shape`` and item size ``itemsize``. diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index 4a8eb1b..c16bcd4 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -148,6 +148,26 @@ def test_hash(): assert hash(meta1) != hash(meta3) +def test_repr(): + meta = ArrayMetadata((5, 6), numpy.int32) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + + meta = ArrayMetadata((5, 6), numpy.int32, first_element_offset=0) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + meta = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), first_element_offset=12)" + + meta = ArrayMetadata((5, 6), numpy.int32, buffer_size=5 * 6 * 4) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + meta = ArrayMetadata((5, 6), numpy.int32, buffer_size=512) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), buffer_size=512)" + + meta = ArrayMetadata((5, 6), numpy.int32, strides=(24, 4)) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + meta = ArrayMetadata((5, 6), numpy.int32, strides=(48, 4)) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), strides=(48, 4))" + + def test_view(): meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4, -1:-5:-2] From e3d58657af91b39939fa8770cc19988c3ed768e3 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 20:31:34 -0700 Subject: [PATCH 07/12] Add a missing test for ArrayMetadata.from_arraylike --- grunnur/array_metadata.py | 2 +- tests/test_array_metadata.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 0909e16..005dfa2 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -46,7 +46,7 @@ class ArrayMetadata: @classmethod def from_arraylike(cls, array: ArrayMetadataLike) -> ArrayMetadata: - return cls(array.shape, array.dtype, strides=getattr(array, "strides", None)) + return cls(shape=array.shape, dtype=array.dtype, strides=getattr(array, "strides", None)) def __init__( self, diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index c16bcd4..66ca8a5 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -168,6 +168,20 @@ def test_repr(): assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), strides=(48, 4))" +def test_from_arraylike(): + meta = ArrayMetadata.from_arraylike(numpy.empty((5, 6), numpy.int32)) + assert meta.shape == (5, 6) + assert meta.dtype == numpy.int32 + assert meta.strides == (24, 4) + + meta = ArrayMetadata.from_arraylike( + ArrayMetadata(shape=(5, 6), dtype=numpy.int32, strides=(48, 4)) + ) + assert meta.shape == (5, 6) + assert meta.dtype == numpy.int32 + assert meta.strides == (48, 4) + + def test_view(): meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4, -1:-5:-2] From 259cd0c023aed2368a350bf732a5b1e9eafb13b8 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 22:00:00 -0700 Subject: [PATCH 08/12] Preserve parent buffer size in array views --- grunnur/array_metadata.py | 6 +++++- tests/test_array_metadata.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 005dfa2..18f0f6d 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -139,7 +139,11 @@ def __getitem__(self, slices: slice | tuple[slice, ...]) -> ArrayMetadata: slices += (slice(None),) * (len(self.shape) - len(slices)) new_fe_offset, new_shape, new_strides = _get_view(self.shape, self.strides, slices) return ArrayMetadata( - new_shape, self.dtype, strides=new_strides, first_element_offset=new_fe_offset + new_shape, + self.dtype, + strides=new_strides, + first_element_offset=new_fe_offset, + buffer_size=self.buffer_size, ) def __repr__(self) -> str: diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index 66ca8a5..8c09b57 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -188,6 +188,11 @@ def test_view(): ref_view = numpy.empty(meta.shape, meta.dtype)[1:4, -1:-5:-2] assert view.shape == ref_view.shape assert view.strides == ref_view.strides + # Note that the buffer size is taken from the parent array + assert view.buffer_size == meta.buffer_size + # The first element is [1, -1] of the original array + # (even though the first in memory will be the [1, -3] one) + assert view.first_element_offset == 1 * 4 * 6 + (6 - 1) * 4 meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4] # omitting the innermost slices From 5ce8260e859693dbe59390e88a43c3f274e36db0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 22:00:11 -0700 Subject: [PATCH 09/12] Add `ArrayMetadata.padded()` --- docs/history.rst | 1 + grunnur/array_metadata.py | 90 ++++++++++++++++++++++++++++-------- tests/test_array_metadata.py | 12 +++++ 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index 07281a5..19e7c5a 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -16,6 +16,7 @@ Added * Made ``ArrayMetadata`` public. (PR_18_) * Add ``metadata`` attribute to ``Array``. (PR_18_) +* ``ArrayMetadata.padded()``. (PR_18_) .. _PR_17: https://github.com/fjarri/grunnur/pull/17 diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 18f0f6d..0c4d5f3 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, runtime_checkable from .dtypes import _normalize_type @@ -26,6 +26,34 @@ def dtype(self) -> numpy.dtype[Any]: """The type of an array element.""" +class NormalizedArgs(NamedTuple): + shape: tuple[int, ...] + dtype: numpy.dtype[Any] + strides: tuple[int, ...] + default_strides: bool + + +def _normalize_args( + shape: Sequence[int] | int, + dtype: DTypeLike, + *, + strides: Sequence[int] | None = None, +) -> NormalizedArgs: + shape = tuple(shape) if isinstance(shape, Sequence) else (shape,) + + if len(shape) == 0: + raise ValueError("Array shape cannot be an empty sequence") + + dtype = _normalize_type(dtype) + + default_strides = _get_strides(shape, dtype.itemsize) + strides = default_strides if strides is None else tuple(strides) + + return NormalizedArgs( + shape=shape, dtype=dtype, strides=strides, default_strides=strides == default_strides + ) + + class ArrayMetadata: """ A helper object for array-like classes that handles shape/strides/buffer size checks @@ -48,30 +76,48 @@ class ArrayMetadata: def from_arraylike(cls, array: ArrayMetadataLike) -> ArrayMetadata: return cls(shape=array.shape, dtype=array.dtype, strides=getattr(array, "strides", None)) + @classmethod + def padded( + cls, + shape: Sequence[int] | int, + dtype: DTypeLike, + *, + pad: Sequence[int] | int, + ) -> ArrayMetadata: + normalized = _normalize_args(shape=shape, dtype=dtype) + pad = tuple(pad) if isinstance(pad, Sequence) else (pad,) * len(normalized.shape) + + if len(normalized.shape) != len(pad): + raise ValueError( + "`pad` must be either an integer or a sequence of the same length as `shape`" + ) + + padded_shape = [ + dim_len + dim_pad * 2 for dim_len, dim_pad in zip(normalized.shape, pad, strict=True) + ] + + # A little inefficiency here, we will be normalizing the arguments twice + full_metadata = cls(shape=padded_shape, dtype=normalized.dtype) + + slices = tuple( + slice(dim_pad, dim_len + dim_pad) + for dim_len, dim_pad in zip(normalized.shape, pad, strict=True) + ) + return full_metadata[slices] + def __init__( self, shape: Sequence[int] | int, dtype: DTypeLike, + *, strides: Sequence[int] | None = None, first_element_offset: int = 0, buffer_size: int | None = None, ): - shape = tuple(shape) if isinstance(shape, Sequence) else (shape,) - dtype = _normalize_type(dtype) - - if len(shape) == 0: - raise ValueError("Array shape cannot be an empty sequence") - - default_strides = _get_strides(shape, dtype.itemsize) - - if strides is None: - strides = default_strides - self.is_contiguous = True - else: - strides = tuple(strides) - # Technically, an array with non-default (e.g., overlapping) strides - # can be contioguous, but that's too hard to determine. - self.is_contiguous = strides == default_strides + normalized = _normalize_args(shape=shape, dtype=dtype, strides=strides) + shape = normalized.shape + dtype = normalized.dtype + strides = normalized.strides min_offset, max_offset = _get_range(shape, dtype.itemsize, strides) default_buffer_size = first_element_offset + max_offset @@ -96,11 +142,15 @@ def __init__( self.dtype = dtype self.strides = strides self.first_element_offset = first_element_offset - self._full_min_offset = full_min_offset - self._full_max_offset = full_max_offset self.buffer_size = buffer_size - self._default_strides = strides == default_strides + # Technically, an array with non-default (e.g., overlapping) strides + # can be contioguous, but that's too hard to determine. + self.is_contiguous = normalized.default_strides + + self._full_min_offset = full_min_offset + self._full_max_offset = full_max_offset + self._default_strides = normalized.default_strides self._default_buffer_size = buffer_size == default_buffer_size def __eq__(self, other: object) -> bool: diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index 8c09b57..d5f8e42 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -226,3 +226,15 @@ def test_minimal_subregion(): def test_empty_shape(): with pytest.raises(ValueError, match="Array shape cannot be an empty sequence"): ArrayMetadata((), numpy.int32) + + +def test_padded(): + meta = ArrayMetadata.padded((5, 6), numpy.int32, pad=(1, 2)) + # The offset is the full padded first line (6 + left pad + right pad) + # plus the pad of the first line (2) + assert meta.first_element_offset == ((6 + 2 + 2) + 2) * 4 + assert meta.buffer_size == (5 + 1 + 1) * (6 + 2 + 2) * 4 + + message = "`pad` must be either an integer or a sequence of the same length as `shape`" + with pytest.raises(ValueError, match=message): + ArrayMetadata.padded((5, 6), numpy.int32, pad=(1,)) From ab25cffe5db9d71d5b9d3fc4d1f51303fc7f803e Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 30 Jul 2024 22:15:48 -0700 Subject: [PATCH 10/12] Make the docstring of `bounding_power_of_2()` more clear --- grunnur/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/grunnur/utils.py b/grunnur/utils.py index 8622a1b..638ada1 100644 --- a/grunnur/utils.py +++ b/grunnur/utils.py @@ -36,10 +36,7 @@ def log2(num: int) -> int: def bounding_power_of_2(num: int) -> int: - """ - Returns the minimal number of the form ``2**m`` - such that it is greater or equal to ``n``. - """ + """Returns the minimum ``x`` such that ``x == 2**m >= num``.""" if num == 1: return 1 return 1 << (log2(num - 1) + 1) From 9fa0003e7f093fbb709c4ea580e927c2072b2aa5 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 31 Jul 2024 15:36:41 -0700 Subject: [PATCH 11/12] Rework array metadata --- docs/history.rst | 4 +- grunnur/array.py | 50 ++++++---- grunnur/array_metadata.py | 187 ++++++++++++++--------------------- tests/test_array.py | 22 ++++- tests/test_array_metadata.py | 145 +++++++++++---------------- 5 files changed, 181 insertions(+), 227 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index 19e7c5a..472e676 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -15,8 +15,8 @@ Added ^^^^^ * Made ``ArrayMetadata`` public. (PR_18_) -* Add ``metadata`` attribute to ``Array``. (PR_18_) -* ``ArrayMetadata.padded()``. (PR_18_) +* ``metadata`` attribute to ``Array``. (PR_18_) +* ``ArrayMetadata.buffer_size``, ``span``, ``min_offset``, ``first_element_offset``, and ``get_sub_region()``; ``Array.minimum_subregion()``. (PR_18_) .. _PR_17: https://github.com/fjarri/grunnur/pull/17 diff --git a/grunnur/array.py b/grunnur/array.py index 75c1928..e2cf3f6 100644 --- a/grunnur/array.py +++ b/grunnur/array.py @@ -33,6 +33,9 @@ class Array: strides: tuple[int, ...] """Array strides.""" + offset: int + """Offset of the first element in the associated buffer.""" + metadata: ArrayMetadata """Array metadata object.""" @@ -83,10 +86,10 @@ def empty( metadata = ArrayMetadata( shape, dtype, strides=strides, first_element_offset=first_element_offset ) - size = metadata.buffer_size + if allocator is None: allocator = Buffer.allocate - data = allocator(device, size) + data = allocator(device, metadata.buffer_size) return cls(metadata, data) @@ -96,29 +99,38 @@ def empty_like(cls, device: BoundDevice, array_like: ArrayMetadataLike) -> Array # TODO: take other information like strides and offset return cls.empty(device, array_like.shape, array_like.dtype) - def __init__(self, array_metadata: ArrayMetadata, data: Buffer): - self.metadata = array_metadata + def __init__(self, metadata: ArrayMetadata, data: Buffer): + if data.size < metadata.buffer_size: + raise ValueError( + f"The buffer size required by the given metadata ({metadata.buffer_size}) " + f"is larger than the given buffer size ({data.size})" + ) + + self.metadata = metadata self.device = data.device + self.shape = self.metadata.shape self.dtype = self.metadata.dtype self.strides = self.metadata.strides - self.first_element_offset = self.metadata.first_element_offset - self.buffer_size = self.metadata.buffer_size - - if data.size < self.buffer_size: - raise ValueError( - "Provided data buffer is not big enough to hold the array " - "(minimum required {self.buffer_size})" - ) self.data = data - def _view(self, slices: slice | tuple[slice, ...]) -> Array: - new_metadata = self.metadata[slices] - - origin, size, new_metadata = new_metadata.minimal_subregion() + def minimum_subregion(self) -> Array: + """ + Returns a new array with the same metadata and the buffer substituted with + the minimum-sized subregion of the original buffer, + such that all the elements described by the metadata still fit in it. + """ + # TODO: some platforms (e.g. POCL) require this to be aligned. + origin = self.metadata.min_offset + size = self.metadata.span data = self.data.get_sub_region(origin, size) - return Array(new_metadata, data) + metadata = self.metadata.get_sub_region(origin, size) + return Array(metadata, data) + + def __getitem__(self, slices: slice | tuple[slice, ...]) -> Array: + """Returns a view of this array.""" + return Array(self.metadata[slices], self.data) def set( self, @@ -170,10 +182,6 @@ def get( self.data.get(queue, dest, async_=async_) return dest - def __getitem__(self, slices: slice | tuple[slice, ...]) -> Array: - """Returns a view of this array.""" - return self._view(slices) - @runtime_checkable class ArrayLike(ArrayMetadataLike, Protocol): diff --git a/grunnur/array_metadata.py b/grunnur/array_metadata.py index 0c4d5f3..c8f1095 100644 --- a/grunnur/array_metadata.py +++ b/grunnur/array_metadata.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from .dtypes import _normalize_type @@ -26,34 +26,6 @@ def dtype(self) -> numpy.dtype[Any]: """The type of an array element.""" -class NormalizedArgs(NamedTuple): - shape: tuple[int, ...] - dtype: numpy.dtype[Any] - strides: tuple[int, ...] - default_strides: bool - - -def _normalize_args( - shape: Sequence[int] | int, - dtype: DTypeLike, - *, - strides: Sequence[int] | None = None, -) -> NormalizedArgs: - shape = tuple(shape) if isinstance(shape, Sequence) else (shape,) - - if len(shape) == 0: - raise ValueError("Array shape cannot be an empty sequence") - - dtype = _normalize_type(dtype) - - default_strides = _get_strides(shape, dtype.itemsize) - strides = default_strides if strides is None else tuple(strides) - - return NormalizedArgs( - shape=shape, dtype=dtype, strides=strides, default_strides=strides == default_strides - ) - - class ArrayMetadata: """ A helper object for array-like classes that handles shape/strides/buffer size checks @@ -69,41 +41,28 @@ class ArrayMetadata: strides: tuple[int, ...] """Array strides.""" + buffer_size: int + """The size of the buffer this array resides in.""" + + span: int + """The minimum size of the buffer that fits all the elements described by this metadata.""" + + min_offset: int + """The minimum offset of an array element described by this metadata.""" + + first_element_offset: int + """The offset of the first element (that is, the one with the all indices equal to 0).""" + is_contiguous: bool """If ``True``, means that array's data forms a continuous chunk of memory.""" @classmethod def from_arraylike(cls, array: ArrayMetadataLike) -> ArrayMetadata: - return cls(shape=array.shape, dtype=array.dtype, strides=getattr(array, "strides", None)) - - @classmethod - def padded( - cls, - shape: Sequence[int] | int, - dtype: DTypeLike, - *, - pad: Sequence[int] | int, - ) -> ArrayMetadata: - normalized = _normalize_args(shape=shape, dtype=dtype) - pad = tuple(pad) if isinstance(pad, Sequence) else (pad,) * len(normalized.shape) - - if len(normalized.shape) != len(pad): - raise ValueError( - "`pad` must be either an integer or a sequence of the same length as `shape`" - ) - - padded_shape = [ - dim_len + dim_pad * 2 for dim_len, dim_pad in zip(normalized.shape, pad, strict=True) - ] - - # A little inefficiency here, we will be normalizing the arguments twice - full_metadata = cls(shape=padded_shape, dtype=normalized.dtype) - - slices = tuple( - slice(dim_pad, dim_len + dim_pad) - for dim_len, dim_pad in zip(normalized.shape, pad, strict=True) + return cls( + shape=array.shape, + dtype=array.dtype, + strides=getattr(array, "strides", None), ) - return full_metadata[slices] def __init__( self, @@ -111,98 +70,102 @@ def __init__( dtype: DTypeLike, *, strides: Sequence[int] | None = None, - first_element_offset: int = 0, + first_element_offset: int | None = None, buffer_size: int | None = None, ): - normalized = _normalize_args(shape=shape, dtype=dtype, strides=strides) - shape = normalized.shape - dtype = normalized.dtype - strides = normalized.strides + shape = tuple(shape) if isinstance(shape, Sequence) else (shape,) - min_offset, max_offset = _get_range(shape, dtype.itemsize, strides) - default_buffer_size = first_element_offset + max_offset - if buffer_size is None: - buffer_size = default_buffer_size - - full_min_offset = first_element_offset + min_offset - if full_min_offset < 0 or full_min_offset + dtype.itemsize > buffer_size: - raise ValueError( - f"The minimum offset for given strides ({full_min_offset}) " - f"is outside the given buffer range ({buffer_size})" - ) - - full_max_offset = first_element_offset + max_offset - if full_max_offset > buffer_size: - raise ValueError( - f"The maximum offset for given strides ({full_max_offset}) " - f"is outside the given buffer range ({buffer_size})" - ) + if len(shape) == 0: + raise ValueError("Array shape cannot be an empty sequence") + + dtype = _normalize_type(dtype) + + default_strides = _get_strides(shape, dtype.itemsize) + strides = default_strides if strides is None else tuple(strides) self.shape = shape self.dtype = dtype self.strides = strides + + # Note that these are minimum and maximum offsets + # when the first element offset is 0. + min_offset, max_offset = _get_range(shape, dtype.itemsize, strides) + self.span = max_offset - min_offset + + if first_element_offset is None: + first_element_offset = -min_offset + elif first_element_offset < -min_offset: + raise ValueError(f"First element offset is smaller than the minimum {-min_offset}") self.first_element_offset = first_element_offset + self.min_offset = first_element_offset + min_offset + + min_buffer_size = self.first_element_offset + max_offset + if buffer_size is None: + buffer_size = min_buffer_size + elif buffer_size < min_buffer_size: + raise ValueError(f"Buffer size is smaller than the minimum {min_buffer_size}") self.buffer_size = buffer_size # Technically, an array with non-default (e.g., overlapping) strides # can be contioguous, but that's too hard to determine. - self.is_contiguous = normalized.default_strides + self.is_contiguous = strides == default_strides - self._full_min_offset = full_min_offset - self._full_max_offset = full_max_offset - self._default_strides = normalized.default_strides - self._default_buffer_size = buffer_size == default_buffer_size + self._default_strides = strides == default_strides - def __eq__(self, other: object) -> bool: + def _basis(self) -> tuple[numpy.dtype[Any], tuple[int, ...], tuple[int, ...], int, int]: return ( - isinstance(other, ArrayMetadata) - and self.shape == other.shape - and self.dtype == other.dtype - and self.strides == other.strides - and self.first_element_offset == other.first_element_offset - and self.buffer_size == other.buffer_size + self.dtype, + self.shape, + self.strides, + self.first_element_offset, + self.buffer_size, ) + def __eq__(self, other: object) -> bool: + return isinstance(other, ArrayMetadata) and self._basis() == other._basis() + def __hash__(self) -> int: - return hash((type(self), self.dtype, self.shape, self.strides, self.first_element_offset)) + return hash((type(self), self._basis())) - def minimal_subregion(self) -> tuple[int, int, ArrayMetadata]: + def get_sub_region(self, origin: int, size: int) -> ArrayMetadata: """ - Returns the metadata for the minimal subregion that fits all the data in this view, - along with the subgregion offset in the current buffer and the required subregion length. + Returns the same metadata shape-wise, but for the given subregion + of the original buffer. """ - subregion_origin = self._full_min_offset - subregion_size = self._full_max_offset - self._full_min_offset - new_metadata = ArrayMetadata( - self.shape, - self.dtype, + # The size errors will be checked by ArrayMetadata constructor + return ArrayMetadata( + shape=self.shape, + dtype=self.dtype, strides=self.strides, - first_element_offset=self.first_element_offset - self._full_min_offset, - buffer_size=subregion_size, + first_element_offset=self.first_element_offset - origin, + buffer_size=size, ) - return subregion_origin, subregion_size, new_metadata def __getitem__(self, slices: slice | tuple[slice, ...]) -> ArrayMetadata: + """ + Returns the view of this metadata with the given ranges, + with the offsets and buffer size corresponding to the original buffer. + """ if isinstance(slices, slice): slices = (slices,) if len(slices) < len(self.shape): slices += (slice(None),) * (len(self.shape) - len(slices)) - new_fe_offset, new_shape, new_strides = _get_view(self.shape, self.strides, slices) + offset, new_shape, new_strides = _get_view(self.shape, self.strides, slices) return ArrayMetadata( - new_shape, - self.dtype, + shape=new_shape, + dtype=self.dtype, strides=new_strides, - first_element_offset=new_fe_offset, + first_element_offset=self.first_element_offset + offset, buffer_size=self.buffer_size, ) def __repr__(self) -> str: args = [f"dtype={self.dtype}", f"shape={self.shape}"] - if self.first_element_offset != 0: - args.append(f"first_element_offset={self.first_element_offset}") if not self._default_strides: args.append(f"strides={self.strides}") - if not self._default_buffer_size: + if self.first_element_offset != 0: + args.append(f"first_element_offset={self.first_element_offset}") + if self.buffer_size != self.min_offset + self.span: args.append(f"buffer_size={self.buffer_size}") args_str = ", ".join(args) return f"ArrayMetadata({args_str})" diff --git a/tests/test_array.py b/tests/test_array.py index 6d74886..3d8b4f6 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -1,3 +1,5 @@ +import re + import numpy import pytest @@ -204,9 +206,11 @@ def test_custom_buffer(mock_context): metadata = ArrayMetadata.from_arraylike(arr) data = Buffer.allocate(context.device, 100) - with pytest.raises( - ValueError, match="Provided data buffer is not big enough to hold the array" - ): + message = re.escape( + "The buffer size required by the given metadata (400) " + "is larger than the given buffer size (100)" + ) + with pytest.raises(ValueError, match=message): Array(metadata, data) bigger_data = Buffer.allocate(context.device, arr.size * arr.dtype.itemsize) @@ -217,6 +221,18 @@ def test_custom_buffer(mock_context): assert (res == arr).all() +def test_minimum_subregion(mock_context): + context = mock_context + arr = Array.empty(context.device, (5, 6), numpy.int32) + arr_view = arr[1:4] + assert arr.data == arr_view.data + + arr_view_min = arr_view.minimum_subregion() + assert arr_view_min.metadata.first_element_offset == 0 + assert arr_view_min.metadata.buffer_size == 3 * 6 * 4 + assert arr_view_min.data.size == 3 * 6 * 4 + + def test_set_checks_shape(mock_context): context = mock_context queue = Queue(context.device) diff --git a/tests/test_array_metadata.py b/tests/test_array_metadata.py index d5f8e42..2daa6ba 100644 --- a/tests/test_array_metadata.py +++ b/tests/test_array_metadata.py @@ -66,76 +66,50 @@ def test_get_strides(): assert strides == ref.strides -def check_metadata(meta, *, check_max=False): +def check_metadata(meta): """ - Checks array metadata by creating a buffer of the size specified in the metadata - and trying to access every element there. + Checks array metadata by testing that address of every element of the array + lies within the range specified in the metadata. """ - buf = numpy.zeros(meta.buffer_size, numpy.uint8) - itemsize = meta.dtype.itemsize - for indices in itertools.product(*[range(length) for length in meta.shape]): - flat_idx = sum(idx * stride for idx, stride in zip(indices, meta.strides, strict=True)) - addr = flat_idx + meta.first_element_offset - buf[addr : addr + itemsize] = 1 + addresses = [ + sum(idx * stride for idx, stride in zip(indices, meta.strides, strict=True)) + for indices in itertools.product(*[range(length) for length in meta.shape]) + ] + + addresses = numpy.array(addresses) - nz = numpy.flatnonzero(buf) - min_addr = nz[0] - max_addr = nz[-1] + 1 - assert min_addr == meta.first_element_offset - if check_max: - assert max_addr == meta.buffer_size + assert addresses.max() + itemsize - addresses.min() == meta.span + assert meta.first_element_offset + addresses.min() == meta.min_offset + assert meta.first_element_offset + addresses.max() + itemsize <= meta.buffer_size def test_metadata_constructor(): # a scalar shape is converted into a tuple - check_metadata(ArrayMetadata([5], numpy.float64), check_max=True) + check_metadata(ArrayMetadata([5], numpy.float64)) # strides are created automatically if not provided meta = ArrayMetadata((6, 7), numpy.complex128) - check_metadata(meta, check_max=True) + check_metadata(meta) ref = numpy.empty((6, 7), numpy.complex128) assert meta.strides == ref.strides # setting strides manually - check_metadata(ArrayMetadata((6, 7), numpy.complex128, strides=[200, 20]), check_max=True) + check_metadata(ArrayMetadata((6, 7), numpy.complex128, strides=[200, 20])) + check_metadata(ArrayMetadata((6, 7), numpy.complex128, strides=[200, -20])) - # setting buffer size - check_metadata(ArrayMetadata((5, 6), numpy.complex64, strides=[100, 10], buffer_size=1000)) - - # Minimum offset is too small - message = re.escape( - "The minimum offset for given strides (-16) " "is outside the given buffer range (100)" - ) - with pytest.raises(ValueError, match=message): - meta = ArrayMetadata( - (4, 5), numpy.int32, strides=(20, -4), first_element_offset=0, buffer_size=100 - ) - - # Minimum offset is too big - message = re.escape( - "The minimum offset for given strides (120) " "is outside the given buffer range (100)" - ) - with pytest.raises(ValueError, match=message): - meta = ArrayMetadata( - (4, 5), numpy.int32, strides=(20, 4), first_element_offset=120, buffer_size=100 - ) - - # Maximum offset is too big - message = re.escape( - "The maximum offset for given strides (80) " "is outside the given buffer range (70)" - ) - with pytest.raises(ValueError, match=message): - meta = ArrayMetadata( - (4, 5), numpy.int32, strides=(20, 4), first_element_offset=0, buffer_size=70 - ) + # Buffer overflow + with pytest.raises(ValueError, match="First element offset is smaller than the minimum 20"): + ArrayMetadata((5, 6), numpy.int32, strides=(24, -4), first_element_offset=8) + with pytest.raises(ValueError, match="Buffer size is smaller than the minimum 120"): + ArrayMetadata((5, 6), numpy.int32, buffer_size=5 * 6 * 4 - 1) def test_eq(): meta1 = ArrayMetadata((5, 6), numpy.int32) meta2 = ArrayMetadata((5, 6), numpy.int32) - meta3 = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) + meta3 = ArrayMetadata((5, 6), numpy.int32, strides=(48, 4)) assert meta1 == meta2 assert meta1 != meta3 @@ -143,7 +117,7 @@ def test_eq(): def test_hash(): meta1 = ArrayMetadata((5, 6), numpy.int32) meta2 = ArrayMetadata((5, 6), numpy.int32) - meta3 = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) + meta3 = ArrayMetadata((5, 6), numpy.int32, strides=(48, 4)) assert hash(meta1) == hash(meta2) assert hash(meta1) != hash(meta3) @@ -152,6 +126,11 @@ def test_repr(): meta = ArrayMetadata((5, 6), numpy.int32) assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + meta = ArrayMetadata((5, 6), numpy.int32, strides=(24, 4)) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" + meta = ArrayMetadata((5, 6), numpy.int32, strides=(48, 4)) + assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), strides=(48, 4))" + meta = ArrayMetadata((5, 6), numpy.int32, first_element_offset=0) assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" meta = ArrayMetadata((5, 6), numpy.int32, first_element_offset=12) @@ -162,11 +141,6 @@ def test_repr(): meta = ArrayMetadata((5, 6), numpy.int32, buffer_size=512) assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), buffer_size=512)" - meta = ArrayMetadata((5, 6), numpy.int32, strides=(24, 4)) - assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6))" - meta = ArrayMetadata((5, 6), numpy.int32, strides=(48, 4)) - assert repr(meta) == "ArrayMetadata(dtype=int32, shape=(5, 6), strides=(48, 4))" - def test_from_arraylike(): meta = ArrayMetadata.from_arraylike(numpy.empty((5, 6), numpy.int32)) @@ -186,41 +160,31 @@ def test_view(): meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4, -1:-5:-2] ref_view = numpy.empty(meta.shape, meta.dtype)[1:4, -1:-5:-2] + # First element is [1, -1] == [1, 5] of the original array + assert view.first_element_offset == 1 * 6 * 4 + 5 * 4 assert view.shape == ref_view.shape assert view.strides == ref_view.strides - # Note that the buffer size is taken from the parent array assert view.buffer_size == meta.buffer_size - # The first element is [1, -1] of the original array - # (even though the first in memory will be the [1, -3] one) - assert view.first_element_offset == 1 * 4 * 6 + (6 - 1) * 4 + # The element with the minimum address is [1, -3] == [1, 3] of the original array + assert view.min_offset == 1 * 6 * 4 + 3 * 4 + # The element with the maximum address is [3, -1] == [3, 5] of the original array. + # It is located 2 * 4 * 6 (2 elements in the dimension 0 times the stride 4 * 6) + # bytes after the first element of the view. + # The minimum buffer size is the first element offset plus this offset plus the element size (4) + assert view.span == (view.first_element_offset + 2 * 4 * 6 + 4) - view.min_offset meta = ArrayMetadata((5, 6), numpy.int32) view = meta[1:4] # omitting the innermost slices ref_view = numpy.empty(meta.shape, meta.dtype)[1:4] + # First element is [1, 0] of the original array + assert view.first_element_offset == 1 * 6 * 4 assert view.shape == ref_view.shape assert view.strides == ref_view.strides - - -def test_minimal_subregion(): - meta = ArrayMetadata((5, 6), numpy.int32) - view = meta[1:4, -1:-5:-2] - origin, size, new_meta = view.minimal_subregion() - - # the address of the elem with the lowest address, that is (1, -3) == (1, 3) - assert origin == 1 * meta.strides[0] + 3 * meta.strides[1] - - # the distance between the lowest address ((1, -3) == (1, 3)) - # and the highest one ((3, -1) == (3, 5)) plus itemsize - assert size == ( - (3 * meta.strides[0] + 5 * meta.strides[1]) - + meta.dtype.itemsize - - (1 * meta.strides[0] + 3 * meta.strides[1]) - ) - - # The new first element address is the address of (1, -1) == (1, 5) - # minus the origin. - assert new_meta.first_element_offset == 1 * meta.strides[0] + 5 * meta.strides[1] - origin - assert new_meta.buffer_size == size + assert view.buffer_size == meta.buffer_size + # The leftmost element is also the first + assert view.min_offset == view.first_element_offset + # Three lines of stride 4 * 6 each + assert view.span == 3 * 4 * 6 def test_empty_shape(): @@ -228,13 +192,16 @@ def test_empty_shape(): ArrayMetadata((), numpy.int32) -def test_padded(): - meta = ArrayMetadata.padded((5, 6), numpy.int32, pad=(1, 2)) - # The offset is the full padded first line (6 + left pad + right pad) - # plus the pad of the first line (2) - assert meta.first_element_offset == ((6 + 2 + 2) + 2) * 4 - assert meta.buffer_size == (5 + 1 + 1) * (6 + 2 + 2) * 4 +def test_get_sub_region(): + meta = ArrayMetadata((5, 6), numpy.int32) + view = meta[1:4] + + view_region = view.get_sub_region(0, meta.buffer_size) + assert view_region.first_element_offset == view.first_element_offset + assert view_region.buffer_size == view.buffer_size - message = "`pad` must be either an integer or a sequence of the same length as `shape`" - with pytest.raises(ValueError, match=message): - ArrayMetadata.padded((5, 6), numpy.int32, pad=(1,)) + span = 3 * 6 * 4 # 3 lines of 6 elements of 4 bytes each + # The new first element offset will be 24 - 8 == 16 + view_region = view.get_sub_region(8, 16 + span + 1) + assert view_region.first_element_offset == view.first_element_offset - 8 + assert view_region.buffer_size == 16 + span + 1 From 4d25dca987daf51f12a4eea3bb703e39072f4165 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 31 Jul 2024 17:13:14 -0700 Subject: [PATCH 12/12] Rename no_async to sync --- docs/history.rst | 1 + grunnur/adapter_base.py | 2 +- grunnur/adapter_cuda.py | 6 +++--- grunnur/adapter_opencl.py | 4 ++-- grunnur/array.py | 12 ++++++------ grunnur/buffer.py | 6 +++--- grunnur/virtual_alloc.py | 4 ++-- tests/test_array.py | 4 ++-- tests/test_buffer.py | 10 +++++----- tests/test_virtual_alloc.py | 2 +- 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index 472e676..22956be 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -9,6 +9,7 @@ Changed ^^^^^^^ * ``local_mem`` keyword parameter of kernel calls renamed to ``cu_dynamic_local_mem``. (PR_17_) +* Renamed ``no_async`` keyword parameter to ``sync``. (PR_18_) Added diff --git a/grunnur/adapter_base.py b/grunnur/adapter_base.py index cb7545c..b201cf3 100644 --- a/grunnur/adapter_base.py +++ b/grunnur/adapter_base.py @@ -332,7 +332,7 @@ def set( queue_adapter: QueueAdapter, source: numpy.ndarray[Any, numpy.dtype[Any]] | BufferAdapter, *, - no_async: bool = False, + sync: bool = False, ) -> None: pass diff --git a/grunnur/adapter_cuda.py b/grunnur/adapter_cuda.py index 3189e41..26bd0f5 100644 --- a/grunnur/adapter_cuda.py +++ b/grunnur/adapter_cuda.py @@ -491,7 +491,7 @@ def set( queue_adapter: QueueAdapter, source: numpy.ndarray[Any, numpy.dtype[Any]] | BufferAdapter, *, - no_async: bool = False, + sync: bool = False, ) -> None: # Will be checked in the upper levels. assert isinstance(queue_adapter, CuQueueAdapter) # noqa: S101 @@ -503,7 +503,7 @@ def set( ptr = int(self._ptr) if isinstance(self._ptr, numpy.number) else self._ptr if isinstance(source, numpy.ndarray): - if no_async: + if sync: pycuda_driver.memcpy_htod(ptr, source) else: pycuda_driver.memcpy_htod_async(ptr, source, stream=queue_adapter._pycuda_stream) # noqa: SLF001 @@ -511,7 +511,7 @@ def set( # Will be checked in the upper levels. assert isinstance(source, CuBufferAdapter) # noqa: S101 buf_ptr = int(source._ptr) if isinstance(source._ptr, numpy.number) else source._ptr # noqa: SLF001 - if no_async: + if sync: pycuda_driver.memcpy_dtod(ptr, buf_ptr, source.size) else: pycuda_driver.memcpy_dtod_async( diff --git a/grunnur/adapter_opencl.py b/grunnur/adapter_opencl.py index 912b062..d8d61d9 100644 --- a/grunnur/adapter_opencl.py +++ b/grunnur/adapter_opencl.py @@ -491,7 +491,7 @@ def set( queue_adapter: QueueAdapter, source: numpy.ndarray[Any, numpy.dtype[Any]] | BufferAdapter, *, - no_async: bool = False, + sync: bool = False, ) -> None: # Will be checked in the upper levels. assert isinstance(queue_adapter, OclQueueAdapter) # noqa: S101 @@ -506,7 +506,7 @@ def set( # This keyword is only supported for transfers involving hosts in PyOpenCL kwds = {} if not isinstance(source, OclBufferAdapter): - kwds["is_blocking"] = no_async + kwds["is_blocking"] = sync pyopencl.enqueue_copy( queue_adapter._pyopencl_queue, # noqa: SLF001 diff --git a/grunnur/array.py b/grunnur/array.py index e2cf3f6..3402514 100644 --- a/grunnur/array.py +++ b/grunnur/array.py @@ -137,14 +137,14 @@ def set( queue: Queue, array: numpy.ndarray[Any, numpy.dtype[Any]] | Array, *, - no_async: bool = False, + sync: bool = False, ) -> None: """ Copies the contents of the host array to the array. :param queue: the queue to use for the transfer. :param array: the source array. - :param no_async: if `True`, the transfer blocks until completion. + :param sync: if `True`, the transfer blocks until completion. """ array_data: numpy.ndarray[Any, numpy.dtype[Any]] | Buffer if isinstance(array, numpy.ndarray): @@ -161,7 +161,7 @@ def set( if self.dtype != array.dtype: raise ValueError(f"Dtype mismatch: expected {self.dtype}, got {array.dtype}") - self.data.set(queue, array_data, no_async=no_async) + self.data.set(queue, array_data, sync=sync) def get( self, @@ -382,14 +382,14 @@ def set( mqueue: MultiQueue, array: numpy.ndarray[Any, numpy.dtype[Any]] | MultiArray, *, - no_async: bool = False, + sync: bool = False, ) -> None: """ Copies the contents of the host array to the array. :param mqueue: the queue to use for the transfer. :param array: the source array. - :param no_async: if `True`, the transfer blocks until completion. + :param sync: if `True`, the transfer blocks until completion. """ subarrays: Mapping[BoundDevice, Array | numpy.ndarray[Any, numpy.dtype[Any]]] if isinstance(array, numpy.ndarray): @@ -403,4 +403,4 @@ def set( raise ValueError("Mismatched device sets in the source and the destination") for device in self.subarrays: - self.subarrays[device].set(mqueue.queues[device], subarrays[device], no_async=no_async) + self.subarrays[device].set(mqueue.queues[device], subarrays[device], sync=sync) diff --git a/grunnur/buffer.py b/grunnur/buffer.py index 3868fad..68a67eb 100644 --- a/grunnur/buffer.py +++ b/grunnur/buffer.py @@ -55,14 +55,14 @@ def set( queue: Queue, buf: numpy.ndarray[Any, numpy.dtype[Any]] | Buffer, *, - no_async: bool = False, + sync: bool = False, ) -> None: """ Copy the contents of the host array or another buffer to this buffer. :param queue: the queue to use for the transfer. :param buf: the source - ``numpy`` array or a :py:class:`Buffer` object. - :param no_async: if `True`, the transfer blocks until completion. + :param sync: if `True`, the transfer blocks until completion. """ if queue.device != self.device: raise ValueError( @@ -78,7 +78,7 @@ def set( else: raise TypeError(f"Cannot set from an object of type {type(buf)}") - self._buffer_adapter.set(queue._queue_adapter, buf_adapter, no_async=no_async) # noqa: SLF001 + self._buffer_adapter.set(queue._queue_adapter, buf_adapter, sync=sync) # noqa: SLF001 def get( self, diff --git a/grunnur/virtual_alloc.py b/grunnur/virtual_alloc.py index 0bca368..a0c7203 100644 --- a/grunnur/virtual_alloc.py +++ b/grunnur/virtual_alloc.py @@ -105,9 +105,9 @@ def set( queue_adapter: QueueAdapter, source: numpy.ndarray[Any, numpy.dtype[Any]] | BufferAdapter, *, - no_async: bool = False, + sync: bool = False, ) -> None: - return self._real_buffer_adapter.set(queue_adapter, source, no_async=no_async) + return self._real_buffer_adapter.set(queue_adapter, source, sync=sync) @property def offset(self) -> int: diff --git a/tests/test_array.py b/tests/test_array.py index 3d8b4f6..9e8960d 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -31,7 +31,7 @@ def _check_array_operations(queue, array_cls): # sync set arr2 = numpy.arange(100) + 2 - arr_dev.set(queue, arr2, no_async=True) + arr_dev.set(queue, arr2, sync=True) assert (arr_dev.get(queue) == arr2).all() # async set from another array @@ -43,7 +43,7 @@ def _check_array_operations(queue, array_cls): # sync set from another array arr2 = numpy.arange(100) + 4 arr2_dev = array_cls.from_host(queue, arr2) - arr_dev.set(queue, arr2_dev, no_async=True) + arr_dev.set(queue, arr2_dev, sync=True) assert (arr_dev.get(queue) == arr2).all() diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 215061c..f5ce00b 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -24,7 +24,7 @@ def test_transfer(mock_or_real_context, sync): queue = Queue(context.device) - buf.set(queue, arr, no_async=sync) + buf.set(queue, arr, sync=sync) # Read the whole buffer res = numpy.empty_like(arr) @@ -36,7 +36,7 @@ def test_transfer(mock_or_real_context, sync): # Device-to-device copy res = numpy.empty_like(arr) buf2 = Buffer.allocate(context.device, size) - buf2.set(queue, buf, no_async=sync) + buf2.set(queue, buf, sync=sync) buf2.get(queue, res, async_=not sync) if not sync: queue.synchronize() @@ -76,7 +76,7 @@ def test_subregion(mock_or_real_context, sync): res = numpy.empty_like(arr) arr_region = (numpy.ones(50) * region_length).astype(dtype) arr[region_offset : region_offset + region_length] = arr_region - buf_region.set(queue, arr_region, no_async=sync) + buf_region.set(queue, arr_region, sync=sync) buf.get(queue, res, async_=not sync) if not sync: queue.synchronize() @@ -96,7 +96,7 @@ def test_subregion_copy(mock_or_real_context, sync): buf = Buffer.allocate(context.device, size) queue = Queue(context.device) - buf.set(queue, arr, no_async=sync) + buf.set(queue, arr, sync=sync) region_offset = 64 region_length = 100 @@ -105,7 +105,7 @@ def test_subregion_copy(mock_or_real_context, sync): buf2 = Buffer.allocate(context.device, size * 2) buf2.set(queue, numpy.ones(length * 2, dtype)) buf2_view = buf2.get_sub_region(region_offset * dtype.itemsize, region_length * dtype.itemsize) - buf2_view.set(queue, buf, no_async=sync) + buf2_view.set(queue, buf, sync=sync) res2 = numpy.empty(length * 2, dtype) buf2.get(queue, res2, async_=not sync) if not sync: diff --git a/tests/test_virtual_alloc.py b/tests/test_virtual_alloc.py index f9b2808..c235d03 100644 --- a/tests/test_virtual_alloc.py +++ b/tests/test_virtual_alloc.py @@ -102,7 +102,7 @@ def test_contract_mocked(mock_backend_pycuda, mock_context_pycuda, valloc_cls, p for i, metadata in enumerate(buffers_metadata): name, _size, deps = metadata - buffers[name].set(queue, numpy.ones(buffers[name].size, numpy.uint8) * i, no_async=True) + buffers[name].set(queue, numpy.ones(buffers[name].size, numpy.uint8) * i, sync=True) # According to the virtual allocator contract, the allocated buffer # will not intersect with the buffers from the specified dependencies. # So we're filling the buffer and checking that the dependencies did not change.