Skip to content

Commit

Permalink
Refactoring of Buffers (last step towards unifying COW and Spilling) (r…
Browse files Browse the repository at this point in the history
…apidsai#13801)

This PR de-couples buffer slices/views from owning buffers. As it is now, all buffer classes (`ExposureTrackedBuffer`, `BufferSlice`, `SpillableBuffer`, `SpillableBufferSlice`) inherent from `Buffer`, however they are not Liskov substitutable as pointed by @wence- and @vyasr ([here](rapidsai#13307 (comment)) and [here](rapidsai#13307 (comment))). 

To fix this, we now have a `Buffer` and a `BufferOwner` class. We still use the `Buffer` throughout cuDF but it now points to an `BufferOwner`.  

We have the following class hierarchy: 
``` 
ExposureTrackedBufferOwner -> BufferOwner 
SpillableBufferOwner -> BufferOwner 
ExposureTrackedBuffer -> Buffer 
SpillableBuffer -> Buffer 
``` 

With the following relationship: 
``` 
Buffer -> BufferOwner 
ExposureTrackedBuffer -> ExposureTrackedBufferOwner 
SpillableBuffer -> SpillableBufferOwner 
``` 

#### Unify COW and Spilling 

In a follow-up PR, the spilling buffer classes will inherent from the exposure tracked buffer classes so we get the following hierarchy: 
``` 
SpillableBufferOwner -> ExposureTrackedBufferOwner -> BufferOwner 
SpillableBuffer -> ExposureTrackedBuffer -> Buffer 
```

Authors:
  - Mads R. B. Kristensen (https://github.com/madsbk)

Approvers:
  - Lawrence Mitchell (https://github.com/wence-)
  - Vyas Ramasubramani (https://github.com/vyasr)

URL: rapidsai#13801
  • Loading branch information
madsbk authored Jan 15, 2024
1 parent 7ca988f commit 0710335
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 598 deletions.
14 changes: 7 additions & 7 deletions docs/cudf/source/developer_guide/library_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,26 +325,26 @@ This section describes the internal implementation details of the copy-on-write
It is recommended that developers familiarize themselves with [the user-facing documentation](copy-on-write-user-doc) of this functionality before reading through the internals
below.

The core copy-on-write implementation relies on the factory function `as_exposure_tracked_buffer` and the two classes `ExposureTrackedBuffer` and `BufferSlice`.
The core copy-on-write implementation relies on `ExposureTrackedBuffer` and the tracking features of `BufferOwner`.

An `ExposureTrackedBuffer` is a subclass of the regular `Buffer` that tracks internal and external references to its underlying memory. Internal references are tracked by maintaining [weak references](https://docs.python.org/3/library/weakref.html) to every `BufferSlice` of the underlying memory. External references are tracked through "exposure" status of the underlying memory. A buffer is considered exposed if the device pointer (integer or void*) has been handed out to a library outside of cudf. In this case, we have no way of knowing if the data are being modified by a third party.
`BufferOwner` tracks internal and external references to its underlying memory. Internal references are tracked by maintaining [weak references](https://docs.python.org/3/library/weakref.html) to every `ExposureTrackedBuffer` of the underlying memory. External references are tracked through "exposure" status of the underlying memory. A buffer is considered exposed if the device pointer (integer or void*) has been handed out to a library outside of cudf. In this case, we have no way of knowing if the data are being modified by a third party.

`BufferSlice` is a subclass of `ExposureTrackedBuffer` that represents a _slice_ of the memory underlying a exposure tracked buffer.
`ExposureTrackedBuffer` is a subclass of `Buffer` that represents a _slice_ of the memory underlying an exposure tracked buffer.

When the cudf option `"copy_on_write"` is `True`, `as_buffer` calls `as_exposure_tracked_buffer`, which always returns a `BufferSlice`. It is then the slices that determine whether or not to make a copy when a write operation is performed on a `Column` (see below). If multiple slices point to the same underlying memory, then a copy must be made whenever a modification is attempted.
When the cudf option `"copy_on_write"` is `True`, `as_buffer` returns a `ExposureTrackedBuffer`. It is this class that determines whether or not to make a copy when a write operation is performed on a `Column` (see below). If multiple slices point to the same underlying memory, then a copy must be made whenever a modification is attempted.


### Eager copies when exposing to third-party libraries

If a `Column`/`BufferSlice` is exposed to a third-party library via `__cuda_array_interface__`, we are no longer able to track whether or not modification of the buffer has occurred. Hence whenever
If a `Column`/`ExposureTrackedBuffer` is exposed to a third-party library via `__cuda_array_interface__`, we are no longer able to track whether or not modification of the buffer has occurred. Hence whenever
someone accesses data through the `__cuda_array_interface__`, we eagerly trigger the copy by calling
`.make_single_owner_inplace` which ensures a true copy of underlying data is made and that the slice is the sole owner. Any future copy requests must also trigger a true physical copy (since we cannot track the lifetime of the third-party object). To handle this we also mark the `Column`/`BufferSlice` as exposed thus indicating that any future shallow-copy requests will trigger a true physical copy rather than a copy-on-write shallow copy.
`.make_single_owner_inplace` which ensures a true copy of underlying data is made and that the slice is the sole owner. Any future copy requests must also trigger a true physical copy (since we cannot track the lifetime of the third-party object). To handle this we also mark the `Column`/`ExposureTrackedBuffer` as exposed thus indicating that any future shallow-copy requests will trigger a true physical copy rather than a copy-on-write shallow copy.

### Obtaining a read-only object

A read-only object can be quite useful for operations that will not
mutate the data. This can be achieved by calling `.get_ptr(mode="read")`, and using `cuda_array_interface_wrapper` to wrap a `__cuda_array_interface__` object around it.
This will not trigger a deep copy even if multiple `BufferSlice` points to the same `ExposureTrackedBuffer`. This API should only be used when the lifetime of the proxy object is restricted to cudf's internal code execution. Handing this out to external libraries or user-facing APIs will lead to untracked references and undefined copy-on-write behavior. We currently use this API for device to host
This will not trigger a deep copy even if multiple `ExposureTrackedBuffer`s point to the same `ExposureTrackedBufferOwner`. This API should only be used when the lifetime of the proxy object is restricted to cudf's internal code execution. Handing this out to external libraries or user-facing APIs will lead to untracked references and undefined copy-on-write behavior. We currently use this API for device to host
copies like in `ColumnBase.data_array_view(mode="read")` which is used for `Column.values_host`.


Expand Down
10 changes: 8 additions & 2 deletions python/cudf/cudf/core/abc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2020-2022, NVIDIA CORPORATION.
# Copyright (c) 2020-2024, NVIDIA CORPORATION.
"""Common abstract base classes for cudf."""

import pickle
Expand Down Expand Up @@ -89,7 +89,13 @@ def device_serialize(self):
"""
header, frames = self.serialize()
assert all(
isinstance(f, (cudf.core.buffer.Buffer, memoryview))
isinstance(
f,
(
cudf.core.buffer.Buffer,
memoryview,
),
)
for f in frames
)
header["type-serialized"] = pickle.dumps(type(self))
Expand Down
8 changes: 6 additions & 2 deletions python/cudf/cudf/core/buffer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Copyright (c) 2022-2023, NVIDIA CORPORATION.
# Copyright (c) 2022-2024, NVIDIA CORPORATION.

from cudf.core.buffer.buffer import Buffer, cuda_array_interface_wrapper
from cudf.core.buffer.buffer import (
Buffer,
BufferOwner,
cuda_array_interface_wrapper,
)
from cudf.core.buffer.exposure_tracked_buffer import ExposureTrackedBuffer
from cudf.core.buffer.spillable_buffer import SpillableBuffer, SpillLock
from cudf.core.buffer.utils import (
Expand Down
Loading

0 comments on commit 0710335

Please sign in to comment.