Skip to content

Commit 6bcd2fe

Browse files
committed
switch to a context manager
1 parent 5d0440d commit 6bcd2fe

5 files changed

Lines changed: 108 additions & 28 deletions

File tree

python/rapidsmpf/rapidsmpf/memory/buffer.pxd

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
from libc.stddef cimport size_t
55
from libcpp.memory cimport unique_ptr
6+
from rmm.pylibrmm.stream cimport Stream
7+
8+
from rapidsmpf.memory.buffer_resource cimport BufferResource
69

710

811
cdef extern from "<rapidsmpf/memory/buffer.hpp>" namespace "rapidsmpf" nogil:
@@ -13,14 +16,21 @@ cdef extern from "<rapidsmpf/memory/buffer.hpp>" namespace "rapidsmpf" nogil:
1316

1417
cdef cppclass cpp_Buffer "rapidsmpf::Buffer":
1518
size_t size
16-
# data() actually returns const std::byte*, declared as const void* here
17-
# because const std::byte* -> const void* is an implicit C++ conversion.
18-
const void* data() except +
19+
# exclusive_data_access() returns std::byte*; void* works because
20+
# non-const std::byte* -> void* is an implicit C++ conversion.
21+
void* exclusive_data_access() except +
22+
void unlock() noexcept
1923
MemoryType mem_type() noexcept
2024

2125

26+
cdef class BufferHostView:
27+
cdef Buffer _buf
28+
29+
2230
cdef class Buffer:
2331
cdef unique_ptr[cpp_Buffer] _handle
32+
cdef BufferResource _br
33+
cdef Stream _stream
2434

2535
@staticmethod
26-
cdef Buffer from_handle(unique_ptr[cpp_Buffer] handle)
36+
cdef Buffer from_handle(unique_ptr[cpp_Buffer] handle, BufferResource br, Stream stream)
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
from contextlib import AbstractContextManager
45
from enum import IntEnum
56

67
class MemoryType(IntEnum):
78
DEVICE = ...
89
PINNED_HOST = ...
910
HOST = ...
1011

12+
class BufferHostView(AbstractContextManager[memoryview]):
13+
def __enter__(self) -> memoryview: ...
14+
def __exit__(self, *args: object) -> None: ...
15+
1116
class Buffer:
1217
@property
1318
def size(self) -> int: ...
1419
@property
1520
def mem_type(self) -> MemoryType: ...
16-
def __buffer__(self, flags: int, /) -> memoryview: ...
21+
def host_view(self) -> BufferHostView: ...

python/rapidsmpf/rapidsmpf/memory/buffer.pyx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
from cpython.buffer cimport PyBuffer_FillInfo
4+
from cpython.buffer cimport PyBUF_WRITE
5+
from cpython.memoryview cimport PyMemoryView_FromMemory
56
from cython.operator cimport dereference as deref
67
from libcpp.utility cimport move
8+
from rmm.pylibrmm.stream cimport Stream
9+
10+
from rapidsmpf.memory.buffer_resource cimport BufferResource
11+
12+
13+
cdef class BufferHostView:
14+
def __cinit__(self, Buffer buf):
15+
self._buf = buf
16+
17+
def __enter__(self):
18+
cdef void* ptr = deref(self._buf._handle).exclusive_data_access()
19+
try:
20+
return PyMemoryView_FromMemory(
21+
<char*>ptr, <Py_ssize_t>deref(self._buf._handle).size, PyBUF_WRITE
22+
)
23+
except BaseException:
24+
deref(self._buf._handle).unlock()
25+
raise
26+
27+
def __exit__(self, exc_type, exc_val, exc_tb):
28+
deref(self._buf._handle).unlock()
29+
return False
730

831

932
cdef class Buffer:
@@ -22,9 +45,11 @@ cdef class Buffer:
2245
self._handle.reset()
2346

2447
@staticmethod
25-
cdef Buffer from_handle(unique_ptr[cpp_Buffer] handle):
48+
cdef Buffer from_handle(unique_ptr[cpp_Buffer] handle, BufferResource br, Stream stream):
2649
cdef Buffer self = Buffer.__new__(Buffer)
2750
self._handle = move(handle)
51+
self._br = br
52+
self._stream = stream
2853
return self
2954

3055
@property
@@ -37,16 +62,37 @@ cdef class Buffer:
3762
"""Memory type of this buffer."""
3863
return deref(self._handle).mem_type()
3964

40-
def __getbuffer__(self, Py_buffer* view, int flags):
65+
def host_view(self):
66+
"""Context manager providing exclusive writable host access to the buffer.
67+
68+
Acquires an exclusive lock on the buffer for the duration of the ``with``
69+
block, preventing concurrent stream-ordered operations on the C++ side.
70+
The lock is released (and the returned ``memoryview`` must not be used)
71+
once the block exits.
72+
73+
Returns
74+
-------
75+
BufferHostView
76+
A context manager that yields a writable ``memoryview`` of the buffer.
77+
78+
Raises
79+
------
80+
TypeError
81+
If the buffer is not a host buffer (``HOST`` or ``PINNED_HOST``).
82+
std::logic_error
83+
If the buffer is already locked or a stream-ordered write is still
84+
in flight (``is_latest_write_done() == False``).
85+
86+
Examples
87+
--------
88+
>>> with buf.host_view() as mv:
89+
... mv[:] = b"\\x00" * buf.size
90+
"""
4191
if deref(self._handle).mem_type() not in {
4292
MemoryType.HOST, MemoryType.PINNED_HOST
4393
}:
4494
raise TypeError(
45-
"buffer protocol is only supported for host buffers "
95+
"host_view() is only supported for host buffers "
4696
"(MemoryType.HOST or MemoryType.PINNED_HOST)"
4797
)
48-
cdef void* ptr = <void*><const void*>deref(self._handle).data()
49-
PyBuffer_FillInfo(view, self, ptr, deref(self._handle).size, False, flags)
50-
51-
def __releasebuffer__(self, Py_buffer* view):
52-
pass
98+
return BufferHostView(self)

python/rapidsmpf/rapidsmpf/memory/buffer_resource.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ cdef class BufferResource:
595595
size, stream.view(), deref(reservation._handle)
596596
)
597597
)
598-
return Buffer.from_handle(move(handle))
598+
return Buffer.from_handle(move(handle), self, stream)
599599

600600
@property
601601
def statistics(self):

python/rapidsmpf/rapidsmpf/tests/test_buffer.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,37 +63,41 @@ def test_make_buffer(mem_type: MemoryType) -> None:
6363

6464

6565
@skip_if_no_pinned
66-
def test_buffer_protocol_pinned_host(pinned_br: BufferResource) -> None:
66+
def test_host_view_pinned_host(pinned_br: BufferResource) -> None:
6767
size = 256
6868
stream = Stream()
6969
reservation, _ = pinned_br.reserve(
7070
MemoryType.PINNED_HOST, size, allow_overbooking=False
7171
)
7272
buf = pinned_br.make_buffer(size, stream, reservation)
7373

74-
mv = memoryview(buf)
75-
assert len(mv) == size
76-
assert mv.format == "B"
77-
7874
data = np.arange(size, dtype=np.uint8)
79-
np.frombuffer(mv, dtype=np.uint8)[:] = data
80-
assert np.array_equal(np.frombuffer(memoryview(buf), dtype=np.uint8), data)
75+
with buf.host_view() as mv:
76+
assert len(mv) == size
77+
np.frombuffer(mv, dtype=np.uint8)[:] = data
78+
79+
with buf.host_view() as mv:
80+
assert np.array_equal(np.frombuffer(mv, dtype=np.uint8), data)
8181

8282

83-
def test_buffer_protocol_host() -> None:
83+
def test_host_view_host() -> None:
8484
size = 256
8585
mr = rmm.mr.CudaMemoryResource()
8686
br = BufferResource(mr, memory_limits={MemoryType.HOST: size * 4})
8787
stream = Stream()
8888
reservation, _ = br.reserve(MemoryType.HOST, size, allow_overbooking=False)
8989
buf = br.make_buffer(size, stream, reservation)
9090

91-
mv = memoryview(buf)
92-
assert len(mv) == size
93-
assert mv.format == "B"
91+
data = np.arange(size, dtype=np.uint8)
92+
with buf.host_view() as mv:
93+
assert len(mv) == size
94+
np.frombuffer(mv, dtype=np.uint8)[:] = data
95+
96+
with buf.host_view() as mv:
97+
assert np.array_equal(np.frombuffer(mv, dtype=np.uint8), data)
9498

9599

96-
def test_buffer_protocol_rejects_device() -> None:
100+
def test_host_view_rejects_device() -> None:
97101
size = 1024
98102
mr = rmm.mr.CudaMemoryResource()
99103
br = BufferResource(mr, memory_limits={MemoryType.DEVICE: size * 4})
@@ -102,4 +106,19 @@ def test_buffer_protocol_rejects_device() -> None:
102106
buf = br.make_buffer(size, stream, reservation)
103107

104108
with pytest.raises(TypeError, match="host buffers"):
105-
memoryview(buf)
109+
buf.host_view()
110+
111+
112+
def test_host_view_lock_released_on_error() -> None:
113+
size = 256
114+
mr = rmm.mr.CudaMemoryResource()
115+
br = BufferResource(mr, memory_limits={MemoryType.HOST: size * 4})
116+
stream = Stream()
117+
reservation, _ = br.reserve(MemoryType.HOST, size, allow_overbooking=False)
118+
buf = br.make_buffer(size, stream, reservation)
119+
120+
with pytest.raises(RuntimeError), buf.host_view():
121+
raise RuntimeError("intentional")
122+
123+
with buf.host_view():
124+
pass

0 commit comments

Comments
 (0)