Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Implement the DLPack Array API protocols for ndarray. #19083

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

@hameerabbasi
Copy link
Contributor

@hameerabbasi hameerabbasi commented May 24, 2021

Fixes #19013

cc @rgommers @mattip @seberg

TODO

  • from_dlpack function.
  • dtype support in __dlpack__
@hameerabbasi hameerabbasi requested review from seberg and mattip May 24, 2021
@hameerabbasi hameerabbasi changed the title Add the __dlpack__ and __dlpack_device__ methods to ndarray. Implement the DLPack Array API protocols for ndarray. May 24, 2021
@hameerabbasi hameerabbasi force-pushed the dlpack branch 3 times, most recently from d111d23 to 248a695 May 24, 2021
@rgommers
Copy link
Member

@rgommers rgommers commented May 24, 2021

Thanks Hameer, looks like a good start.

Of the points @seberg brought up on the issue, adding a version attribute seems the most important - because checking for version will be needed before any backwards-incompatible change (such as adding an extra field) can be done. I asked about it on dmlc/dlpack#34, and the suggestion was to add it as an attribute on the PyCapsule. Could you look into doing that?

@hameerabbasi hameerabbasi force-pushed the dlpack branch 2 times, most recently from ac6c005 to 5aa28d8 May 24, 2021
@hameerabbasi
Copy link
Contributor Author

@hameerabbasi hameerabbasi commented May 24, 2021

Could you look into doing that?

Sure -- I commented there instead of here, so more of the relevant parties can see it.

Copy link
Contributor

@BvB93 BvB93 left a comment

Hi Hameer, could you also add these two new methods to the ndarray stubs in numpy/__init__.pyi?
Their signatures are fairly simple, so that's a plus:

# NOTE: Every single import below is already present in `__init__.pyi`
# and doesn't have to be repeated
from typing import Any, Tuple
from typing_extensions import Literal as L

from numpy import number
from numpy.typing import NDArray

# `builtins.PyCapsule` unfortunately lacks annotations as of the moment;
# use `Any` as a stopgap measure
_PyCapsule = Any

def __dlpack__(self: NDArray[number[Any]], *, stream: None = ...) -> _PyCapsule: ...
def __dlpack_device__(self) -> Tuple[L[1], L[0]]: ...

numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
@hameerabbasi hameerabbasi force-pushed the dlpack branch 2 times, most recently from 77e49cc to 830903a May 24, 2021
Copy link
Member

@seberg seberg left a comment

Had started looking at it, so a few small comments. It looks good, I have to think a bit more about the API, but that has nothing to do with the implementation.

I think if we put the header where you put it, it will be effectively public. I guess that is fine thoug? Otherwise I think placing it into common is good.

numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
numpy/core/src/multiarray/methods.c Outdated Show resolved Hide resolved
# `builtins.PyCapsule` unfortunately lacks annotations as of the moment;
# use `Any` as a stopgap measure
_PyCapsule = Any

Copy link
Contributor

@BvB93 BvB93 May 24, 2021

Interestingly, if we could get builtins.PyCapsule in typeshed annotated as a parameterizable type,
then it would in principle be possible for static type checkers to read, e.g., the underlying shape and dtype.

The only thing that users or libraries would have to do here is declare the necessary annotations.

Perhaps something to consider for the future?

Examples

from typing import TypeVar, Any, Generic, Tuple as Tuple
import numpy as np

# Improvised `PyCapsule` annotation
_T = TypeVar("_T")
class PyCapsule(Generic[_T]): ...

# Construct a more compact `PyCapsule` alias; `Tuple` used herein to introduce 2 parameters 
# (there may be more appropriate types that can fulfill this functionality)
_Shape = TypeVar("_Shape", bound=Any)  # TODO: Wait for PEP 646's TypeVarTuple
_DType = TypeVar("_DType", bound=np.dtype[Any])
DLPackCapsule = PyCapsule[Tuple[_Shape, _DType]]

# A practical example
def from_dlpack(__x: DLPackCapsule[_Shape, _DType]) -> np.ndarray[_Shape, _DType]: ...

Copy link
Contributor Author

@hameerabbasi hameerabbasi May 31, 2021

I'll consider this as out of scope of this PR for now, but will leave the conversation unresolved for visibility.

@hameerabbasi hameerabbasi force-pushed the dlpack branch 2 times, most recently from a5d1adf to becdf4d May 25, 2021
@hameerabbasi
Copy link
Contributor Author

@hameerabbasi hameerabbasi commented May 25, 2021

This name should be some standardized DLPack name? Or is this intentional for now to keep it effectively private right now?

Well, we have to be able to delete it to free the memory, but maybe we need some state as well, to ensure that we don't delete it twice if the consumer already deletes it without deleting the capsule (might be good for non-reference counted Python also?).

I thought I read that there was some capsule renaming involved (to signal that its "invalid" after deletion). I guess setting the data to NULL or so would be just as well. (If this is standardized, do that. If not setting data to NULL to guard against deleting twice? Once from python once from the consumer.)

There's a standardized name, see data-apis/array-api#186. I have modified the PR accordingly.

We need to also check for the DType being native. I assume unaligned is OK?

Unaligned is OK, see #19013 (comment). How does one check for native dtypes?

Maybe we should just explicitly list them? We cannot include longdouble here, since the C99 standard doesn't specify it (I am not even sure IBM double-double is strictly standard conform). And I assume Float in DLPack means IEEE compatible float format of defined size. float16, float32, and float64 are fine, for the others maybe DLPack should add an extended float field or so, if we want it?

I just don't see longdoublel to be much good, considering its already system dependend on the CPU alone...

Can we verify this?

@hameerabbasi hameerabbasi force-pushed the dlpack branch 4 times, most recently from 835c638 to e6d2195 May 25, 2021
@hameerabbasi
Copy link
Contributor Author

@hameerabbasi hameerabbasi commented May 25, 2021

Can we verify this?

Ignore, it does mean IEEE float. Does np.float80 or np.float128 not respect the IEEE convention? In that case, it makes sense to limit both complex and floats. Otherwise, the receiving library can deny the incoming datatype by reading the bits field.

@hameerabbasi hameerabbasi requested a review from seberg May 25, 2021
@eric-wieser
Copy link
Member

@eric-wieser eric-wieser commented May 25, 2021

np.float80 etc are all just aliases for np.longdouble based on what C produces for sizeof(long double)

@seberg
Copy link
Member

@seberg seberg commented May 25, 2021

I guess if longdouble is 64bit, you can be sure its just double. That we could check for if we want. There are a couple of macros in numpy/core/src/common/npy_fpmath.h such as HAVE_LDOUBLE_IEEE_QUAD_BE. I don't know how well they work/are tested, but I guess technically you could check for that to catch the rare cases where we have proper quad precision.

Technically, most current hardware uses something like 80bit (IEEE?) stored as 96bit or 128bit. But DLPack can't describe it (its an old thing that our float128 should relaly be float128_80 or something else than float128...). Something, that will eventually be important if quad precision becomes a real thing.

@seberg
Copy link
Member

@seberg seberg commented May 25, 2021

How does one check for native dtypes?

You can use PyDataType_ISNOTSWAPPED.

DLManagedTensor *managed =
(DLManagedTensor *)PyCapsule_GetPointer(self, NPY_DLPACK_INTERNAL_CAPSULE_NAME);
if (managed == NULL) {
return;
}
managed->deleter(managed);
Copy link
Member

@eric-wieser eric-wieser May 30, 2021

As above, this is unsafe:

  • deleter may be null
  • PyCapsule_GetPointer is unsafe to call if an exception is in flight, so PyErr_Fetch is needed first.

Copy link
Member

@eric-wieser eric-wieser Jun 1, 2021

@hameerabbasi, I don't think this one is resolved.

@hameerabbasi hameerabbasi changed the title Implement the DLPack Array API protocols for ndarray. ENH: Implement the DLPack Array API protocols for ndarray. May 31, 2021
@leofang
Copy link
Contributor

@leofang leofang commented Jun 1, 2021

I am working on the counterpart support for CuPy and I realize we should have this question settled first: For xp.from_dlpack(), do we expect to return a NumPy/CuPy array (i.e. just a plain xp.ndarray), or an array that's compliant with NEP-47 for NumPy (which CuPy will likely follow) under the new Array API namespace (#18585)? I was assuming the latter, but it looks like the whole review here has been centered around the former?

cc: @kmaehashi @emcastillo @asi1024 @rgommers @asmeurer @hameerabbasi @eric-wieser @seberg @mattip @BvB93

int64_t *managed_shape_strides = PyMem_Malloc(sizeof(int64_t) * ndim * 2);
if (managed_shape_strides == NULL) {
Copy link
Member

@eric-wieser eric-wieser Jun 1, 2021

This incorrectly raises an error when ndim = 0. Coalescing the allocations would resolve that bug.

return NULL;
}

const int ndim = managed->dl_tensor.ndim;
Copy link

@dalcinl dalcinl Jun 4, 2021

Add a check for ndim < 0

npy_intp shape[NPY_MAXDIMS];
npy_intp strides[NPY_MAXDIMS];

for (int i = 0; i < ndim; ++i) {
Copy link

@dalcinl dalcinl Jun 4, 2021

Before this loop, add a check for shape == NULL if ndim > 0.

Copy link
Member

@seberg seberg Jun 4, 2021

Thanks for the comments, but at least I expect that the exporter ensures that this is not clearly malformed (since this is C-API and not Python input/API). For half of these, they are actually also checked during the array creation.

Copy link

@dalcinl dalcinl Jun 4, 2021

I just did my best to find potential problems. Pleases resolve my comments if you consider any of them a non-issue.

npy_intp strides[NPY_MAXDIMS];

for (int i = 0; i < ndim; ++i) {
shape[i] = managed->dl_tensor.shape[i];
Copy link

@dalcinl dalcinl Jun 4, 2021

Add a check for shape[i] < 0.

}
}

char *data = (char *)managed->dl_tensor.data +
Copy link

@dalcinl dalcinl Jun 4, 2021

Add a check for managed->dl_tensor.data == NULL if product(shape) > 0.

@rgommers
Copy link
Member

@rgommers rgommers commented Jun 4, 2021

For xp.from_dlpack(), do we expect to return a NumPy/CuPy array (i.e. just a plain xp.ndarray), or an array that's compliant with NEP-47 for NumPy (which CuPy will likely follow) under the new Array API namespace (#18585)? I was assuming the latter, but it looks like the whole review here has been centered around the former?

The question is a little ambiguous (use of xp). The intent is:

  1. numpy.from_dlpack returns a numpy.ndarray
  2. numpy.array_api.from_dlpack returns the new, array API compliant, array instance

DLPack is useful standalone, independent of NEP 47. This PR therefore adds DLPack support to NumPy in its main namespace (so only (1) above).

@leofang
Copy link
Contributor

@leofang leofang commented Jun 4, 2021

DLPack is useful standalone, independent of NEP 47.

Ah OK, thanks @rgommers, it makes sense although I didn't anticipate this. We should add a clause to https://data-apis.org/array-api/latest/design_topics/data_interchange.html to clarify this. The way I read it as in its current form, I would think from_dlpack() returns an array-API-compliant object, not any random object, but obviously we do not want to impose this strict requirement.

@rgommers
Copy link
Member

@rgommers rgommers commented Jun 4, 2021

The way I read it as in its current form, I would think from_dlpack() returns an array-API-compliant object, not any random object,

It should do that! Assuming the namespace you call it in conforms to the array API:) In this way it's no different from numpy.zeros vs. numpy.array_api.zeros.

@leofang
Copy link
Contributor

@leofang leofang commented Jun 4, 2021

Right, I was just trying to make sure we are allowed to add from_dlpack() to non-compliant namespaces like numpy and cupy, and get a non-complaint object back 🙂

Copy link
Member

@seberg seberg left a comment

I personally would like to just create a new dlpack_impl.c and dlpack_impl.h in multiarray. Then export the __dlpack__ and from_dlpack there include it into methods.c and multiarray.c and keep all of DLPack in one place. Especially multiarray.c is just too unnecessarily long, and its easy to go from the method list/struct to the actual definition in the other file.

device_type != kDLCUDAManaged) {
PyErr_SetString(PyExc_RuntimeError,
"Unsupported device in DLTensor.");
Py_XDECREF(capsule);
Copy link
Member

@seberg seberg Jun 7, 2021

Suggested change
Py_XDECREF(capsule);
Py_DECREF(capsule);

Not that it really matters.


if (managed->dl_tensor.dtype.lanes != 1) {
PyErr_SetString(PyExc_RuntimeError,
"Unsupported lanes in DLTensor dtype.");
Copy link
Member

@seberg seberg Jun 7, 2021

Might be nice to expand on "lanes" here (or include a link to where its explained?). On the other hand, I guess the reader should know what lanes are if they use them?

}

PyArray_Descr *descr = PyArray_DescrFromType(typenum);
if (descr == NULL) {
Copy link
Member

@seberg seberg Jun 7, 2021

This can't actually fail. But its obscure enough that I think its fine to keep.

Copy link
Member

@seberg seberg Jun 7, 2021

Just move this down directly before the PyArray_NewFromDescr call (you can even put it into the call, since it can't fail). That avoids reference counting headaches.

return NULL;
}

if (PyCapsule_SetName(capsule, NPY_DLPACK_USED_CAPSULE_NAME) < 0) {
Copy link
Member

@seberg seberg Jun 7, 2021

This feels a bit tricky if it would fail. But I guess its fine. Most importantly, at this time it cannot even fail, since failure is only possible if the capsule was not valid. (If it would fail, we might end up freeing twice here? Once for the new_capsule and once for the capsule itself?)

DLDevice device = array_get_dl_device(self);
if (PyErr_Occurred()) {
return NULL;
}
char *data = array_get_dl_data(self);
if (data == NULL) {
return NULL;
}
Copy link
Member

@seberg seberg Jun 7, 2021

Do we really need these? All we would lose is a bit of information when ndarray is an intermediate exchange step, which seems OK?

Copy link
Member

@seberg seberg Jun 7, 2021

(if we do, it would be nice to add a test, since I think calling from_dlpack twice on the same array will do the trick?)

PyErr_SetString(PyExc_TypeError, "DLPack only supports native "
"byte swapping.");
return NULL;
}
Copy link
Member

@seberg seberg Jun 7, 2021

It might be that we should refuse exporting readonly arrays here. At least if they are not backed by numpy-owned data (truly readonly), to prevent a pytorch importer from being able to accidentally crash the interpreter. I have opened an issue here for discussion.

managed_dtype.code = kDLInt;
} else if (PyDataType_ISUNSIGNED(dtype)) {
managed_dtype.code = kDLUInt;
} else if (PyDataType_ISFLOAT(dtype)) {
Copy link
Member

@seberg seberg Jun 7, 2021

NIT: we put the else on the next line.

PyErr_SetString(PyExc_TypeError, "DLPack only supports IEEE "
"floating point types without padding.");
Copy link
Member

@seberg seberg Jun 7, 2021

NIT:

Suggested change
PyErr_SetString(PyExc_TypeError, "DLPack only supports IEEE "
"floating point types without padding.");
PyErr_SetString(PyExc_TypeError,
"DLPack only supports IEEE floating point types without "
"padding.");

y2 = x[:, 0]
assert_array_equal(y2, np.from_dlpack(y2))

y3 = x[1, :]
Copy link
Member

@seberg seberg Jun 7, 2021

Could parametrize, but its fine. Also ::2 is a nice, since then you got a 2-D non-contiguous one.

@@ -3654,3 +3661,8 @@ class broadcast:
def __next__(self) -> Tuple[Any, ...]: ...
def __iter__(self: _T) -> _T: ...
def reset(self) -> None: ...

class _SupportsDLPack(Protocol[_T_contra]):
def __dlpack__(self, *, stream: Optional[int] = ...) -> _PyCapsule: ...
Copy link
Contributor

@BvB93 BvB93 Jun 15, 2021

Suggested change
def __dlpack__(self, *, stream: Optional[int] = ...) -> _PyCapsule: ...
def __dlpack__(self, *, stream: None | _T_contra = ...) -> _PyCapsule: ...

The | operator is just a more compact shortcut for Union or Optional,
but more importantly: the typevar is missing from the argument type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

7 participants