Skip to content

Commit

Permalink
Change ways to access ptr in Buffer (#12587)
Browse files Browse the repository at this point in the history
This PR:
With the introduction of `copy-on-write`(#11718), and `spilling`, there are different ways each `Buffer` implementation expects the code to behave when a `.ptr` is accessed, i.e., in case of COW, a `ptr` can be accessed for read or write purposes, in case of spilling the ptr can be accessed with a spill lock and the buffer will be spillable after the spill lock goes out of scope during execution. For these reasons we introduced `ptr`, `mutable_ptr` and `data_array_view`, `_data_array_view`, `mask_array_view`, and `_mask_array_view`. With so many ways to access buffer ptr and array views, this has become quite difficult for devs to know when to use what unless you are fully familiar with the implementation details. It will also lead us to a lot of special case handling for `Buffer`, `SpillableBuffer`, and `CopyonWriteBuffer`. For this reason, we have decided to simplify fetching the pointer with a `get_ptr(mode="read"/"write")` API, fetching data & mask array views will also become methods that accept `mode` like `data_array_view(mode="read"/"write")` & `mask_array_view(mode="read"/"write")`. It is the expectation that when the caller passed "read", they don't tamper with the buffer or the memory it is pointing to. In the case of "write", they are good to mutate the memory it is pointing to.

Note that even with `mode="read"/"write"` the caller should still, if appropriate, acquire a spill lock for the duration of the access. If this is not done, and the buffer is a `SpillableBuffer`, it will permanently be marked as unspillable.

- [x] Introduces `get_ptr()` to replace `ptr` property. 
- [x] Replaces `data_array_view` & `mask_array_view` methods with `data_array_view(mode=r/w)` & `mask_array_view(mode=r/w)`

Authors:
  - GALI PREM SAGAR (https://github.com/galipremsagar)

Approvers:
  - https://github.com/brandon-b-miller
  - Lawrence Mitchell (https://github.com/wence-)
  - Ashwin Srinath (https://github.com/shwina)

URL: #12587
  • Loading branch information
galipremsagar authored Jan 26, 2023
1 parent 35e90ff commit f7d434d
Show file tree
Hide file tree
Showing 30 changed files with 284 additions and 144 deletions.
6 changes: 2 additions & 4 deletions docs/cudf/source/developer_guide/library_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,11 @@ Spilling consists of two components:
- A spill manager that tracks all instances of `SpillableBuffer` and spills them on demand.
A global spill manager is used throughout cudf when spilling is enabled, which makes `as_buffer()` return `SpillableBuffer` instead of the default `Buffer` instances.

Accessing `Buffer.ptr`, we get the device memory pointer of the buffer. This is unproblematic in the case of `Buffer` but what happens when accessing `SpillableBuffer.ptr`, which might have spilled its device memory. In this case, `SpillableBuffer` needs to unspill the memory before returning its device memory pointer. Furthermore, while this device memory pointer is being used (or could be used), `SpillableBuffer` cannot spill its memory back to host memory because doing so would invalidate the device pointer.
Accessing `Buffer.get_ptr(...)`, we get the device memory pointer of the buffer. This is unproblematic in the case of `Buffer` but what happens when accessing `SpillableBuffer.get_ptr(...)`, which might have spilled its device memory. In this case, `SpillableBuffer` needs to unspill the memory before returning its device memory pointer. Furthermore, while this device memory pointer is being used (or could be used), `SpillableBuffer` cannot spill its memory back to host memory because doing so would invalidate the device pointer.

To address this, we mark the `SpillableBuffer` as unspillable, we say that the buffer has been _exposed_. This can either be permanent if the device pointer is exposed to external projects or temporary while `libcudf` accesses the device memory.

The `SpillableBuffer.get_ptr()` returns the device pointer of the buffer memory just like `.ptr` but if given an instance of `SpillLock`, the buffer is only unspillable as long as the instance of `SpillLock` is alive.

For convenience, one can use the decorator/context `acquire_spill_lock` to associate a `SpillLock` with a lifetime bound to the context automatically.
The `SpillableBuffer.get_ptr(...)` returns the device pointer of the buffer memory but if called within an `acquire_spill_lock` decorator/context, the buffer is only marked unspillable while running within the decorator/context.

#### Statistics
cuDF supports spilling statistics, which can be very useful for performance profiling and to identify code that renders buffers unspillable.
Expand Down
2 changes: 0 additions & 2 deletions python/cudf/cudf/_lib/column.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ class Column:
@property
def base_mask(self) -> Optional[Buffer]: ...
@property
def base_mask_ptr(self) -> int: ...
@property
def mask(self) -> Optional[Buffer]: ...
@property
def mask_ptr(self) -> int: ...
Expand Down
34 changes: 15 additions & 19 deletions python/cudf/cudf/_lib/column.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ cdef class Column:
if self.data is None:
return 0
else:
return self.data.ptr
return self.data.get_ptr(mode="write")

def set_base_data(self, value):
if value is not None and not isinstance(value, Buffer):
Expand All @@ -124,13 +124,6 @@ cdef class Column:
def base_mask(self):
return self._base_mask

@property
def base_mask_ptr(self):
if self.base_mask is None:
return 0
else:
return self.base_mask.ptr

@property
def mask(self):
if self._mask is None:
Expand All @@ -145,7 +138,7 @@ cdef class Column:
if self.mask is None:
return 0
else:
return self.mask.ptr
return self.mask.get_ptr(mode="write")

def set_base_mask(self, value):
"""
Expand Down Expand Up @@ -206,7 +199,7 @@ cdef class Column:
elif hasattr(value, "__cuda_array_interface__"):
if value.__cuda_array_interface__["typestr"] not in ("|i1", "|u1"):
if isinstance(value, Column):
value = value.data_array_view
value = value.data_array_view(mode="write")
value = cp.asarray(value).view('|u1')
mask = as_buffer(value)
if mask.size < required_num_bytes:
Expand Down Expand Up @@ -329,10 +322,10 @@ cdef class Column:

if col.base_data is None:
data = NULL
elif isinstance(col.base_data, SpillableBuffer):
data = <void*><uintptr_t>(col.base_data).get_ptr()
else:
data = <void*><uintptr_t>(col.base_data.ptr)
data = <void*><uintptr_t>(col.base_data.get_ptr(
mode="write")
)

cdef Column child_column
if col.base_children:
Expand All @@ -341,7 +334,9 @@ cdef class Column:

cdef libcudf_types.bitmask_type* mask
if self.nullable:
mask = <libcudf_types.bitmask_type*><uintptr_t>(self.base_mask_ptr)
mask = <libcudf_types.bitmask_type*><uintptr_t>(
self.base_mask.get_ptr(mode="write")
)
else:
mask = NULL

Expand Down Expand Up @@ -387,10 +382,8 @@ cdef class Column:

if col.base_data is None:
data = NULL
elif isinstance(col.base_data, SpillableBuffer):
data = <void*><uintptr_t>(col.base_data).get_ptr()
else:
data = <void*><uintptr_t>(col.base_data.ptr)
data = <void*><uintptr_t>(col.base_data.get_ptr(mode="read"))

cdef Column child_column
if col.base_children:
Expand All @@ -399,7 +392,9 @@ cdef class Column:

cdef libcudf_types.bitmask_type* mask
if self.nullable:
mask = <libcudf_types.bitmask_type*><uintptr_t>(self.base_mask_ptr)
mask = <libcudf_types.bitmask_type*><uintptr_t>(
self.base_mask.get_ptr(mode="read")
)
else:
mask = NULL

Expand Down Expand Up @@ -549,7 +544,8 @@ cdef class Column:
f"{data_owner} is spilled, which invalidates "
f"the exposed data_ptr ({hex(data_ptr)})"
)
data_owner.ptr # accessing the pointer marks it exposed.
# accessing the pointer marks it exposed permanently.
data_owner.mark_exposed()
else:
data = as_buffer(
rmm.DeviceBuffer(ptr=data_ptr, size=0)
Expand Down
4 changes: 2 additions & 2 deletions python/cudf/cudf/_lib/copying.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2020-2022, NVIDIA CORPORATION.
# Copyright (c) 2020-2023, NVIDIA CORPORATION.

import pickle

Expand Down Expand Up @@ -765,7 +765,7 @@ cdef class _CPackedColumns:
gpu_data = Buffer.deserialize(header["data"], frames)

dbuf = DeviceBuffer(
ptr=gpu_data.ptr,
ptr=gpu_data.get_ptr(mode="write"),
size=gpu_data.nbytes
)

Expand Down
4 changes: 3 additions & 1 deletion python/cudf/cudf/_lib/transform.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def mask_to_bools(object mask_buffer, size_type begin_bit, size_type end_bit):
if not isinstance(mask_buffer, cudf.core.buffer.Buffer):
raise TypeError("mask_buffer is not an instance of "
"cudf.core.buffer.Buffer")
cdef bitmask_type* bit_mask = <bitmask_type*><uintptr_t>(mask_buffer.ptr)
cdef bitmask_type* bit_mask = <bitmask_type*><uintptr_t>(
mask_buffer.get_ptr(mode="read")
)

cdef unique_ptr[column] result
with nogil:
Expand Down
69 changes: 61 additions & 8 deletions python/cudf/cudf/core/buffer/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ def _getitem(self, offset: int, size: int) -> Buffer:
"""
return self._from_device_memory(
cuda_array_interface_wrapper(
ptr=self.ptr + offset, size=size, owner=self.owner
ptr=self.get_ptr(mode="read") + offset,
size=size,
owner=self.owner,
)
)

Expand All @@ -202,11 +204,6 @@ def nbytes(self) -> int:
"""Size of the buffer in bytes."""
return self._size

@property
def ptr(self) -> int:
"""Device pointer to the start of the buffer."""
return self._ptr

@property
def owner(self) -> Any:
"""Object owning the memory of the buffer."""
Expand All @@ -215,18 +212,74 @@ def owner(self) -> Any:
@property
def __cuda_array_interface__(self) -> Mapping:
"""Implementation of the CUDA Array Interface."""
return self._get_cuda_array_interface(readonly=False)

def _get_cuda_array_interface(self, readonly=False):
"""Helper function to create a CUDA Array Interface.
Parameters
----------
readonly : bool, default False
If True, returns a CUDA Array Interface with
readonly flag set to True.
If False, returns a CUDA Array Interface with
readonly flag set to False.
Returns
-------
dict
"""
return {
"data": (self.ptr, False),
"data": (
self.get_ptr(mode="read" if readonly else "write"),
readonly,
),
"shape": (self.size,),
"strides": None,
"typestr": "|u1",
"version": 0,
}

@property
def _readonly_proxy_cai_obj(self):
"""
Returns a proxy object with a read-only CUDA Array Interface.
"""
return cuda_array_interface_wrapper(
ptr=self.get_ptr(mode="read"),
size=self.size,
owner=self,
readonly=True,
typestr="|u1",
version=0,
)

def get_ptr(self, *, mode) -> int:
"""Device pointer to the start of the buffer.
Parameters
----------
mode : str
Supported values are {"read", "write"}
If "write", the data pointed to may be modified
by the caller. If "read", the data pointed to
must not be modified by the caller.
Failure to fulfill this contract will cause
incorrect behavior.
See Also
--------
SpillableBuffer.get_ptr
"""
return self._ptr

def memoryview(self) -> memoryview:
"""Read-only access to the buffer through host memory."""
host_buf = host_memory_allocation(self.size)
rmm._lib.device_buffer.copy_ptr_to_host(self.ptr, host_buf)
rmm._lib.device_buffer.copy_ptr_to_host(
self.get_ptr(mode="read"), host_buf
)
return memoryview(host_buf).toreadonly()

def serialize(self) -> Tuple[dict, list]:
Expand Down
32 changes: 10 additions & 22 deletions python/cudf/cudf/core/buffer/spillable_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def __len__(self):

def __getitem__(self, i):
if i == 0:
return self._buf.ptr
return self._buf.get_ptr(mode="write")
elif i == 1:
return False
raise IndexError("tuple index out of range")
Expand Down Expand Up @@ -359,7 +359,7 @@ def spill_lock(self, spill_lock: SpillLock) -> None:
self.spill(target="gpu")
self._spill_locks.add(spill_lock)

def get_ptr(self) -> int:
def get_ptr(self, *, mode) -> int:
"""Get a device pointer to the memory of the buffer.
If this is called within an `acquire_spill_lock` context,
Expand All @@ -369,8 +369,8 @@ def get_ptr(self) -> int:
If this is *not* called within a `acquire_spill_lock` context,
this buffer is marked as unspillable permanently.
Return
------
Returns
-------
int
The device pointer as an integer
"""
Expand Down Expand Up @@ -409,18 +409,6 @@ def memory_info(self) -> Tuple[int, int, str]:
).__array_interface__["data"][0]
return (ptr, self.nbytes, self._ptr_desc["type"])

@property
def ptr(self) -> int:
"""Access the memory directly
Notice, this will mark the buffer as "exposed" and make
it unspillable permanently.
Consider using `.get_ptr()` instead.
"""
self.mark_exposed()
return self._ptr

@property
def owner(self) -> Any:
return self._owner
Expand Down Expand Up @@ -559,12 +547,12 @@ def __init__(self, base: SpillableBuffer, offset: int, size: int) -> None:
self._owner = base
self.lock = base.lock

@property
def ptr(self) -> int:
return self._base.ptr + self._offset

def get_ptr(self) -> int:
return self._base.get_ptr() + self._offset
def get_ptr(self, *, mode) -> int:
"""
A passthrough method to `SpillableBuffer.get_ptr`
with factoring in the `offset`.
"""
return self._base.get_ptr(mode=mode) + self._offset

def _getitem(self, offset: int, size: int) -> Buffer:
return SpillableBufferSlice(
Expand Down
7 changes: 4 additions & 3 deletions python/cudf/cudf/core/column/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,9 +956,10 @@ def clip(self, lo: ScalarLike, hi: ScalarLike) -> "column.ColumnBase":
self.astype(self.categories.dtype).clip(lo, hi).astype(self.dtype)
)

@property
def data_array_view(self) -> cuda.devicearray.DeviceNDArray:
return self.codes.data_array_view
def data_array_view(
self, *, mode="write"
) -> cuda.devicearray.DeviceNDArray:
return self.codes.data_array_view(mode=mode)

def unique(self, preserve_order=False) -> CategoricalColumn:
codes = self.as_numerical.unique(preserve_order=preserve_order)
Expand Down
Loading

0 comments on commit f7d434d

Please sign in to comment.