Skip to content

Add dparray #192

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

Merged
merged 15 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion dpctl/__init__.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@

from dpctl._sycl_core cimport *
from dpctl._memory import *

261 changes: 261 additions & 0 deletions dpctl/dparray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
##===---------- dparray.py - dpctl -------*- Python -*----===##
##
## Data Parallel Control (dpCtl)
##
## Copyright 2020 Intel Corporation
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##
##===----------------------------------------------------------------------===##
###
### \file
### This file implements a dparray - USM aware implementation of ndarray.
##===----------------------------------------------------------------------===##

import numpy as np
from inspect import getmembers, isfunction, isclass, isbuiltin
from numbers import Number
from types import FunctionType as ftype, BuiltinFunctionType as bftype
import sys
import inspect
import dpctl
from dpctl.memory import MemoryUSMShared

debug = False


def dprint(*args):
if debug:
print(*args)
sys.stdout.flush()


functions_list = [o[0] for o in getmembers(np) if isfunction(o[1]) or isbuiltin(o[1])]
class_list = [o for o in getmembers(np) if isclass(o[1])]

array_interface_property = "__sycl_usm_array_interface__"


def has_array_interface(x):
return hasattr(x, array_interface_property)


class ndarray(np.ndarray):
"""
numpy.ndarray subclass whose underlying memory buffer is allocated
with a foreign allocator.
"""

def __new__(
subtype, shape, dtype=float, buffer=None, offset=0, strides=None, order=None
):
# Create a new array.
if buffer is None:
dprint("dparray::ndarray __new__ buffer None")
nelems = np.prod(shape)
dt = np.dtype(dtype)
isz = dt.itemsize
nbytes = int(isz * max(1, nelems))
buf = MemoryUSMShared(nbytes)
new_obj = np.ndarray.__new__(
subtype,
shape,
dtype=dt,
buffer=buf,
offset=0,
strides=strides,
order=order,
)
if hasattr(new_obj, array_interface_property):
dprint("buffer None new_obj already has sycl_usm")
else:
dprint("buffer None new_obj will add sycl_usm")
setattr(new_obj, array_interface_property, {})
return new_obj
# zero copy if buffer is a usm backed array-like thing
elif hasattr(buffer, array_interface_property):
dprint("dparray::ndarray __new__ buffer", array_interface_property)
# also check for array interface
new_obj = np.ndarray.__new__(
subtype,
shape,
dtype=dtype,
buffer=buffer,
offset=offset,
strides=strides,
order=order,
)
if hasattr(new_obj, array_interface_property):
dprint("buffer None new_obj already has sycl_usm")
else:
dprint("buffer None new_obj will add sycl_usm")
setattr(new_obj, array_interface_property, {})
return new_obj
else:
dprint("dparray::ndarray __new__ buffer not None and not sycl_usm")
nelems = np.prod(shape)
# must copy
ar = np.ndarray(
shape,
dtype=dtype,
buffer=buffer,
offset=offset,
strides=strides,
order=order,
)
nbytes = int(ar.nbytes)
buf = MemoryUSMShared(nbytes)
new_obj = np.ndarray.__new__(
subtype,
shape,
dtype=dtype,
buffer=buf,
offset=0,
strides=strides,
order=order,
)
np.copyto(new_obj, ar, casting="no")
if hasattr(new_obj, array_interface_property):
dprint("buffer None new_obj already has sycl_usm")
else:
dprint("buffer None new_obj will add sycl_usm")
setattr(new_obj, array_interface_property, {})
return new_obj

def __array_finalize__(self, obj):
dprint("__array_finalize__:", obj, hex(id(obj)), type(obj))
# When called from the explicit constructor, obj is None
if obj is None:
return
# When called in new-from-template, `obj` is another instance of our own
# subclass, that we might use to update the new `self` instance.
# However, when called from view casting, `obj` can be an instance of any
# subclass of ndarray, including our own.
if hasattr(obj, array_interface_property):
return
if isinstance(obj, np.ndarray):
ob = self
while isinstance(ob, np.ndarray):
if hasattr(ob, array_interface_property):
return
ob = ob.base

# Just raise an exception since __array_ufunc__ makes all reasonable cases not
# need the code below.
raise ValueError(
"Non-USM allocated ndarray can not viewed as a USM-allocated one without a copy"
)

# Tell Numba to not treat this type just like a NumPy ndarray but to propagate its type.
# This way it will use the custom dparray allocator.
__numba_no_subtype_ndarray__ = True
Copy link
Contributor

Choose a reason for hiding this comment

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

@DrTodd13 Do we really need this extra attribute? Can we not use the facts that the array is a subclass of NumPy.ndarray and it defines the __sycl_usm_array_interface__ to recognize this class inside Numba?

Copy link
Contributor

Choose a reason for hiding this comment

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

Numba is currently made to treat Numpy.ndarray subclasses as if they were Numpy.ndarrays. Therefore, we can't just check for whether it is a ndarray subclass to keep those types separate without breaking backwards compatibility.

Also, you might want to keep types separate even if you don't need a special allocator. So, for generality, we shouldn't specialize on something allocator or USM-specific inside Numba.


# Convert to a NumPy ndarray.
def as_ndarray(self):
return np.copy(self)

def __array__(self):
return self

def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
if method == "__call__":
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to also handle method="accumulate" , "reduce", "reduceat", "outer"

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a TODO and open a ticket to track this future work.

N = None
scalars = []
typing = []
for inp in inputs:
if isinstance(inp, Number):
scalars.append(inp)
typing.append(inp)
elif isinstance(inp, (self.__class__, np.ndarray)):
if isinstance(inp, self.__class__):
scalars.append(np.ndarray(inp.shape, inp.dtype, inp))
typing.append(np.ndarray(inp.shape, inp.dtype))
else:
scalars.append(inp)
typing.append(inp)
if N is not None:
if N != inp.shape:
raise TypeError("inconsistent sizes")
else:
N = inp.shape
else:
return NotImplemented
# Have to avoid recursive calls to array_ufunc here.
# If no out kwarg then we create a dparray out so that we get
# USM memory. However, if kwarg has dparray-typed out then
# array_ufunc is called recursively so we cast out as regular
# NumPy ndarray (having a USM data pointer).
if kwargs.get("out", None) is None:
# maybe copy?
# deal with multiple returned arrays, so kwargs['out'] can be tuple
res_type = np.result_type(*typing)
out = empty(inputs[0].shape, dtype=res_type)
out_as_np = np.ndarray(out.shape, out.dtype, out)
kwargs["out"] = out_as_np
else:
# If they manually gave dparray as out kwarg then we have to also
# cast as regular NumPy ndarray to avoid recursion.
if isinstance(kwargs["out"], ndarray):
out = kwargs["out"]
kwargs["out"] = np.ndarray(out.shape, out.dtype, out)
else:
out = kwargs["out"]
ret = ufunc(*scalars, **kwargs)
return out
else:
return NotImplemented


def isdef(x):
try:
eval(x)
return True
except NameError:
return False


for c in class_list:
cname = c[0]
if isdef(cname):
continue
# For now we do the simple thing and copy the types from NumPy module into dparray module.
new_func = "%s = np.%s" % (cname, cname)
try:
the_code = compile(new_func, "__init__", "exec")
exec(the_code)
except:
print("Failed to exec type propagation", cname)
pass

# Redefine all Numpy functions in this module and if they
# return a Numpy array, transform that to a USM-backed array
# instead. This is a stop-gap. We should eventually find a
# way to do the allocation correct to start with.
for fname in functions_list:
if isdef(fname):
continue
new_func = "def %s(*args, **kwargs):\n" % fname
new_func += " ret = np.%s(*args, **kwargs)\n" % fname
new_func += " if type(ret) == np.ndarray:\n"
new_func += " ret = ndarray(ret.shape, ret.dtype, ret)\n"
new_func += " return ret\n"
the_code = compile(new_func, "__init__", "exec")
exec(the_code)


def from_ndarray(x):
return copy(x)


def as_ndarray(x):
return np.copy(x)
1 change: 1 addition & 0 deletions dpctl/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
## Top-level module of all dpctl Python unit test cases.
##===----------------------------------------------------------------------===##

from .test_dparray import *
from .test_dump_functions import *
from .test_sycl_device import *
from .test_sycl_kernel_submit import *
Expand Down
79 changes: 79 additions & 0 deletions dpctl/tests/test_dparray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
##===---------- test_dparray.py - dpctl -------*- Python -*----===##
##
## Data Parallel Control (dpCtl)
##
## Copyright 2020 Intel Corporation
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##
##===----------------------------------------------------------------------===##
###
### \file
### A basic unit test for dpctl.dparray.
##===----------------------------------------------------------------------===##

import unittest
from dpctl import dparray
import numpy


class Test_dparray(unittest.TestCase):
def setUp(self):
self.X = dparray.ndarray((256, 4), dtype="d")
self.X.fill(1.0)

def test_dparray_type(self):
self.assertIsInstance(self.X, dparray.ndarray)

def test_dparray_as_ndarray_self(self):
Y = self.X.as_ndarray()
self.assertEqual(type(Y), numpy.ndarray)

def test_dparray_as_ndarray(self):
Y = dparray.as_ndarray(self.X)
self.assertEqual(type(Y), numpy.ndarray)

def test_dparray_from_ndarray(self):
Y = dparray.as_ndarray(self.X)
dp1 = dparray.from_ndarray(Y)
self.assertIsInstance(dp1, dparray.ndarray)

def test_multiplication_dparray(self):
C = self.X * 5
self.assertIsInstance(C, dparray.ndarray)

def test_dparray_mixing_dpctl_and_numpy(self):
dp_numpy = numpy.ones((256, 4), dtype="d")
res = dp_numpy * self.X
self.assertIsInstance(res, dparray.ndarray)

def test_dparray_shape(self):
res = self.X.shape
self.assertEqual(res, (256, 4))

def test_dparray_T(self):
res = self.X.T
self.assertEqual(res.shape, (4, 256))

def test_numpy_ravel_with_dparray(self):
res = numpy.ravel(self.X)
self.assertEqual(res.shape, (1024,))

@unittest.expectedFailure
def test_numpy_sum_with_dparray(self):
res = numpy.sum(self.X)
self.assertEqual(res, 1024.0)


if __name__ == "__main__":
unittest.main()