From 6207b9f8b8a3afe07b7d7491c427103f212f754e Mon Sep 17 00:00:00 2001 From: Matthew Murray Date: Tue, 4 Aug 2026 20:52:16 +0000 Subject: [PATCH 1/3] Add basic slicing support to gpumemoryview --- python/pylibcudf/pylibcudf/gpumemoryview.pyi | 3 ++- python/pylibcudf/pylibcudf/gpumemoryview.pyx | 27 ++++++++++++++++++-- python/pylibcudf/tests/test_gpumemoryview.py | 25 +++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyi b/python/pylibcudf/pylibcudf/gpumemoryview.pyi index 10d8d3aec052..5c2abf1664e3 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyi +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyi @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping @@ -9,6 +9,7 @@ class gpumemoryview: @property def __cuda_array_interface__(self) -> Mapping[str, Any]: ... def __len__(self) -> int: ... + def __getitem__(self, index: slice) -> gpumemoryview: ... @property def ptr(self) -> int: ... @property diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyx b/python/pylibcudf/pylibcudf/gpumemoryview.pyx index 03af09faca47..0aff2d2944e1 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyx +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyx @@ -1,13 +1,23 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from libc.stddef cimport size_t +from libc.stdint cimport uintptr_t, uint64_t import functools import operator from .types cimport DataType, size_of, type_id +cdef gpumemoryview _slice(gpumemoryview parent, uintptr_t ptr, uint64_t nbytes): + cdef gpumemoryview v = gpumemoryview.__new__(gpumemoryview) + v.ptr = ptr + v.nbytes = nbytes + v.obj = parent + v.cai = {"data": (ptr, False), "shape": (nbytes,), "typestr": "|u1", "version": 3} + return v + + __all__ = ["gpumemoryview"] @@ -83,6 +93,19 @@ cdef class gpumemoryview: return self.nbytes def __len__(self): - return self.obj.__cuda_array_interface__["shape"][0] + return self.cai["shape"][0] + + def __getitem__(self, index): + if not isinstance(index, slice): + raise TypeError( + f"gpumemoryview indices must be slices, not {type(index).__name__}" + ) + start, stop, step = index.indices(self.nbytes) + if step != 1: + raise ValueError("gpumemoryview only supports step=1 slices") + length = stop - start + if length <= 0: + return _slice(self, self.ptr + start, 0) + return _slice(self, self.ptr + start, length) __hash__ = None diff --git a/python/pylibcudf/tests/test_gpumemoryview.py b/python/pylibcudf/tests/test_gpumemoryview.py index 98a04422563b..62b10a37223d 100644 --- a/python/pylibcudf/tests/test_gpumemoryview.py +++ b/python/pylibcudf/tests/test_gpumemoryview.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import itertools @@ -63,3 +63,26 @@ def test_len(np_array, stream): assert len(gpumemview) == len(np_array_view) assert gpumemview.nbytes == np_array.nbytes + + +@pytest.mark.parametrize( + "s", + [ + slice(1, 3), + slice(None, 2), + slice(3, None), + slice(2, 2), + ], +) +def test_slice(np_array, s): + gv = plc.Column.from_array(np_array.view("u1")).data() + result = plc.Column.from_array(gv[s]).to_pylist() + assert result == np_array.view("u1")[s].tolist() + + +def test_slice_fails(np_array): + gv = plc.Column.from_array(np_array.view("u1")).data() + with pytest.raises(TypeError, match="indices must be slices"): + gv[0] + with pytest.raises(ValueError, match="step=1"): + gv[::2] From e29dffbf382eba27b6041e35e8f6364feede3311 Mon Sep 17 00:00:00 2001 From: Matthew Murray Date: Tue, 4 Aug 2026 22:19:19 +0000 Subject: [PATCH 2/3] use explicit byte_slice API --- python/pylibcudf/pylibcudf/gpumemoryview.pyi | 2 +- python/pylibcudf/pylibcudf/gpumemoryview.pyx | 29 ++++++++++++++++---- python/pylibcudf/tests/test_gpumemoryview.py | 19 ++++++++++--- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyi b/python/pylibcudf/pylibcudf/gpumemoryview.pyi index 5c2abf1664e3..81c9a4779839 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyi +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyi @@ -9,7 +9,7 @@ class gpumemoryview: @property def __cuda_array_interface__(self) -> Mapping[str, Any]: ... def __len__(self) -> int: ... - def __getitem__(self, index: slice) -> gpumemoryview: ... + def byte_slice(self, s: slice) -> gpumemoryview: ... @property def ptr(self) -> int: ... @property diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyx b/python/pylibcudf/pylibcudf/gpumemoryview.pyx index 0aff2d2944e1..30bc5d03e60b 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyx +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyx @@ -14,6 +14,7 @@ cdef gpumemoryview _slice(gpumemoryview parent, uintptr_t ptr, uint64_t nbytes): v.ptr = ptr v.nbytes = nbytes v.obj = parent + # always returns a raw byte view regardless of the source dtype. v.cai = {"data": (ptr, False), "shape": (nbytes,), "typestr": "|u1", "version": 3} return v @@ -95,14 +96,32 @@ cdef class gpumemoryview: def __len__(self): return self.cai["shape"][0] - def __getitem__(self, index): - if not isinstance(index, slice): + def byte_slice(self, s): + """Return a byte-range sub-view of this buffer. + + Parameters + ---------- + s : slice + Byte-based slice. + + Returns + ------- + gpumemoryview + A ``|u1`` view of the requested byte range. The returned view + holds a reference to the parent buffer, keeping it alive. + + Raises + ------ + TypeError + If ``s`` is not a slice. + """ + if not isinstance(s, slice): raise TypeError( - f"gpumemoryview indices must be slices, not {type(index).__name__}" + f"byte_slice requires a slice, not {type(s).__name__}" ) - start, stop, step = index.indices(self.nbytes) + start, stop, step = s.indices(self.nbytes) if step != 1: - raise ValueError("gpumemoryview only supports step=1 slices") + raise ValueError("byte_slice only supports step=1 slices") length = stop - start if length <= 0: return _slice(self, self.ptr + start, 0) diff --git a/python/pylibcudf/tests/test_gpumemoryview.py b/python/pylibcudf/tests/test_gpumemoryview.py index 62b10a37223d..22c2e33bc088 100644 --- a/python/pylibcudf/tests/test_gpumemoryview.py +++ b/python/pylibcudf/tests/test_gpumemoryview.py @@ -76,13 +76,24 @@ def test_len(np_array, stream): ) def test_slice(np_array, s): gv = plc.Column.from_array(np_array.view("u1")).data() - result = plc.Column.from_array(gv[s]).to_pylist() + result = plc.Column.from_array(gv.byte_slice(s)).to_pylist() assert result == np_array.view("u1")[s].tolist() def test_slice_fails(np_array): gv = plc.Column.from_array(np_array.view("u1")).data() - with pytest.raises(TypeError, match="indices must be slices"): - gv[0] + with pytest.raises(TypeError, match="requires a slice"): + gv.byte_slice(0) with pytest.raises(ValueError, match="step=1"): - gv[::2] + gv.byte_slice(slice(None, None, 2)) + + +def test_slice_keeps_parent_alive(): + import gc + + col = plc.Column.from_array(np.arange(10, dtype="u1")) + gv = col.data() + s = gv.byte_slice(slice(2, 5)) + del col, gv + gc.collect() + assert plc.Column.from_array(s).to_pylist() == [2, 3, 4] From df4c1d13d44ac10adac7bf1f308981644cbf5cd3 Mon Sep 17 00:00:00 2001 From: Matthew Murray Date: Wed, 5 Aug 2026 20:45:10 +0000 Subject: [PATCH 3/3] address reviews --- python/pylibcudf/pylibcudf/column.pxd | 1 + python/pylibcudf/pylibcudf/gpumemoryview.pxd | 3 ++- python/pylibcudf/pylibcudf/gpumemoryview.pyx | 7 ++++++- python/pylibcudf/tests/test_gpumemoryview.py | 12 ++++++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/python/pylibcudf/pylibcudf/column.pxd b/python/pylibcudf/pylibcudf/column.pxd index 8cb3b7d4093f..4fdc9ad08f49 100644 --- a/python/pylibcudf/pylibcudf/column.pxd +++ b/python/pylibcudf/pylibcudf/column.pxd @@ -53,6 +53,7 @@ cdef class Column: # _children: List[Column] list _children size_type _num_children + object __weakref__ cdef column_view view(self) cdef mutable_column_view mutable_view(self) diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pxd b/python/pylibcudf/pylibcudf/gpumemoryview.pxd index 87f52bb8bc58..08be0a8b4771 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pxd +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from libc.stdint cimport uint64_t, uintptr_t @@ -9,3 +9,4 @@ cdef class gpumemoryview: cdef readonly object obj cdef readonly dict cai cdef readonly uint64_t nbytes + cdef object __weakref__ diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyx b/python/pylibcudf/pylibcudf/gpumemoryview.pyx index 30bc5d03e60b..0312198e2a84 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyx +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyx @@ -15,7 +15,8 @@ cdef gpumemoryview _slice(gpumemoryview parent, uintptr_t ptr, uint64_t nbytes): v.nbytes = nbytes v.obj = parent # always returns a raw byte view regardless of the source dtype. - v.cai = {"data": (ptr, False), "shape": (nbytes,), "typestr": "|u1", "version": 3} + # TODO: Need to propagate stream from parent.cai if present + v.cai = {"data": (ptr, parent.cai["data"][1]), "shape": (nbytes,), "typestr": "|u1", "version": 3} return v @@ -70,6 +71,7 @@ cdef class gpumemoryview: self.obj = obj self.cai = cai # TODO: Need to respect readonly + # TODO: Need to synchronize on stream if present in cai self.ptr = cai["data"][0] # Compute the buffer size. @@ -114,6 +116,9 @@ cdef class gpumemoryview: ------ TypeError If ``s`` is not a slice. + ValueError + If the slice step is not 1. Out-of-range or reversed ranges + return a zero-length view rather than raising. """ if not isinstance(s, slice): raise TypeError( diff --git a/python/pylibcudf/tests/test_gpumemoryview.py b/python/pylibcudf/tests/test_gpumemoryview.py index 22c2e33bc088..9bd0f6c840ab 100644 --- a/python/pylibcudf/tests/test_gpumemoryview.py +++ b/python/pylibcudf/tests/test_gpumemoryview.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import itertools +import weakref import numpy as np import pytest @@ -72,6 +73,7 @@ def test_len(np_array, stream): slice(None, 2), slice(3, None), slice(2, 2), + slice(0, 10000), ], ) def test_slice(np_array, s): @@ -89,11 +91,13 @@ def test_slice_fails(np_array): def test_slice_keeps_parent_alive(): - import gc - col = plc.Column.from_array(np.arange(10, dtype="u1")) gv = col.data() + col_ref = weakref.ref(col) + gv_ref = weakref.ref(gv) s = gv.byte_slice(slice(2, 5)) del col, gv - gc.collect() - assert plc.Column.from_array(s).to_pylist() == [2, 3, 4] + assert col_ref() is None + assert gv_ref() is not None + del s + assert gv_ref() is None