Skip to content

Commit c82e0d8

Browse files
authored
refactor: update StridedMemoryView.from_buffer to accept tuples and dtype instead of now-private _StridedLayout (NVIDIA#1380)
* refactor: update `StridedMemoryView.from_buffer` to accept tuples and dtype instead of now-private `_StridedLayout` * chore: remove `order`, add `itemsize` to `from_buffer` to avoid needing consistency checks * test: add test for failure modes * chore: privatize `StridedMemoryView._layout` * docs: remove references to `_StridedLayout` * refactor: remove `_StridedLayout` import * refactor: remove `_StridedLayout` from error message
1 parent 5d11fa3 commit c82e0d8

5 files changed

Lines changed: 89 additions & 61 deletions

File tree

cuda_core/cuda/core/experimental/_layout.pxd

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,7 @@ cdef class _StridedLayout:
297297
raise ValueError(
298298
f"Allocation size for a layout that maps elements "
299299
f"to negative memory offsets is ambiguous. "
300-
f"The layout's min_offset is {min_offset}. "
301-
f"To create a supported layout with the same shape "
302-
f"please use _StridedLayout.to_dense()."
300+
f"The layout's min_offset is {min_offset}."
303301
)
304302
if max_offset < min_offset:
305303
return 0

cuda_core/cuda/core/experimental/_layout.pyx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ cdef class _StridedLayout:
4848

4949
def __init__(
5050
self : _StridedLayout,
51-
shape : tuple[int],
52-
strides : tuple[int] | None,
51+
shape : tuple[int, ...],
52+
strides : tuple[int, ...] | None,
5353
itemsize : int,
5454
divide_strides : bool = False
5555
) -> None:
@@ -455,7 +455,7 @@ cdef class _StridedLayout:
455455
a_view = StridedMemoryView(a, -1)
456456
# get the original layout of ``a`` and convert it to a dense layout
457457
# to avoid overallocating memory (e.g. if the ``a`` was sliced)
458-
layout = a_view.layout.to_dense()
458+
layout = a_view._layout.to_dense()
459459
# get the required size in bytes to fit the tensor
460460
required_size = layout.required_size_in_bytes()
461461
# allocate the memory on the device
@@ -669,12 +669,12 @@ cdef class _StridedLayout:
669669
# Viewing (5, 6) float array as (5, 3) complex64 array.
670670
a = numpy.ones((5, 6), dtype=numpy.float32)
671671
float_view = StridedMemoryView(a, -1)
672-
layout = float_view.layout
672+
layout = float_view._layout
673673
assert layout.shape == (5, 6)
674674
assert layout.itemsize == 4
675675
complex_view = float_view.view(layout.repacked(8), numpy.complex64)
676-
assert complex_view.layout.shape == (5, 3)
677-
assert complex_view.layout.itemsize == 8
676+
assert complex_view._layout.shape == (5, 3)
677+
assert complex_view._layout.itemsize == 8
678678
b = numpy.from_dlpack(complex_view)
679679
assert b.shape == (5, 3)
680680
"""

cuda_core/cuda/core/experimental/_memoryview.pyx

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ cdef class StridedMemoryView:
2828
2929
1. Using the :obj:`args_viewable_as_strided_memory` decorator (recommended)
3030
2. Explicit construction relying on DLPack or CUDA Array Interface, see below.
31-
3. From :obj:`~_memory.Buffer` and a :obj:`_StridedLayout` (see :meth:`from_buffer` classmethod)
31+
3. From :obj:`~_memory.Buffer` and shape and size tuples (see
32+
:meth:`from_buffer` classmethod)
3233
3334
``StridedMemoryView(obj, stream_ptr)`` can be used to create a view from
3435
objects supporting either DLPack (up to v1.0) or CUDA Array Interface
@@ -160,22 +161,20 @@ cdef class StridedMemoryView:
160161

161162
@classmethod
162163
def from_buffer(
163-
cls, buffer : Buffer, layout : _StridedLayout,
164+
cls,
165+
buffer : Buffer,
166+
shape : tuple[int, ...],
167+
strides : tuple[int, ...] | None = None,
168+
*,
169+
itemsize : int | None = None,
164170
dtype : numpy.dtype | None = None,
165171
is_readonly : bool = False
166172
) -> StridedMemoryView:
167173
"""
168-
Creates a :obj:`StridedMemoryView` instance from a :obj:`~_memory.Buffer` and a :obj:`_StridedLayout`.
174+
Creates a :obj:`StridedMemoryView` instance from a :obj:`~_memory.Buffer` and shape and strides tuples.
169175
The Buffer can be either allocation coming from a :obj:`MemoryResource` or an external allocation
170176
wrapped in a :obj:`~_memory.Buffer` object with ``Buffer.from_handle(ptr, size, owner=...)``.
171177

172-
.. hint::
173-
When allocating the memory for a given layout, the required allocation size
174-
can be obtained with the :meth:`_StridedLayout.required_size_in_bytes` method.
175-
It is best to use the :meth:`_StridedLayout.to_dense` method
176-
first to make sure the layout is contiguous, to avoid overallocating memory
177-
for layouts with gaps.
178-
179178
.. caution::
180179
When creating a :obj:`StridedMemoryView` from a :obj:`~_memory.Buffer`,
181180
no synchronization is performed. It is the user's responsibility to ensure
@@ -185,19 +184,33 @@ cdef class StridedMemoryView:
185184
----------
186185
buffer : :obj:`~_memory.Buffer`
187186
The buffer to create the view from.
188-
layout : :obj:`_StridedLayout`
187+
shape : :obj:`tuple`
188+
The layout describing the shape, strides and itemsize of the elements in
189+
the buffer.
190+
strides : :obj:`tuple`
189191
The layout describing the shape, strides and itemsize of the elements in
190192
the buffer.
191-
dtype : :obj:`numpy.dtype`, optional
193+
dtype : :obj:`numpy.dtype`
192194
Optional dtype.
193195
If specified, the dtype's itemsize must match the layout's itemsize.
194-
To view the buffer with a different itemsize, please use :meth:`_StridedLayout.repacked`
195-
first to transform the layout to the desired itemsize.
196196
is_readonly : bool, optional
197197
Whether the mark the view as readonly.
198198
"""
199199
cdef StridedMemoryView view = StridedMemoryView.__new__(cls)
200-
view_buffer_strided(view, buffer, layout, dtype, is_readonly)
200+
if itemsize is None and dtype is None:
201+
raise ValueError("Either itemsize or dtype must be specified")
202+
if itemsize is not None and dtype is not None and itemsize != dtype.itemsize:
203+
raise ValueError(
204+
f"itemsize ({itemsize}) does not match dtype.itemsize ({dtype.itemsize})"
205+
)
206+
# (itemsize is None XOR dtype is None) OR they are equal
207+
view_buffer_strided(
208+
view,
209+
buffer,
210+
_StridedLayout(shape=shape, strides=strides, itemsize=getattr(dtype, "itemsize", itemsize)),
211+
dtype,
212+
is_readonly,
213+
)
201214
return view
202215

203216
def __dealloc__(self):
@@ -245,22 +258,14 @@ cdef class StridedMemoryView:
245258
The copy can be performed between following memory spaces:
246259
host-to-device, device-to-host, device-to-device (on the same device).
247260
248-
The following conditions must be met:
249-
* Both views must have compatible shapes, i.e. the shapes must be equal
250-
or the source view's shape must be broadcastable to the target view's shape
251-
(see :meth:`_StridedLayout.broadcast_to`).
252-
* Both views must have the same :attr:`dtype` (or :attr:`_StridedLayout.itemsize`
253-
if :attr:`dtype` is not specified).
254-
* The destination's layout must be unique (see :meth:`_StridedLayout.is_unique`).
255-
256261
Parameters
257262
----------
258263
other : StridedMemoryView
259264
The view to copy data from.
260265
stream : Stream | None, optional
261266
The stream to schedule the copy on.
262267
allocator : MemoryResource | None, optional
263-
If temporary buffers are needed, the specifed memory resources
268+
If temporary buffers are needed, the specified memory resources
264269
will be used to allocate the memory. If not specified, default
265270
resources will be used.
266271
blocking : bool | None, optional
@@ -289,7 +294,7 @@ cdef class StridedMemoryView:
289294
raise NotImplementedError("Sorry, not supported: copy_to")
290295

291296
@property
292-
def layout(self) -> _StridedLayout:
297+
def _layout(self) -> _StridedLayout:
293298
"""
294299
The layout of the tensor. For StridedMemoryView created from DLPack or CAI,
295300
the layout is inferred from the tensor object's metadata.
@@ -325,7 +330,7 @@ cdef class StridedMemoryView:
325330
return (f"StridedMemoryView(ptr={self.ptr},\n"
326331
+ f" shape={self.shape},\n"
327332
+ f" strides={self.strides},\n"
328-
+ f" itemsize={self.layout.itemsize},\n"
333+
+ f" itemsize={self._layout.itemsize},\n"
329334
+ f" dtype={get_simple_repr(self.dtype)},\n"
330335
+ f" device_id={self.device_id},\n"
331336
+ f" is_device_accessible={self.is_device_accessible},\n"
@@ -677,8 +682,7 @@ cdef inline int view_buffer_strided(
677682
if dtype.itemsize != layout.itemsize:
678683
raise ValueError(
679684
f"The dtype's itemsize ({dtype.itemsize}) does not match the layout's "
680-
f"itemsize ({layout.itemsize}). Please use :meth:`_StridedLayout.repacked` "
681-
f"to transform the layout to the desired itemsize."
685+
f"itemsize ({layout.itemsize})."
682686
)
683687
# Check the layout's offset range [min_offset, max_offset] fits
684688
# within the [0, buffer.size - 1] range.

cuda_core/cuda/core/experimental/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from cuda.core.experimental._layout import _StridedLayout # noqa: F401
65
from cuda.core.experimental._memoryview import (
76
StridedMemoryView, # noqa: F401
87
args_viewable_as_strided_memory, # noqa: F401

cuda_core/tests/test_utils.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import numpy as np
1717
import pytest
1818
from cuda.core.experimental import Device
19-
from cuda.core.experimental.utils import StridedMemoryView, _StridedLayout, args_viewable_as_strided_memory
19+
from cuda.core.experimental._layout import _StridedLayout
20+
from cuda.core.experimental.utils import StridedMemoryView, args_viewable_as_strided_memory
2021

2122

2223
def test_cast_to_3_tuple_success():
@@ -234,44 +235,60 @@ def _dense_strides(shape, stride_order):
234235
return tuple(strides)
235236

236237

237-
@pytest.mark.parametrize("shape", [tuple(), (2, 3), (10, 10), (10, 13, 11)])
238-
@pytest.mark.parametrize("itemsize", [1, 4])
238+
@pytest.mark.parametrize("shape", [tuple(), (2, 3), (10, 10), (10, 13, 11)], ids=str)
239+
@pytest.mark.parametrize("dtype", [np.dtype(np.int8), np.dtype(np.uint32)], ids=str)
239240
@pytest.mark.parametrize("stride_order", ["C", "F"])
240241
@pytest.mark.parametrize("readonly", [True, False])
241-
def test_from_buffer(shape, itemsize, stride_order, readonly):
242+
def test_from_buffer(shape, dtype, stride_order, readonly):
242243
dev = Device()
243244
dev.set_current()
244-
layout = _StridedLayout.dense(shape=shape, itemsize=itemsize, stride_order=stride_order)
245+
layout = _StridedLayout.dense(shape=shape, itemsize=dtype.itemsize, stride_order=stride_order)
245246
required_size = layout.required_size_in_bytes()
246-
assert required_size == math.prod(shape) * itemsize
247+
assert required_size == math.prod(shape) * dtype.itemsize
247248
buffer = dev.memory_resource.allocate(required_size)
248-
view = StridedMemoryView.from_buffer(buffer, layout, is_readonly=readonly)
249+
view = StridedMemoryView.from_buffer(buffer, shape=shape, strides=layout.strides, dtype=dtype, is_readonly=readonly)
249250
assert view.exporting_obj is buffer
250-
assert view.layout is layout
251+
assert view._layout == layout
251252
assert view.ptr == int(buffer.handle)
252253
assert view.shape == shape
253254
assert view.strides == _dense_strides(shape, stride_order)
254-
assert view.dtype is None
255+
assert view.dtype == dtype
255256
assert view.device_id == dev.device_id
256257
assert view.is_device_accessible
257258
assert view.readonly == readonly
258259

259260

261+
@pytest.mark.parametrize(
262+
("dtype", "itemsize", "msg"),
263+
[
264+
(np.dtype("int16"), 1, "itemsize .+ does not match dtype.itemsize .+"),
265+
(None, None, "itemsize or dtype must be specified"),
266+
],
267+
)
268+
def test_from_buffer_incompatible_dtype_and_itemsize(dtype, itemsize, msg):
269+
layout = _StridedLayout.dense((5,), 2)
270+
device = Device()
271+
device.set_current()
272+
buffer = device.memory_resource.allocate(layout.required_size_in_bytes())
273+
with pytest.raises(ValueError, match=msg):
274+
StridedMemoryView.from_buffer(buffer, (5,), dtype=dtype, itemsize=itemsize)
275+
276+
260277
@pytest.mark.parametrize("stride_order", ["C", "F"])
261278
def test_from_buffer_sliced(stride_order):
262279
layout = _StridedLayout.dense((5, 7), 2, stride_order=stride_order)
263280
device = Device()
264281
device.set_current()
265282
buffer = device.memory_resource.allocate(layout.required_size_in_bytes())
266-
view = StridedMemoryView.from_buffer(buffer, layout)
283+
view = StridedMemoryView.from_buffer(buffer, (5, 7), dtype=np.dtype(np.int16))
267284
assert view.shape == (5, 7)
268285
assert int(buffer.handle) == view.ptr
269286

270287
sliced_view = view.view(layout[:-2, 3:])
271288
assert sliced_view.shape == (3, 4)
272289
expected_offset = 3 if stride_order == "C" else 3 * 5
273-
assert sliced_view.layout.slice_offset == expected_offset
274-
assert sliced_view.layout.slice_offset_in_bytes == expected_offset * 2
290+
assert sliced_view._layout.slice_offset == expected_offset
291+
assert sliced_view._layout.slice_offset_in_bytes == expected_offset * 2
275292
assert sliced_view.ptr == view.ptr + expected_offset * 2
276293
assert int(buffer.handle) + expected_offset * 2 == sliced_view.ptr
277294

@@ -282,16 +299,26 @@ def test_from_buffer_too_small():
282299
d.set_current()
283300
buffer = d.memory_resource.allocate(20)
284301
with pytest.raises(ValueError, match="Expected at least 40 bytes, got 20 bytes."):
285-
StridedMemoryView.from_buffer(buffer, layout)
302+
StridedMemoryView.from_buffer(
303+
buffer,
304+
shape=layout.shape,
305+
strides=layout.strides,
306+
dtype=np.dtype("int16"),
307+
)
286308

287309

288310
def test_from_buffer_disallowed_negative_offset():
289311
layout = _StridedLayout((5, 4), (-4, 1), 1)
290312
d = Device()
291313
d.set_current()
292314
buffer = d.memory_resource.allocate(20)
293-
with pytest.raises(ValueError, match="please use _StridedLayout.to_dense()."):
294-
StridedMemoryView.from_buffer(buffer, layout)
315+
with pytest.raises(ValueError):
316+
StridedMemoryView.from_buffer(
317+
buffer,
318+
shape=layout.shape,
319+
strides=layout.strides,
320+
dtype=np.dtype("uint8"),
321+
)
295322

296323

297324
class _EnforceCAIView:
@@ -331,7 +358,7 @@ def test_view_sliced_external(shape, slices, stride_order, view_as):
331358
pytest.skip("CuPy is not installed")
332359
a = cp.arange(math.prod(shape), dtype=cp.int32).reshape(shape, order=stride_order)
333360
view = StridedMemoryView.from_cuda_array_interface(_EnforceCAIView(a), -1)
334-
layout = view.layout
361+
layout = view._layout
335362
assert layout.is_dense
336363
assert layout.required_size_in_bytes() == a.nbytes
337364
assert view.ptr == _get_ptr(a)
@@ -344,11 +371,11 @@ def test_view_sliced_external(shape, slices, stride_order, view_as):
344371

345372
assert 0 <= sliced_layout.required_size_in_bytes() <= a.nbytes
346373
assert not sliced_layout.is_dense
347-
assert sliced_view.layout is sliced_layout
374+
assert sliced_view._layout is sliced_layout
348375
assert view.dtype == sliced_view.dtype
349-
assert sliced_view.layout.itemsize == a_sliced.itemsize == layout.itemsize
376+
assert sliced_view._layout.itemsize == a_sliced.itemsize == layout.itemsize
350377
assert sliced_view.shape == a_sliced.shape
351-
assert sliced_view.layout.strides_in_bytes == a_sliced.strides
378+
assert sliced_view._layout.strides_in_bytes == a_sliced.strides
352379

353380

354381
@pytest.mark.parametrize(
@@ -369,7 +396,7 @@ def test_view_sliced_external_negative_offset(stride_order, view_as):
369396
a = cp.arange(math.prod(shape), dtype=cp.int32).reshape(shape, order=stride_order)
370397
a = a[::-1]
371398
view = StridedMemoryView.from_cuda_array_interface(_EnforceCAIView(a), -1)
372-
layout = view.layout
399+
layout = view._layout
373400
assert not layout.is_dense
374401
assert layout.strides == (-1,)
375402
assert view.ptr == _get_ptr(a)
@@ -381,8 +408,8 @@ def test_view_sliced_external_negative_offset(stride_order, view_as):
381408
assert sliced_view.ptr == view.ptr - 3 * a.itemsize
382409

383410
assert not sliced_layout.is_dense
384-
assert sliced_view.layout is sliced_layout
411+
assert sliced_view._layout is sliced_layout
385412
assert view.dtype == sliced_view.dtype
386-
assert sliced_view.layout.itemsize == a_sliced.itemsize == layout.itemsize
413+
assert sliced_view._layout.itemsize == a_sliced.itemsize == layout.itemsize
387414
assert sliced_view.shape == a_sliced.shape
388-
assert sliced_view.layout.strides_in_bytes == a_sliced.strides
415+
assert sliced_view._layout.strides_in_bytes == a_sliced.strides

0 commit comments

Comments
 (0)