Skip to content

Add a minimal public C-API to fetch ndarray metadata - #10320

Open
seberg wants to merge 1 commit into
cupy:mainfrom
seberg:public-c-api
Open

seberg wants to merge 1 commit into
cupy:mainfrom
seberg:public-c-api

Conversation

@seberg

@seberg seberg commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

This adds a minimal public C-API to fetch commonly used ndarray metadata mainly into a provided struct. This does not expose any internal ABI/API.
The caller must keep the original alive for this to be correct.

The API is "backported" in the C-header to work with old versions of CuPy, although filling the itemsize will go via a slow Python path.

Summary of API:

  • A get_cupy_api() function which is somewhat more than PyCapsule_Import() because it adds backwards-compatibility to the table (for old cupy versions)
  • ndarray_type the cupy.ndarray type (for C/C++ convenience really) for type-checking.
  • int get_ndarray_ptr(CuPyNDArray *arr, void **ptr)
  • get_ndarray_metadata(CuPyNDArray *arr, CuPyNDArrayMetadata *out). The metadata includes:
    • ptr
    • ndim, size, itemsize
    • shape, strides
    • dtype (Python object)
  • get_current_stream_ptr(int device_id, void **ptr)

The Cython API does check types (implicitly), but of course you can cast to avoid that.

Things to consider:

  • Are there any missing fields, or should we split things up? (nvmath-python needs the itemsize for example).
  • Add a function to get the DLPack dtype or even include it? My main reservation is that fetching it may be relatively slow (especially for backports)?

And of course is this approach of a capsule with functions what we want. It is very safe (so long users are careful as documented). And the capsule has the nice property that it can even be deprecated in the module level __getattr__.

(I think the main point against this would be "we'll just use the new DLPack API always anyway.)


Closes gh-9881, CC @NaderAlAwar the .pxd and header file here are designed so that one can also just vendor them (otherwise you need a build_requires with cupy>=14.3).
(The "backport" is a very safe hack to make this much more nice to adopt.)

From what I could tell nvmath-python basically needs everything (because it needs strides in itemsize). But to use this, you would have to Cythonize (or similar) your TensorHolder/CupyTensor.

This adds a minimal public C-API to fetch commonly used `ndarray`
metadata mainly into a provided struct.
The caller must keep the original alive for this to be correct.

The API is "backported" in the C-header to work with old versions
of CuPy, although filling the `itemsize` will go via a slow Python
path.

Summary of API:
* A get_cupy_api() function which is somewhat more than PyCapsule_Import()
  because it adds backwards-compatibility to the table (for old cupy versions)
* `ndarray_type` the cupy.ndarray type (for C/C++ convenience really)
* `int get_ndarray_ptr(CuPyNDArray *arr, void **ptr)`
* `get_ndarray_metadata(CuPyNDArray *arr, CuPyNDArrayMetadata *out)`. The metadata includes:
  * ptr
  * ndim, size, itemsize
  * shape, strides
  * dtype (Python object)
* `get_current_stream_ptr(int device_id, void **ptr)`

Things to consider:
* Are there any missing fields, or should we split things up?
  (`nvmath-python` needs the `itemsize` for example).
* Add a function to get the DLPack dtype or even include it?
  My main reservation is that fetching it may be relatively
  slow (especially for backports)?

And of course is this approach of a capsule with functions what we
want. It is very safe (so long users are careful as documented).
And the capsule has the nice property that it can even be deprecated
in the module level `__getattr__`.

(I think the main point against this would be "we'll just use the
new DLPack API always anyway.)
@seberg seberg added the cat:feature New features/APIs label Sep 22, 2026
@leofang leofang self-assigned this Sep 23, 2026
@leofang

leofang commented Sep 23, 2026

Copy link
Copy Markdown
Member

cc @shwina for vis

@seberg

seberg commented Sep 23, 2026 •

Copy link
Copy Markdown
Member Author

One thing I would consider adding as well is a flags space with contiguity and maybe 32bit indexing 1 flags set. (future flags seem plausible too; another flag might be the memory kind, although checking it is currently very slightly more expensive maybe.)

EDIT: If we are nervous about ABI safety (considering how small this should be I didn't worry too much), I would suggest to move "table" building to be user responsibility:

cdef get_ndarray_metadata_func get_metadata = get_cupy_api(ENUM_VALUE)
cdef get_ndarray_ptr_func get_ptr = get_cupy_api(ENUM_VALUE)

That would allow targeted deprecation of API/enum values. And a NULL return for "not included in this CuPy version". (Doesn't matter much if get_cupy_api is a Python or C function...)

Footnotes

  1. although, the promise of it very slightly shifted over time. (My current PR guarantees that shapes fit, not just indexing.) ↩

* defined in C++ and not compiling for the stable ABI (limited API is fine).
*
* When we can assume almost all downstream uses CuPy 14.3+ this fallback
* should be removed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this interface exposes C++ standard-library types across the binary boundary, users must compile against a compatible C++ ABI and standard-library configuration. Matching CuPy’s C++ language standard alone does not guarantee compatibility.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arg, thanks, you are right should have thought of that. That means that std::vector .data() call can't be "backported" :(.

Which means, I could still backport other API, but get_ndarray_metadata (or anything that accesses shape/strides) can't be...

To be clearer: The interface doesn't, but for the backport I wanted to abuse this...

from cupy._version import __version__ as _cupy_version


cdef int _get_ndarray_ptr(ndarray arr, void **ptr) except -1:

@oleksandr-pavlyk oleksandr-pavlyk Oct 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since cupy/public_c_api.pxd uses ctypedef class cupy.ndarray [object CuPyNDArray, check_size ignore]:, perhaps we need to fully qualify ndarray type here as well:

Suggested change
cdef int _get_ndarray_ptr(ndarray arr, void **ptr) except -1:
cdef int _get_ndarray_ptr(cupy.ndarray arr, void **ptr) except -1:

The code is correct as written. The semantics of ctypedef class cupy.ndarray [object CuPyNDArray, check_size ignore]: is define Cython type in cupy.public_c_api namespace that corresponds to Python type cupy.ndarray.

Hence, when from cupy.public_c_api cimport * implies ndarray type is define in current namespace now.

out.itemsize = (<cnp.dtype>a.dtype).itemsize
out.shape = a._shape.data()
out.strides = a._strides.data()
out.dtype = <PyObject *>a.dtype

@oleksandr-pavlyk oleksandr-pavlyk Oct 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we use PyObject here, I think we must cimport it at the top of this Cython file:

from cpython.ref cimport PyObject

I realize this is defined via transitive import, but it's best to be explicit.

Comment thread cupy/public_c_api.pxd
cdef CuPyAPI *cp_api = get_cupy_api()

def use_array(arr):
cdef void *ptr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cdef void *ptr
cdef void *ptr = NULL

I would prefer to initialize the pointer in the example source code

Comment thread cupy/public_c_api.pxd
Comment on lines +36 to +38
Cython type-checks the first form for you. `None` is the one exception:
it passes the check and is then cast like any other object, so declare
arguments as `ndarray not None` where you can.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps provide an example

Suggested change
Cython type-checks the first form for you. `None` is the one exception:
it passes the check and is then cast like any other object, so declare
arguments as `ndarray not None` where you can.
Cython type-checks the first form for you. `None` is the one exception:
it passes the check and is then cast like any other object, so declare
arguments as `ndarray not None` where you can. For example,
cdef void * _get_ndarray_ptr(ndarray arr not None):
cdef void *ptr = NULL
cp_api.get_ndarray_ptr(<ndarray>arr, &ptr)
return ptr

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: stable C API for read-only access to ndarray metadata

3 participants