From bb27d8a5aac27abd44acfb80081871e7a16bd50d Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Dec 2019 23:35:49 -0600 Subject: [PATCH 01/59] Add subscripting boilerplate --- lappy/core/array.py | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/lappy/core/array.py b/lappy/core/array.py index 8a20567..1ec856a 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -57,6 +57,10 @@ def default_env(): class PreconditionNotMetError(Exception): pass + +class IndexError(Exception): + pass + # {{{ base lazy obj class @@ -396,6 +400,72 @@ class Array(LazyObjectBase): from lappy.core.basic_ops import transpose return transpose(self, axes, name) + def __getitem__(self, indices): + """Returns a new array by sub-indexing the current array. + + :param idx: a tuple of indices or slices. + """ + if not isinstance(indices, tuple): + # array of bools + assert isinstance(indices, Array) + assert indices.ndim == self.ndim + raise NotImplementedError() + + for idx in indices: + if idx is None: + # newaxis + raise NotImplementedError() + elif isinstance(idx, Ellipsis): + # wildcard ellipsis + raise NotImplementedError() + elif isinstance(idx, int): + # numerical indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, Unsigned): + # symbolic indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, slice): + # slicing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, tuple): + # fancy indexing with static shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + for fc_id in idx: + if isinstance(fc_id, int): + # static + raise NotImplementedError() + else: + assert isinstance(fc_id, Unsigned) + # (semi-)dynamic + raise NotImplementedError() + elif isinstance(idx, Array): + # fancy indexing with dynamic shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + else: + raise IndexError("only integers, slices (`:`), ellipses (`...`), " + "numpy.newaxis (`None`) and integer or boolean Arrays are " + "valid indices") + + if len(self.name) > 1 and self.name[:2] == '__': + name_prefix = '' + else: + name_prefix = '__' + name = (name_prefix + self.name + + '_subarray%d' % self.__class__._counter) # noqa: W0212 + sub_arr = self.with_name(name) + + # FIXME + return sub_arr + def __iter__(self): """Iterator support if ndim and shape are all fixed. Its behavior mimics numpy.ndarray.__iter__() @@ -1343,6 +1413,9 @@ class Int(Array): assert self.shape == () assert self.ndim == 0 + def __getitem__(self, indices): + raise TypeError("cannot sub-index an integer") + class Unsigned(Int): """Non-negative integer (size type). -- GitLab From d38a8ec64c92859881f53a35bddcf892417e794e Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Dec 2019 23:46:41 -0600 Subject: [PATCH 02/59] Fix shape name generation --- lappy/core/array.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 1ec856a..4f8685f 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -557,7 +557,12 @@ class Array(LazyObjectBase): # constant positive dimensions assert self.ndim > 0 - names = ['__' + self.name + '_shape_%d' % d for d in range(self.ndim)] + if len(self.name) > 1 and self.name[:2] == '__': + name_prefix = '' + else: + name_prefix = '__' + names = [name_prefix + self.name + '_shape_%d' % d + for d in range(self.ndim)] return tuple( Unsigned( name=names[d], -- GitLab From e92e126276e4723b93b5c61ce9baf08c26d40e4e Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Dec 2019 23:10:42 -0600 Subject: [PATCH 03/59] Restructure slicing --- lappy/core/array.py | 293 ++++++++++++++++++++++++++++++++------------ 1 file changed, 212 insertions(+), 81 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 2501c7a..9a60089 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1,5 +1,27 @@ from __future__ import division, absolute_import, print_function +import warnings +import logging +from traceback import format_list, extract_stack +from itertools import chain + +import pyopencl as cl + +import numpy as np +import loopy as lp + +from operator import mul +import functools + +import islpy as isl +import pymbolic +from pymbolic import var, evaluate, substitute +from pymbolic.primitives import ( + Expression, Substitution, Lookup, Subscript, Variable, Product) +from pymbolic.mapper.evaluator import UnknownVariableError + +from pprint import pformat + copyright__ = "Copyright (C) 2017 Sophia Lin and Andreas Kloeckner" __license__ = """ @@ -21,27 +43,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import warnings -import logging -from traceback import format_list, extract_stack -from itertools import chain - -import pyopencl as cl - -import numpy as np -import loopy as lp - -from operator import mul -import functools - -import islpy as isl -import pymbolic -from pymbolic import var, evaluate -from pymbolic.primitives import ( - Expression, Substitution, Lookup, Subscript, Variable, Product) -from pymbolic.mapper.evaluator import UnknownVariableError - -from pprint import pformat def default_env(): @@ -172,7 +173,8 @@ class LazyObjectBase(object): elif isinstance(val, tuple): repr_dict[key] = '(' + ', '.join(str(v) for v in val) + ')' elif isinstance(val, dict): - repr_dict[key] = '{' + ', '.join(str(v) for v in val.keys()) + '}' + repr_dict[key] = '{' + ', '.join( + str(v) for v in val.keys()) + '}' else: repr_dict[key] = str(val) @@ -192,8 +194,8 @@ class LazyObjectBase(object): def __eq__(self, other): """Two objects are considered equal if they share the same name. - Also allows comparing with scalar constants, which amounts to comparing the - value with other. + Also allows comparing with scalar constants, which amounts to + comparing the value with other. """ if np.isscalar(other): return self.value == other @@ -350,10 +352,11 @@ class Array(LazyObjectBase): @property def shape(self): - """Returns the shape as a tuple of ints if the value is known, or strings of - names otherwise. + """Returns the shape as a tuple of ints if the value is known, or + strings of names otherwise. """ - return tuple(s.value if s.value is not None else s.name for s in self._shape) + return tuple(s.value if s.value is not None else s.name + for s in self._shape) @property def size(self): @@ -374,7 +377,7 @@ class Array(LazyObjectBase): for ax in axes) size_expr = Product(shape_vars) shape_ctx = {s.name: s.value for s in self._shape - if isinstance(s, Array) and s.value is not None} + if isinstance(s, Array) and s.value is not None} try: return evaluate(size_expr, shape_ctx) except UnknownVariableError: @@ -409,61 +412,13 @@ class Array(LazyObjectBase): # array of bools assert isinstance(indices, Array) assert indices.ndim == self.ndim - raise NotImplementedError() - - for idx in indices: - if idx is None: - # newaxis - raise NotImplementedError() - elif isinstance(idx, Ellipsis): - # wildcard ellipsis - raise NotImplementedError() - elif isinstance(idx, int): - # numerical indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, Unsigned): - # symbolic indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, slice): - # slicing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, tuple): - # fancy indexing with static shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - for fc_id in idx: - if isinstance(fc_id, int): - # static - raise NotImplementedError() - else: - assert isinstance(fc_id, Unsigned) - # (semi-)dynamic - raise NotImplementedError() - elif isinstance(idx, Array): - # fancy indexing with dynamic shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - else: - raise IndexError("only integers, slices (`:`), ellipses (`...`), " - "numpy.newaxis (`None`) and integer or boolean Arrays are " - "valid indices") - - if len(self.name) > 1 and self.name[:2] == '__': - name_prefix = '' + arr_fac = FilteredArrayFactory(self, indices) else: - name_prefix = '__' - name = (name_prefix + self.name - + '_subarray%d' % self.__class__._counter) # noqa: W0212 - sub_arr = self.with_name(name) + assert isinstance(indices, tuple) + arr_fac = SubArrayFactory(self, indices) + + sub_arr = arr_fac() - # FIXME return sub_arr def __iter__(self): @@ -1395,6 +1350,167 @@ class Array(LazyObjectBase): # }}} End array class +# {{{ filtered array factory + + +class FilteredArrayFactory(object): + """Factory class of the sub-arrays produced by indexing with a boolean + array with equal shape. + + Filtered arrays cannot be created as stand-alone expressions; they + are used solely for the lhs of an assignment. For example, to execute + + A[A > 0] = B[A > 0] + + It is achieved by overloading __setitem__() of the LHS. + + If the user asks for the value of A[A > 0], the kernel will compute + the full array A and the boolean array (A > 0). And then lappy will + simply use numpy to give the results. + """ + + def __init__(self, base, mask): + assert isinstance(base, Array) + assert isinstance(mask, Array) + assert base.ndim == mask.ndim + self.base_arr = base + self.mask_arr = mask + # TODO: precondition on equal shapes + + def __call__(self): + # TODO: make a special kind of array, whose evaluation returns multiple + # results, which form the final reults after some post-processing. + # + # e.g., A[A > 0] + raise NotImplementedError() + +# }}} End filtered array factory + +# {{{ sub-array factory + + +class SubArrayFactory(object): + """Factory class of the sub-arrays produced by indexing with a tuple of + (generalized) indices. + + Indexing any axis with a boolean array will make the sub array a filtered + array, which has subtle effects on the behavior. For details, see + :class:`FilteredArrayFactory`. + """ + + def __init__(self, base, indices): + assert isinstance(base, Array) + self.base_arr = base + + assert isinstance(indices, tuple) + assert len(indices) <= base.ndim + 1 # at most: all axes + an ellipsis + + # step 0: expand ellipsis + # normalize the indices to be of length ndim + n_ell = 0 + for idx in indices: + if idx is Ellipsis: + n_ell += 1 + if n_ell > 1: + raise IndexError( + "an index can only have a single ellipsis ('...')") + elif n_ell == 1: + # replace with proper number of newaxis + indices = list(indices) + ell_id = indices.index(...) + indices[ell_id:ell_id + 1] = (None, ) * ( + self.base_arr.ndim - len(indices) + 1) + indices = tuple(indices) + + assert len(indices) == base.ndim + self.indices = indices + + def __call__(self): + """Create the sub-array. + """ + # step 1: figure out the new inames for each axis + # - keep the old iname if indexed by ':', by a lazy boolean array, + # or is part of an ellipsis + # - make a new iname otherwise + + # step 2: figure out the new shape + # - for surviving old inames, shapes are unchanged + # - for new inames, shapes are calculated + # + # NOTE: if the index is a slice with variable step, say '0:k:n', + # then 'k' must be known at the time of code generation. + # Otherwise the new shape 'n // k' would not be an affine expression. + + # step 3: figure out the new expression + # substitute the inames of changed axes with the properly updated + # expressions. + for idx in self.indices: + if idx is None: + # newaxis + raise NotImplementedError() + elif isinstance(idx, Ellipsis): + # wildcard ellipsis + raise NotImplementedError() + elif isinstance(idx, int): + # numerical indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, Unsigned): + # symbolic indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, slice): + # slicing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, tuple): + # fancy indexing with static shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + for fc_id in idx: + if isinstance(fc_id, int): + # static + raise NotImplementedError() + else: + assert isinstance(fc_id, Unsigned) + # (semi-)dynamic + raise NotImplementedError() + elif isinstance(idx, Array): + # fancy indexing with dynamic shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + else: + raise IndexError( + "only integers, slices (`:`), ellipses (`...`), " + "numpy.newaxis (`None`) and integer or boolean " + "Arrays are valid indices") + + name_prefix = get_internal_name_prefix(self.base_arr.name) + name = (name_prefix + self.base_arr.name + + '_subarray%d' % self.base_arr.__class__._counter) + sub_arr = self.base_arr.with_name(name) + + new_inames = sub_arr._make_default_inames() + new_shape = tuple(Unsigned() for iax in range(self.base_arr.ndim)) + iname_maps = {} + new_expr = substitute(self.base_arr.expr, iname_maps) + + sub_arr.inames = new_inames + sub_arr._shape = new_shape + sub_arr.expr = new_expr + + # add base array as an intermediate object + sub_arr.intermediaries[self.base_arr.name] = \ + self.base_arr.as_stateless() + + raise NotImplementedError() + +# }}} End sub-array factory + # {{{ special subtypes of array # for more readable names and handy constructors @@ -1598,3 +1714,18 @@ def to_lappy_shape(shape): # }}} End digest shape specifiers + +# {{{ misc utils + + +def get_internal_name_prefix(name): + """If the name is already an internal name (starting with '__'), return ''; + otherwise return '__'. + """ + if len(name) > 1 and name[:2] == '__': + name_prefix = '' + else: + name_prefix = '__' + return name_prefix + +# }}} End misc utils -- GitLab From cc3b97fca97a945c8fb8d785949424ec238b1c66 Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Dec 2019 23:17:16 -0600 Subject: [PATCH 04/59] Py2 fix --- lappy/core/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 9a60089..2bd12a2 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1417,7 +1417,7 @@ class SubArrayFactory(object): elif n_ell == 1: # replace with proper number of newaxis indices = list(indices) - ell_id = indices.index(...) + ell_id = indices.index(Ellipsis) indices[ell_id:ell_id + 1] = (None, ) * ( self.base_arr.ndim - len(indices) + 1) indices = tuple(indices) -- GitLab From e68ca2d0d4d3fba8592d4375907f3c9fc7bf322f Mon Sep 17 00:00:00 2001 From: xywei Date: Wed, 25 Dec 2019 22:47:10 -0600 Subject: [PATCH 05/59] Staging updates --- lappy/core/array.py | 173 ++-------------------------------- lappy/core/basic_ops.py | 184 ++++++++++++++++++++++++++++++++++++- lappy/core/masked_array.py | 64 +++++++++++++ 3 files changed, 255 insertions(+), 166 deletions(-) create mode 100644 lappy/core/masked_array.py diff --git a/lappy/core/array.py b/lappy/core/array.py index 2bd12a2..d857906 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -15,14 +15,14 @@ import functools import islpy as isl import pymbolic -from pymbolic import var, evaluate, substitute +from pymbolic import var, evaluate from pymbolic.primitives import ( Expression, Substitution, Lookup, Subscript, Variable, Product) from pymbolic.mapper.evaluator import UnknownVariableError from pprint import pformat -copyright__ = "Copyright (C) 2017 Sophia Lin and Andreas Kloeckner" +copyright__ = "Copyright (C) 2019 Sophia Lin, Andreas Kloeckner and Xiaoyu Wei" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -409,12 +409,16 @@ class Array(LazyObjectBase): :param idx: a tuple of indices or slices. """ if not isinstance(indices, tuple): - # array of bools + # masking assert isinstance(indices, Array) assert indices.ndim == self.ndim - arr_fac = FilteredArrayFactory(self, indices) + from lappy.core.basic_ops import MaskedArrayFactory + arr_fac = MaskedArrayFactory(self, indices) else: + # indexing / mixture of indexing and masking assert isinstance(indices, tuple) + # FIXME only if no mixed use of indexing and masking + from lappy.core.basic_ops import SubArrayFactory arr_fac = SubArrayFactory(self, indices) sub_arr = arr_fac() @@ -1350,167 +1354,6 @@ class Array(LazyObjectBase): # }}} End array class -# {{{ filtered array factory - - -class FilteredArrayFactory(object): - """Factory class of the sub-arrays produced by indexing with a boolean - array with equal shape. - - Filtered arrays cannot be created as stand-alone expressions; they - are used solely for the lhs of an assignment. For example, to execute - - A[A > 0] = B[A > 0] - - It is achieved by overloading __setitem__() of the LHS. - - If the user asks for the value of A[A > 0], the kernel will compute - the full array A and the boolean array (A > 0). And then lappy will - simply use numpy to give the results. - """ - - def __init__(self, base, mask): - assert isinstance(base, Array) - assert isinstance(mask, Array) - assert base.ndim == mask.ndim - self.base_arr = base - self.mask_arr = mask - # TODO: precondition on equal shapes - - def __call__(self): - # TODO: make a special kind of array, whose evaluation returns multiple - # results, which form the final reults after some post-processing. - # - # e.g., A[A > 0] - raise NotImplementedError() - -# }}} End filtered array factory - -# {{{ sub-array factory - - -class SubArrayFactory(object): - """Factory class of the sub-arrays produced by indexing with a tuple of - (generalized) indices. - - Indexing any axis with a boolean array will make the sub array a filtered - array, which has subtle effects on the behavior. For details, see - :class:`FilteredArrayFactory`. - """ - - def __init__(self, base, indices): - assert isinstance(base, Array) - self.base_arr = base - - assert isinstance(indices, tuple) - assert len(indices) <= base.ndim + 1 # at most: all axes + an ellipsis - - # step 0: expand ellipsis - # normalize the indices to be of length ndim - n_ell = 0 - for idx in indices: - if idx is Ellipsis: - n_ell += 1 - if n_ell > 1: - raise IndexError( - "an index can only have a single ellipsis ('...')") - elif n_ell == 1: - # replace with proper number of newaxis - indices = list(indices) - ell_id = indices.index(Ellipsis) - indices[ell_id:ell_id + 1] = (None, ) * ( - self.base_arr.ndim - len(indices) + 1) - indices = tuple(indices) - - assert len(indices) == base.ndim - self.indices = indices - - def __call__(self): - """Create the sub-array. - """ - # step 1: figure out the new inames for each axis - # - keep the old iname if indexed by ':', by a lazy boolean array, - # or is part of an ellipsis - # - make a new iname otherwise - - # step 2: figure out the new shape - # - for surviving old inames, shapes are unchanged - # - for new inames, shapes are calculated - # - # NOTE: if the index is a slice with variable step, say '0:k:n', - # then 'k' must be known at the time of code generation. - # Otherwise the new shape 'n // k' would not be an affine expression. - - # step 3: figure out the new expression - # substitute the inames of changed axes with the properly updated - # expressions. - for idx in self.indices: - if idx is None: - # newaxis - raise NotImplementedError() - elif isinstance(idx, Ellipsis): - # wildcard ellipsis - raise NotImplementedError() - elif isinstance(idx, int): - # numerical indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, Unsigned): - # symbolic indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, slice): - # slicing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, tuple): - # fancy indexing with static shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - for fc_id in idx: - if isinstance(fc_id, int): - # static - raise NotImplementedError() - else: - assert isinstance(fc_id, Unsigned) - # (semi-)dynamic - raise NotImplementedError() - elif isinstance(idx, Array): - # fancy indexing with dynamic shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - else: - raise IndexError( - "only integers, slices (`:`), ellipses (`...`), " - "numpy.newaxis (`None`) and integer or boolean " - "Arrays are valid indices") - - name_prefix = get_internal_name_prefix(self.base_arr.name) - name = (name_prefix + self.base_arr.name - + '_subarray%d' % self.base_arr.__class__._counter) - sub_arr = self.base_arr.with_name(name) - - new_inames = sub_arr._make_default_inames() - new_shape = tuple(Unsigned() for iax in range(self.base_arr.ndim)) - iname_maps = {} - new_expr = substitute(self.base_arr.expr, iname_maps) - - sub_arr.inames = new_inames - sub_arr._shape = new_shape - sub_arr.expr = new_expr - - # add base array as an intermediate object - sub_arr.intermediaries[self.base_arr.name] = \ - self.base_arr.as_stateless() - - raise NotImplementedError() - -# }}} End sub-array factory - # {{{ special subtypes of array # for more readable names and handy constructors diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 038546a..02fbe56 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -24,11 +24,187 @@ from pymbolic import var, evaluate, substitute from pymbolic.primitives import Variable, Product from numpy import AxisError -from lappy.core.array import to_lappy_shape, to_lappy_unsigned +from lappy.core.array import ( + Array, Unsigned, to_lappy_shape, to_lappy_unsigned, + get_internal_name_prefix) from lappy.core.preconditions import ( EvaluationPrecondition, make_size_conservation_condition) +# {{{ masking + + +class MaskedArrayFactory(object): + """Factory class of the sub-arrays produced by indexing with a boolean + array with equal shape. + + Filtered arrays cannot be created as stand-alone expressions; they + are used solely for the lhs of an assignment. For example, to execute + + A[A > 0] = B[A > 0] + + It is achieved by overloading __setitem__() of the LHS. + + If the user asks for the value of A[A > 0], the kernel will compute + the full array A and the boolean array (A > 0). And then lappy will + simply use numpy to give the results. + """ + + def __init__(self, base, mask): + assert isinstance(base, Array) + assert isinstance(mask, Array) + assert base.ndim == mask.ndim + self.base_arr = base + self.mask_arr = mask + # TODO: precondition on equal shapes + + def __call__(self): + # TODO: make a special kind of array, whose evaluation returns multiple + # results, which form the final reults after some post-processing. + # + # e.g., A[A > 0] + raise NotImplementedError() + +# }}} End masking + +# {{{ indexing + + +class SubArrayFactory(object): + """Factory class of the sub-arrays produced by indexing with a tuple of + (generalized) indices. + + Indexing any axis with a boolean array will make the sub array a filtered + array, which has subtle effects on the behavior. For details, see + :class:`FilteredArrayFactory`. + """ + + def __init__(self, base, indices): + assert isinstance(base, Array) + self.base_arr = base + + assert isinstance(indices, tuple) + assert len(indices) <= base.ndim + 1 # at most: all axes + an ellipsis + + # step 0.1: expand ellipsis + # normalize the indices to be of length ndim + n_ell = 0 + for idx in indices: + if idx is Ellipsis: + n_ell += 1 + if n_ell > 1: + raise IndexError( + "an index can only have a single ellipsis ('...')") + elif n_ell == 1: + # replace with proper number of newaxis + indices = list(indices) + ell_id = indices.index(Ellipsis) + indices[ell_id:ell_id + 1] = (None, ) * ( + self.base_arr.ndim - len(indices) + 1) + indices = tuple(indices) + + assert len(indices) == base.ndim + self.indices = indices + + # step 0.2: handle boolean masks + # masks can be applied to one or more axes + # TODO + + def __call__(self): + """Create the sub-array. + """ + # step 1: figure out the new inames for each axis + # - keep the old iname if indexed by ':', by a lazy boolean array, + # or is part of an ellipsis + # - make a new iname otherwise + new_inames = [] + for idx, iname in zip(self.indices, self.base_arr.inames): + if idx is None: + new_inames.append(iname) + elif isinstance(idx, Array): + assert idx.ndim + new_inames.append(iname) + + # step 2: figure out the new shape + # - for surviving old inames, shapes are unchanged + # - for new inames, shapes are calculated + # + # NOTE: if the index is a slice with variable step, say '0:k:n', + # then 'k' must be known at the time of code generation. + # Otherwise the new shape 'n // k' would not be an affine expression. + + # step 3: figure out the new expression + # substitute the inames of changed axes with the properly updated + # expressions. + for idx in self.indices: + if idx is None: + # newaxis + raise NotImplementedError() + elif isinstance(idx, Ellipsis): + # wildcard ellipsis + raise NotImplementedError() + elif isinstance(idx, int): + # numerical indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, Unsigned): + # symbolic indexing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, slice): + # slicing + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + elif isinstance(idx, tuple): + # fancy indexing with static shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + for fc_id in idx: + if isinstance(fc_id, int): + # static + raise NotImplementedError() + else: + assert isinstance(fc_id, Unsigned) + # (semi-)dynamic + raise NotImplementedError() + elif isinstance(idx, Array): + # fancy indexing with dynamic shape + if self.ndim == 0: + raise TypeError("invalid index to scalar variable") + raise NotImplementedError() + else: + raise IndexError( + "only integers, slices (`:`), ellipses (`...`), " + "numpy.newaxis (`None`) and integer or boolean " + "arrays are valid indices") + + name_prefix = get_internal_name_prefix(self.base_arr.name) + name = (name_prefix + self.base_arr.name + + '_subarray%d' % self.base_arr.__class__._counter) + sub_arr = self.base_arr.with_name(name) + + new_inames = sub_arr._make_default_inames() + new_shape = tuple(Unsigned() for iax in range(self.base_arr.ndim)) + iname_maps = {} + new_expr = substitute(self.base_arr.expr, iname_maps) + + sub_arr.inames = new_inames + sub_arr._shape = new_shape + sub_arr.expr = new_expr + + # add base array as an intermediate object + sub_arr.intermediaries[self.base_arr.name] = \ + self.base_arr.as_stateless() + + raise NotImplementedError() + +# }}} End indexing + +# {{{ reshape + def reshape(array, newshape, order='C', name=None, inames=None): """Reshape an array to the given shape. @@ -189,6 +365,10 @@ def reshape(array, newshape, order='C', name=None, inames=None): return new_arr +# }}} End reshape + +# {{{ transpose + def transpose(array, axes=None, name=None): """Transpose. @@ -234,3 +414,5 @@ def transpose(array, axes=None, name=None): new_arr.inames = tuple(array.inames[axes[i]] for i in range(ndim)) new_arr._shape = tuple(array._shape[axes[i]] for i in range(ndim)) return new_arr + +# }}} End transpose diff --git a/lappy/core/masked_array.py b/lappy/core/masked_array.py new file mode 100644 index 0000000..0f51b34 --- /dev/null +++ b/lappy/core/masked_array.py @@ -0,0 +1,64 @@ +from __future__ import division, absolute_import, print_function + +from lappy.core.array import Array + +__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +__doc__ = """ +============= +Masked Arrays +============= +""" + + +class MaskedArrayError(Exception): + pass + + +class MaskError(MaskedArrayError): + pass + + +class MaskedArray(Array): + """An array class with possibly masked values. + The mask array is an array of booleans that shares the shape and inames + with the (base) data array, and lives in the same closure. + + For a masked array A, A.expr and A.value means the data array. To get the + value of the final results, access A.masked_value instead. + + .. attribute:: mask_expr + + expression for the mask array + + .. attribute:: mask_value + + value for the mask array + + .. attribute:: masked_value + + value of the masked array (after post-processing) + """ + _counter = 0 + _name_prefix = '__lappy_masked_array_' + pass -- GitLab From 52e00a5480233370826c99c74d4de075f069d0c7 Mon Sep 17 00:00:00 2001 From: xywei Date: Sun, 23 Feb 2020 23:05:34 -0600 Subject: [PATCH 06/59] Fix names --- lappy/core/basic_ops.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 02fbe56..1a577c8 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -74,9 +74,12 @@ class SubArrayFactory(object): """Factory class of the sub-arrays produced by indexing with a tuple of (generalized) indices. - Indexing any axis with a boolean array will make the sub array a filtered + For the syntax and semantics are the same as numpy arrays + https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html + + Indexing any axis with a boolean array will make the sub array a masked array, which has subtle effects on the behavior. For details, see - :class:`FilteredArrayFactory`. + :class:`MaskedArrayFactory`. """ def __init__(self, base, indices): -- GitLab From 023f7bd6ef2d040a6ac56454f0a971c953fd02e4 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 24 Feb 2020 11:51:09 -0600 Subject: [PATCH 07/59] Add scalar subclass --- lappy/core/array.py | 30 +++++++-- lappy/core/basic_ops.py | 138 ++++++++++++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 29 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index d857906..a2988b6 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1359,18 +1359,32 @@ class Array(LazyObjectBase): # for more readable names and handy constructors -class Int(Array): - """Integer (index type). +class Scalar(Array): + """Scalar. """ _counter = 0 - _name_prefix = '__lappy_int_' + _name_prefix = '__lappy_scalar_' def __init__(self, name=None, **kwargs): if name is not None: kwargs['name'] = name - kwargs['shape'] = () kwargs['ndim'] = 0 + super(Scalar, self).__init__(**kwargs) + + def __getitem__(self, indices): + raise TypeError("cannot sub-index a scalar") + + +class Int(Scalar): + """Integer (index type). + """ + _counter = 0 + _name_prefix = '__lappy_int_' + + def __init__(self, name=None, **kwargs): + if name is not None: + kwargs['name'] = name super(Int, self).__init__(**kwargs) self.is_integral = True @@ -1556,6 +1570,14 @@ def to_lappy_shape(shape): return tuple(components) +def isscalar(obj): + """Like np.isscalar, but also works for lazy objects. + """ + if np.isscalar(obj): + return True + else: + return isinstance(obj, Int) + # }}} End digest shape specifiers # {{{ misc utils diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 1a577c8..6ee8433 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -26,7 +26,7 @@ from numpy import AxisError from lappy.core.array import ( Array, Unsigned, to_lappy_shape, to_lappy_unsigned, - get_internal_name_prefix) + isscalar, get_internal_name_prefix) from lappy.core.preconditions import ( EvaluationPrecondition, make_size_conservation_condition) @@ -76,42 +76,130 @@ class SubArrayFactory(object): For the syntax and semantics are the same as numpy arrays https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html - - Indexing any axis with a boolean array will make the sub array a masked - array, which has subtle effects on the behavior. For details, see - :class:`MaskedArrayFactory`. """ - def __init__(self, base, indices): + def __init__(self, base, selobj): + """ + :param base: the base array. + :param selobj: the selection object. + """ assert isinstance(base, Array) self.base_arr = base - assert isinstance(indices, tuple) - assert len(indices) <= base.ndim + 1 # at most: all axes + an ellipsis + if isinstance(selobj, tuple): + selist = list(selobj) + elif isinstance(selobj, (int, Array, list, np.ndarray)): + selist = [selobj, ] + else: + raise TypeError() + + self.selectors = selist + +# {{{ consume the selection object + + def _get_out_ndim(self, selist): + """Given the selction object in a list, computes the rank (ndim) of the + output array. + """ + ndim = 0 + n_annihil_dim = 0 + has_ellipsis = False + for sel in selist: + if isinstance(sel, slice): # slicing + ndim += 1 + elif sel is None: # np.newaxis + ndim += 1 + elif isscalar(sel): # scalar indexing + n_annihil_dim += 1 + elif isinstance(sel, (tuple, list, np.ndarray, Array)): + sel_arr = np.array(sel) + if sel_arr.dtype is np.dtype(bool) or sel_arr.dtype is bool: + # boolean indexing "flattens" + ndim += 1 + else: + # integer indexing keeps the dimension + ndim += sel_arr.ndim + else: + if sel is not Ellipsis: + raise TypeError() + else: + has_ellipsis = True + + if has_ellipsis: + ndim = max(ndim, self.base_arr.ndim, n_annihil_dim) + + return ndim + + def _prepare_index_list(selobj): + """Identify if the selection object, :var:`selobj`, is/contains a lazy + array. There are three cases being considered: - # step 0.1: expand ellipsis - # normalize the indices to be of length ndim + (1) basic slicing and indexing + (3) integer-typed advanced indexing + (4) boolean-typed advanced indexing + """ + + def _complete_slice(slc, dim): + """Fill in all information for a partially specified slice. + + :param slc: an (incomplete) slice object. + :param dim: the dimension of the vector being sliced. + """ + assert isinstance(slc, slice) + + step_none = slc.step is None + if step_none: + step = 1 + else: + step = slc.step + + start_none = slc.start is None + if not start_none: + start = slc.start + if start < 0: + start += dim + + stop_none = slc.stop is None + if not stop_none: + stop = slc.stop + if stop < 0: + stop += dim + + if step > 0: + start = 0 if start_none else max(0, min(dim, start)) + stop = dim if stop_none else max(start, min(dim, stop)) + else: + start = dim - 1 if start_none else max(-1, min(dim - 1, start)) + stop = -1 if stop_none else max(-1, min(start, stop)) + + return slice(start, stop, step) + + def _consume_ellipsis(selobj): + """Expand ellipsis into slices. + + :param indices: the selection object in the form of a list. + """ n_ell = 0 - for idx in indices: + for idx in selobj: if idx is Ellipsis: n_ell += 1 + if n_ell > 1: raise IndexError( "an index can only have a single ellipsis ('...')") - elif n_ell == 1: - # replace with proper number of newaxis - indices = list(indices) - ell_id = indices.index(Ellipsis) - indices[ell_id:ell_id + 1] = (None, ) * ( - self.base_arr.ndim - len(indices) + 1) - indices = tuple(indices) - - assert len(indices) == base.ndim - self.indices = indices - - # step 0.2: handle boolean masks - # masks can be applied to one or more axes - # TODO + elif n_ell == 0: + selobj.append(Ellipsis) + n_ell += 1 + + assert n_ell == 1 + # replace with proper number of slices + indices = list(indices) + ell_id = indices.index(Ellipsis) + indices[ell_id:ell_id + 1] = (None, ) * ( + self.base_arr.ndim - len(indices) + 1) + indices = tuple(indices) + +# }}} End consume the selection object def __call__(self): """Create the sub-array. -- GitLab From ebe273adce4952bfc40edff2f26329dab9b3c570 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 24 Feb 2020 17:43:36 -0600 Subject: [PATCH 08/59] Add more operators --- lappy/core/array.py | 239 ++++++++++++++++++++++++++++++++++++++++--- lappy/core/ufuncs.py | 12 ++- 2 files changed, 233 insertions(+), 18 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index a2988b6..1f629d1 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -46,7 +46,8 @@ THE SOFTWARE. def default_env(): - """Default environment to be captured. + """Default environment to be captured. Also used as the environment + for all stateless arrays. """ return { "isl_ctx": isl.DEFAULT_CONTEXT, @@ -293,16 +294,23 @@ class Array(LazyObjectBase): kwargs['name'] = name super(Array, self).__init__(**kwargs) - self._ndim = to_lappy_unsigned(kwargs.pop("ndim", None)) + given_ndim = kwargs.pop("ndim", None) + if given_ndim is not None: + self._ndim = to_lappy_unsigned(given_ndim) + else: + self._ndim = None self.domain_expr = domain_expr - if self.ndim is None: + if self._ndim is None and shape is not None: # infer ndim from given shape # NOTE: use fixed ndim only shape = to_lappy_shape(shape) self._ndim = to_lappy_unsigned(int(len(shape))) - self.inames = kwargs.pop("inames", self._make_default_inames()) + if self.ndim > 0: + self.inames = kwargs.pop("inames", self._make_default_inames()) + else: + self.inames = () if self.expr is None: self.expr = self._make_default_expr() @@ -345,10 +353,21 @@ class Array(LazyObjectBase): @property def ndim(self): - if self._ndim == 0: - return 0 - else: - return self._ndim.value + """Numerical value of ndim, -1 if unknown. + """ + try: + ndim = self._ndim.value + if ndim is None: + return -1 # unknown value + else: + return ndim + except AttributeError: + # could be that ndim == 0, since Unsigned cannot be + # used to represent zero values + if self._ndim == 0: + return 0 + else: + return -1 @property def shape(self): @@ -538,7 +557,7 @@ class Array(LazyObjectBase): cp_assumptions = self.assumptions.copy() if stateless: - env = None + env = default_env() else: env = self.env.copy() @@ -1055,8 +1074,9 @@ class Array(LazyObjectBase): """The default expression is the array it self. """ sym_arr = var(self.name) - sym_indices = self._make_axis_indices() if self.ndim > 0: + # array + sym_indices = self._make_axis_indices() expr = Subscript(sym_arr, index=sym_indices) else: # scalar @@ -1076,6 +1096,20 @@ class Array(LazyObjectBase): assert isinstance(sym_s, Unsigned) sym_s.value = s + # {{{ unary operations + + def __abs__(self): + from lappy.core.ufuncs import absolute + return absolute(self) + + def __neg__(self): + from lappy.core.ufuncs import negative + return negative(self) + + # }}} End unary operations + + # {{{ arithmetics + def __add__(self, other): if not isinstance(other, Array): warnings.warn( @@ -1087,7 +1121,184 @@ class Array(LazyObjectBase): from lappy.core.ufuncs import add return add(self, other_arr) - __radd__ = __add__ + def __radd__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import add + return add(other_arr, self) + + def __sub__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import subtract + return subtract(self, other_arr) + + def __rsub__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import subtract + return subtract(other_arr, self) + + def __mul__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import multiply + return multiply(self, other_arr) + + def __rmul__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import multiply + return multiply(other_arr, self) + + def __div__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import divide + return divide(self, other_arr) + + __truediv__ = __div__ + + def __rdiv__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import divide + return divide(other_arr, self) + + __rtruediv__ = __rdiv__ + + def __pow__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import power + return power(self, other_arr) + + def __rpow__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import power + return power(other_arr, self) + + def __floordiv__(self, other): + raise NotImplementedError() + + def __rfloordiv__(self, other): + raise NotImplementedError() + + def __mod__(self, other): + raise NotImplementedError() + + def __rmod__(self, other): + raise NotImplementedError() + + # }}} End arithmetics + + # {{{ comparisons + + def __eq__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import equal + return equal(self, other_arr) + + def __ne__(self, other): + return not self == other + + def __le__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import less_equal + return less_equal(self, other_arr) + + def __lt__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import less + return less(self, other_arr) + + def __ge__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import greater_equal + return greater_equal(self, other_arr) + + def __gt__(self, other): + if not isinstance(other, Array): + warnings.warn( + "Implicit conversion of %s to Lappy array" + % str(type(other))) + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import greater + return greater(self, other_arr) + + # }}} End comparisons @staticmethod def _generic_constructor(obj, data=None): @@ -1431,7 +1642,7 @@ def to_lappy_array(arr_like, name=None, base_env=None): if np.isscalar(arr_like): try: - dtype = np.dtype(arr_like) + dtype = np.dtype(type(arr_like)) except TypeError: dtype = None @@ -1439,9 +1650,9 @@ def to_lappy_array(arr_like, name=None, base_env=None): if int(arr_like) == arr_like: arr_class = Int else: - arr_class = Array + arr_class = Scalar - arr = arr_class(name=name, dtype=dtype, env=base_env) + arr = arr_class(name=name, dtype=dtype, env=base_env, value=arr_like) arr.env[arr.name] = arr_like arr.arguments = dict() return arr diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 1c8d322..ad4b6aa 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -249,8 +249,13 @@ class BinaryOperation(UFunc): if self.dtype is None: # FIXME: allow numpy type promotion rules - assert a.dtype == b.dtype - new_dtype = a.dtype + if a.dtype is None: + new_dtype = b.dtype + elif b.dtype is None: + new_dtype = a.dtype + else: + assert a.dtype == b.dtype + new_dtype = a.dtype else: new_dtype = self.dtype @@ -364,6 +369,7 @@ add = BinaryOperation(expr_func=lambda x, y: x + y, preserve_integral=True) subtract = BinaryOperation(expr_func=lambda x, y: x - y, preserve_integral=True) multiply = BinaryOperation(expr_func=lambda x, y: x * y, preserve_integral=True) divide = BinaryOperation(expr_func=lambda x, y: x / y) +power = BinaryOperation(expr_func=lambda x, y: x**y) equal = BinaryOperation(lambda x, y: x.eq(y)) not_equal = BinaryOperation(lambda x, y: x.ne(y)) @@ -381,6 +387,4 @@ mod = remainder = BinaryOperation(lambda x, y: x % y) # floating point mod, and returns the same sign with x fmod = BinaryOperation(lambda x, y: FloatingPointRemainder(x, y)) -# TODO: add these when needed: arctan2, alltrue, sometrue, logical_xor, hypot - # }}} End binary ufuncs -- GitLab From 2bc9ee149c9b090a32d8d1c5221ae9f6681a311f Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 24 Feb 2020 18:12:04 -0600 Subject: [PATCH 09/59] Better numpy consistency --- lappy/__init__.py | 5 +- lappy/core/array.py | 10 +++- lappy/core/basic_ops.py | 18 +++---- test/{test_array_api.py => test_api_array.py} | 2 +- test/test_numpy_consistency.py | 50 +++++++++++++++++++ test/test_reshape.py | 2 +- test/test_transpose.py | 2 +- test/test_ufunc.py | 10 ++-- 8 files changed, 77 insertions(+), 22 deletions(-) rename test/{test_array_api.py => test_api_array.py} (98%) create mode 100644 test/test_numpy_consistency.py diff --git a/lappy/__init__.py b/lappy/__init__.py index f6e08c4..2479184 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -22,7 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from lappy.core import Array, to_lappy_array, to_lappy_shape +from lappy.core import Array as ndarray - -__all__ = ["Array", "to_lappy_array", "to_lappy_shape"] +__all__ = ["ndarray", ] diff --git a/lappy/core/array.py b/lappy/core/array.py index 1f629d1..71c0469 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -285,6 +285,8 @@ class Array(LazyObjectBase): _counter = 0 _name_prefix = '__lappy_array_' + # {{{ constructor + def __init__(self, name=None, shape=None, domain_expr=None, **kwargs): # default expr if 'expr' not in kwargs: @@ -339,6 +341,8 @@ class Array(LazyObjectBase): if self.assumptions is not None: assert self.is_integral + # }}} End constructor + def _to_repr_dict(self): repr_dict = super(Array, self)._to_repr_dict() repr_dict.update({ @@ -379,9 +383,9 @@ class Array(LazyObjectBase): @property def size(self): - return self.shadow_size() + return self._shadow_size() - def shadow_size(self, axes=None): + def _shadow_size(self, axes=None): """When axes is None, computes the full size. Otherwise computes the size of the projected array on the given axes. """ @@ -422,6 +426,8 @@ class Array(LazyObjectBase): from lappy.core.basic_ops import transpose return transpose(self, axes, name) + T = transpose + def __getitem__(self, indices): """Returns a new array by sub-indexing the current array. diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 6ee8433..617414a 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -362,16 +362,16 @@ def reshape(array, newshape, order='C', name=None, inames=None): """The shape must be an integer along the newaxis. """ old_size = evaluate(array.size, context) - shadow_size = evaluate(Product(shadow_expr), context) + _shadow_size = evaluate(Product(shadow_expr), context) act_new_shape = tuple( var(s.name) if hasattr(s, 'name') else s for s in newshape) - if not old_size % shadow_size == 0: + if not old_size % _shadow_size == 0: raise ValueError("cannot reshape array of size %s into shape %s" % (str(old_size), str(act_new_shape))) - return old_size % shadow_size == 0 + return old_size % _shadow_size == 0 # add pre-eval runtime checks on size consistency new_precond.append( @@ -422,20 +422,20 @@ def reshape(array, newshape, order='C', name=None, inames=None): if order == 'C': iname_subs = { - array.inames[0]: flat_id // array.shadow_size(range(1, array.ndim)) + array.inames[0]: flat_id // array._shadow_size(range(1, array.ndim)) } for iaxis in range(1, array.ndim): - flat_id = flat_id % array.shadow_size(range(iaxis, array.ndim)) - iname_subs[array.inames[iaxis]] = flat_id // array.shadow_size( + flat_id = flat_id % array._shadow_size(range(iaxis, array.ndim)) + iname_subs[array.inames[iaxis]] = flat_id // array._shadow_size( range(iaxis + 1, array.ndim)) else: assert order == 'F' iname_subs = { - array.inames[-1]: flat_id // array.shadow_size(range(array.ndim - 1)) + array.inames[-1]: flat_id // array._shadow_size(range(array.ndim - 1)) } for iaxis in range(-2, -array.ndim - 1, -1): - flat_id = flat_id % array.shadow_size(range(array.ndim + iaxis + 1)) - iname_subs[array.inames[iaxis]] = flat_id // array.shadow_size( + flat_id = flat_id % array._shadow_size(range(array.ndim + iaxis + 1)) + iname_subs[array.inames[iaxis]] = flat_id // array._shadow_size( range(array.ndim + iaxis)) new_arr = array.with_name(name) diff --git a/test/test_array_api.py b/test/test_api_array.py similarity index 98% rename from test/test_array_api.py rename to test/test_api_array.py index 4bc703f..04f9bd1 100644 --- a/test/test_array_api.py +++ b/test/test_api_array.py @@ -23,7 +23,7 @@ THE SOFTWARE. """ import numpy as np -from lappy import Array, to_lappy_array +from lappy.core import Array, to_lappy_array import pytest diff --git a/test/test_numpy_consistency.py b/test/test_numpy_consistency.py new file mode 100644 index 0000000..6d57de3 --- /dev/null +++ b/test/test_numpy_consistency.py @@ -0,0 +1,50 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import numpy as np +import lappy as ly +import pytest + +from warnings import warn + +def test_array_public_interface(): + npi = dir(np.ndarray) + lpi = dir(ly.ndarray) + + missing = [] + extra = [] + + for fn in npi: + if fn not in lpi: + missing.append(fn) + + for fn in lpi: + if fn not in npi: + extra.append(fn) + + warn("\n>>>> Missing array API functions:\n %s" + % ', '.join(missing)) + warn("\n>>>> Extra array API functions:\n %s" + % ', '.join(extra)) + # assert success diff --git a/test/test_reshape.py b/test/test_reshape.py index 66eb8bb..9848ada 100644 --- a/test/test_reshape.py +++ b/test/test_reshape.py @@ -54,7 +54,7 @@ def test_reshape(ctx_factory, in_shape, out_shape, order, dtype=np.float64): env = la.core.array.default_env() env['cl_ctx'] = ctx_factory() - A = la.Array('A', ndim=ndim, env=env) # noqa: N806 + A = la.core.Array('A', ndim=ndim, env=env) # noqa: N806 B = A.reshape(out_shape, order=order, name='B') # noqa: N806 data = np.random.rand(*in_shape).astype(dtype) diff --git a/test/test_transpose.py b/test/test_transpose.py index 3ea5516..5d9ee06 100644 --- a/test/test_transpose.py +++ b/test/test_transpose.py @@ -42,7 +42,7 @@ def test_transpose(ctx_factory, test_shape, axes, dtype=np.float64): env = la.core.array.default_env() env['cl_ctx'] = ctx_factory() - A = la.Array('A', ndim=ndim, env=env) # noqa: N806 + A = la.core.Array('A', ndim=ndim, env=env) # noqa: N806 B = A.transpose(axes=axes, name='B') # noqa: N806 data = np.random.rand(*test_shape).astype(dtype) diff --git a/test/test_ufunc.py b/test/test_ufunc.py index 296cc8b..d01d2bd 100644 --- a/test/test_ufunc.py +++ b/test/test_ufunc.py @@ -43,7 +43,7 @@ def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): ndim = len(shapes[0]) # symbolic code is reused for different shapes (same ndim) - mat = la.Array('A', ndim=ndim, dtype=dtype, env=env) + mat = la.core.Array('A', ndim=ndim, dtype=dtype, env=env) fmat = getattr(math, ufunc)(mat).with_name('B') for shape in shapes: @@ -71,8 +71,8 @@ def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): sym_shape = tuple('s%d' % i for i in range(ndim)) - mat_a = la.Array('A', shape=sym_shape, dtype=dtype, env=env) - mat_b = la.Array('B', shape=sym_shape, dtype=dtype, env=env) + mat_a = la.core.Array('A', shape=sym_shape, dtype=dtype, env=env) + mat_b = la.core.Array('B', shape=sym_shape, dtype=dtype, env=env) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') @@ -114,8 +114,8 @@ def test_binary_ufunc_with_broadcast(ctx_factory, ufunc, shapes, dtype): sym_shape_a = tuple('s_a_%d' % i for i in range(ndim_a)) sym_shape_b = tuple('s_b_%d' % i for i in range(ndim_b)) - mat_a = la.Array('A', shape=sym_shape_a, dtype=dtype, env=env) - mat_b = la.Array('B', shape=sym_shape_b, dtype=dtype, env=env) + mat_a = la.core.Array('A', shape=sym_shape_a, dtype=dtype, env=env) + mat_b = la.core.Array('B', shape=sym_shape_b, dtype=dtype, env=env) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') -- GitLab From 5eb56b213e791bbedf464c4434bfbac9fad0ccc8 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 24 Feb 2020 18:39:40 -0600 Subject: [PATCH 10/59] Flake8 fixes --- lappy/__init__.py | 2 +- lappy/core/array.py | 3 +++ lappy/core/basic_ops.py | 17 ++++++++++------- test/test_numpy_consistency.py | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lappy/__init__.py b/lappy/__init__.py index 2479184..ce2c953 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -22,6 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from lappy.core import Array as ndarray +from lappy.core import ndarray __all__ = ["ndarray", ] diff --git a/lappy/core/array.py b/lappy/core/array.py index 71c0469..ac44c83 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1569,8 +1569,11 @@ class Array(LazyObjectBase): expr_dict[old_index[i]] = old_index[i] return expr_dict + # }}} End array class +ndarray = Array + # {{{ special subtypes of array # for more readable names and handy constructors diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 617414a..4a64228 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -130,7 +130,7 @@ class SubArrayFactory(object): return ndim - def _prepare_index_list(selobj): + def _prepare_index_list(self, selobj): """Identify if the selection object, :var:`selobj`, is/contains a lazy array. There are three cases being considered: @@ -139,13 +139,16 @@ class SubArrayFactory(object): (4) boolean-typed advanced indexing """ - def _complete_slice(slc, dim): + def _complete_slice(self, slc, iaxis): """Fill in all information for a partially specified slice. :param slc: an (incomplete) slice object. - :param dim: the dimension of the vector being sliced. + :param iaxis: the index of the axis being sliced. """ assert isinstance(slc, slice) + dim = self.base_arr.shape[iaxis] + if isinstance(dim, str): + dim = var(dim) step_none = slc.step is None if step_none: @@ -174,7 +177,7 @@ class SubArrayFactory(object): return slice(start, stop, step) - def _consume_ellipsis(selobj): + def _consume_ellipsis(self, selobj): """Expand ellipsis into slices. :param indices: the selection object in the form of a list. @@ -193,7 +196,7 @@ class SubArrayFactory(object): assert n_ell == 1 # replace with proper number of slices - indices = list(indices) + indices = list(selobj) ell_id = indices.index(Ellipsis) indices[ell_id:ell_id + 1] = (None, ) * ( self.base_arr.ndim - len(indices) + 1) @@ -431,8 +434,8 @@ def reshape(array, newshape, order='C', name=None, inames=None): else: assert order == 'F' iname_subs = { - array.inames[-1]: flat_id // array._shadow_size(range(array.ndim - 1)) - } + array.inames[-1]: flat_id // array._shadow_size(range(array.ndim - 1)) + } for iaxis in range(-2, -array.ndim - 1, -1): flat_id = flat_id % array._shadow_size(range(array.ndim + iaxis + 1)) iname_subs[array.inames[iaxis]] = flat_id // array._shadow_size( diff --git a/test/test_numpy_consistency.py b/test/test_numpy_consistency.py index 6d57de3..6f22788 100644 --- a/test/test_numpy_consistency.py +++ b/test/test_numpy_consistency.py @@ -24,10 +24,10 @@ THE SOFTWARE. import numpy as np import lappy as ly -import pytest from warnings import warn + def test_array_public_interface(): npi = dir(np.ndarray) lpi = dir(ly.ndarray) -- GitLab From 72a91f02e5d6f6028aa22b196c0e34293bdb9fba Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 24 Feb 2020 22:31:50 -0600 Subject: [PATCH 11/59] Fix imports --- lappy/core/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lappy/core/__init__.py b/lappy/core/__init__.py index e3b5af4..f808938 100644 --- a/lappy/core/__init__.py +++ b/lappy/core/__init__.py @@ -22,7 +22,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from lappy.core.array import Array, to_lappy_array, to_lappy_shape +from lappy.core.array import ( + Array, ndarray, to_lappy_array, to_lappy_shape) -__all__ = ["Array", "to_lappy_array", "to_lappy_shape"] +__all__ = [ + "Array", "ndarray", + "to_lappy_array", "to_lappy_shape"] -- GitLab From 3830cb5736cfb022b7190058d24dec6ca4cc760f Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 25 Feb 2020 11:56:56 -0600 Subject: [PATCH 12/59] Use plain ints for ndim --- lappy/core/array.py | 33 +++++++-------------------------- lappy/core/basic_ops.py | 2 +- lappy/core/broadcast.py | 10 +++------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index ac44c83..1139c12 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -296,18 +296,17 @@ class Array(LazyObjectBase): kwargs['name'] = name super(Array, self).__init__(**kwargs) - given_ndim = kwargs.pop("ndim", None) - if given_ndim is not None: - self._ndim = to_lappy_unsigned(given_ndim) - else: - self._ndim = None self.domain_expr = domain_expr + self.ndim = kwargs.pop("ndim", None) - if self._ndim is None and shape is not None: + if self.ndim is None and shape is not None: # infer ndim from given shape # NOTE: use fixed ndim only shape = to_lappy_shape(shape) - self._ndim = to_lappy_unsigned(int(len(shape))) + self.ndim = int(len(shape)) + + if self.ndim is None: + raise ValueError("ndim cannot be determined") if self.ndim > 0: self.inames = kwargs.pop("inames", self._make_default_inames()) @@ -355,24 +354,6 @@ class Array(LazyObjectBase): 'assumptions': self.assumptions}) return repr_dict - @property - def ndim(self): - """Numerical value of ndim, -1 if unknown. - """ - try: - ndim = self._ndim.value - if ndim is None: - return -1 # unknown value - else: - return ndim - except AttributeError: - # could be that ndim == 0, since Unsigned cannot be - # used to represent zero values - if self._ndim == 0: - return 0 - else: - return -1 - @property def shape(self): """Returns the shape as a tuple of ints if the value is known, or @@ -568,7 +549,7 @@ class Array(LazyObjectBase): env = self.env.copy() return self.__class__( - name=self.name, ndim=self._ndim, + name=self.name, ndim=self.ndim, inames=self.inames, shape=self._shape, dtype=self.dtype, expr=self.expr, is_integral=self.is_integral, diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 4a64228..50b136c 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -442,7 +442,7 @@ def reshape(array, newshape, order='C', name=None, inames=None): range(array.ndim + iaxis)) new_arr = array.with_name(name) - new_arr._ndim = to_lappy_unsigned(new_ndim) + new_arr.ndim = new_ndim new_arr.inames = inames new_arr.preconditions = new_precond diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 11e5fe4..482ec19 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -24,7 +24,7 @@ from functools import partial from pymbolic import evaluate, substitute, var from pymbolic.primitives import Product, Expression, Max -from lappy.core.array import Unsigned, to_lappy_shape +from lappy.core.array import to_lappy_shape from lappy.core.tools import is_nonnegative_int from lappy.core.preconditions import EvaluationPrecondition @@ -79,7 +79,7 @@ class BroadcastResult(object): """Compute the ndim, shape and size of the broadcast result. """ ndims = [arr.ndim for arr in self.base_arrays] - self._ndim = max(ndims) + self.ndim = max(ndims) # values when available, names otherwise shapes = [arr.shape for arr in self.base_arrays] @@ -226,7 +226,7 @@ class BroadcastResult(object): brdc_arr = base_arr.with_name(name) assert isinstance(self.ndim, int) and self.ndim >= 0 - brdc_arr._ndim = Unsigned(value=self.ndim) + brdc_arr.ndim = self.ndim brdc_arr._shape = lappy_shape # make new inames @@ -261,10 +261,6 @@ class BroadcastResult(object): assert len(self.base_arrays) == len(self.broadcast_arrays) - @property - def ndim(self): - return self._ndim - @property def shape(self): return tuple(s if is_nonnegative_int(s) else str(s) -- GitLab From 33c73029667c5c407bfc136f25400325d9044557 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 25 Feb 2020 11:58:36 -0600 Subject: [PATCH 13/59] Remove Unsigned != 0 constraint --- lappy/core/array.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 1139c12..088044e 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1593,9 +1593,6 @@ class Int(Scalar): assert self.shape == () assert self.ndim == 0 - def __getitem__(self, indices): - raise TypeError("cannot sub-index an integer") - class Unsigned(Int): """Non-negative integer (size type). @@ -1671,9 +1668,6 @@ def to_lappy_array(arr_like, name=None, base_env=None): def to_lappy_unsigned(unsigned_like, name=None, base_env=None): """Do nothing if the array is already an :class:`Unsigned`. Make an :class:`Unsigned` with captured values for an actual number. - - Specifically, returns 0 if the value is 0. This is to avoid infinite - recursion when using Array type to represent shape and ndim. """ if base_env is None: base_env = default_env() @@ -1682,8 +1676,6 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None): return Unsigned(name=name, env=base_env) if np.isscalar(unsigned_like): - if unsigned_like == 0: - return 0 if abs(int(unsigned_like)) - unsigned_like == 0: lunsigned = Unsigned(name=name, value=unsigned_like, env=base_env) lunsigned.env[lunsigned.name] = unsigned_like -- GitLab From 5c2fec88974e495e268d2843ad7d448b73adfd39 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 25 Feb 2020 14:07:43 -0600 Subject: [PATCH 14/59] Add placeholder for basic indexing --- lappy/core/array.py | 1 + lappy/core/basic_ops.py | 142 ++++++++++++++++++++++++++++++---------- 2 files changed, 110 insertions(+), 33 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 088044e..77cbea3 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -145,6 +145,7 @@ class LazyObjectBase(object): def _make_default_name(self): name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) self.__class__._counter += 1 + print("A default is created %s" % name) return name def __str__(self): diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 50b136c..bcc7636 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -76,8 +76,22 @@ class SubArrayFactory(object): For the syntax and semantics are the same as numpy arrays https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html + + .. attribute:: base_arr + + the base array being indexed. + + .. attribute:: selectors + + a list of selection objects. + + .. attribute:: ndim + + ndim of the output array. """ + # {{{ constructor + def __init__(self, base, selobj): """ :param base: the base array. @@ -93,18 +107,35 @@ class SubArrayFactory(object): else: raise TypeError() + assert isinstance(selist, list) + n_ell = 0 + for idx in selist: + if idx is Ellipsis: + n_ell += 1 + + if n_ell > 1: + raise IndexError( + "an index can only have a single ellipsis ('...')") + elif n_ell == 0: + # hand un-addressed axes with a trailing ellipsis + selist.append(Ellipsis) + n_ell += 1 + self.selectors = selist -# {{{ consume the selection object + self.ndim = self._get_out_ndim() + self._expand_ellipsis() + + # }}} End constructor - def _get_out_ndim(self, selist): - """Given the selction object in a list, computes the rank (ndim) of the - output array. + # {{{ consume the selection object + + def _get_out_ndim(self): + """Compute the rank (ndim) of the output array. """ ndim = 0 n_annihil_dim = 0 - has_ellipsis = False - for sel in selist: + for sel in self.selectors: if isinstance(sel, slice): # slicing ndim += 1 elif sel is None: # np.newaxis @@ -122,13 +153,8 @@ class SubArrayFactory(object): else: if sel is not Ellipsis: raise TypeError() - else: - has_ellipsis = True - - if has_ellipsis: - ndim = max(ndim, self.base_arr.ndim, n_annihil_dim) - return ndim + return max(ndim, self.base_arr.ndim, n_annihil_dim) def _prepare_index_list(self, selobj): """Identify if the selection object, :var:`selobj`, is/contains a lazy @@ -177,32 +203,82 @@ class SubArrayFactory(object): return slice(start, stop, step) - def _consume_ellipsis(self, selobj): - """Expand ellipsis into slices. + def _expand_ellipsis(self): + """Expand ellipsis into proper number of slices. + """ + ell_id = self.selectors.index(Ellipsis) + fullslice = slice(None, None, None) + self.selectors[ell_id:ell_id + 1] = ( + (fullslice, ) * (self.base_arr.ndim - len(self.selectors) + 1)) + + # }}} End consume the selection object - :param indices: the selection object in the form of a list. + # {{{ basic indexing + + def _basic_getitem(self): + """When the selectors consist of: + slices, integers, Ellipsis, and np.newaxis (None). """ - n_ell = 0 - for idx in selobj: - if idx is Ellipsis: - n_ell += 1 + ca = 0 # current axis + i_newaxis = 0 + new_shape = [] + new_inames = [] + rhs_index_exprs = [] + for i_sel, sel in enumerate(self.selectors): + if sel is None: + # newaxis does not advance the ca pointer + new_shape.append(to_lappy_unsigned(1)) + new_inames.append(var('__indexing_%s_newaxis_inames_%d' + % (self.base_arr.name, i_newaxis))) + i_newaxis += 1 + elif ca >= self.base_arr.ndim: + raise IndexError('too many indices for array') + elif isinstance(sel, slice): + # TODO + ca += 1 + elif isscalar(sel): + # TODO + ca += 1 + else: + raise TypeError('invalid index type: %s' + % type(self.selectors[i_sel])) + + new_name = self.base_arr._make_default_name() + + new_interm = self.base_arr.intermediaries.copy() + new_interm[self.base_arr.name] = self.base_arr.as_stateless() + + new_expr = rhs_index_exprs # TODO + + obj = { + 'name': new_name, + 'inames': new_inames, + 'expr': new_expr, + 'value': None, + 'domain_expr': self.base_arr.domain_expr, + 'arguments': self.base_arr.arguments.copy(), + 'bound_arguments': self.base_arr.bound_arguments.copy(), + 'env': self.base_arr.env.copy(), + 'preconditions': self.base_arr.preconditions, + 'intermediaries': new_interm, + 'ndim': self.ndim, + 'shape': new_shape, + 'dtype': self.base_arr.dtype, + 'is_integral': self.base_arr.is_integral, + 'assumptions': self.base_arr.assumptions, + } + arr = Array(**obj) - if n_ell > 1: - raise IndexError( - "an index can only have a single ellipsis ('...')") - elif n_ell == 0: - selobj.append(Ellipsis) - n_ell += 1 + # remove self reference + if self.base_arr.name in arr.arguments: + arr.arguments[self.base_arr.name] = self.base_arr.as_stateless() + elif self.base_arr.name in arr.bound_arguments: + arr.bound_arguments[self.base_arr.name] = \ + self.base_arr.as_stateless() - assert n_ell == 1 - # replace with proper number of slices - indices = list(selobj) - ell_id = indices.index(Ellipsis) - indices[ell_id:ell_id + 1] = (None, ) * ( - self.base_arr.ndim - len(indices) + 1) - indices = tuple(indices) + return arr -# }}} End consume the selection object + # }}} End basic indexing def __call__(self): """Create the sub-array. -- GitLab From 75c5b40d808f21c3d71e9920973fffa357f2df1e Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 25 Feb 2020 22:15:17 -0600 Subject: [PATCH 15/59] Add the first indexing test --- test/test_indexing.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/test_indexing.py diff --git a/test/test_indexing.py b/test/test_indexing.py new file mode 100644 index 0000000..6f085eb --- /dev/null +++ b/test/test_indexing.py @@ -0,0 +1,38 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import lappy as la +import numpy as np +from lappy.core.basic_ops import SubArrayFactory + + +def test_subarr_out_ndim(): + arr = la.ndarray('A', ndim=2) + subfac = SubArrayFactory( + arr, + (slice(None, None, None), [[0, 1], [1, 1]], np.newaxis)) + print(subfac.selectors) + print(subfac.ndim) + print(arr.inames) + print(arr.shape) -- GitLab From 812dc4eea83feaff80841f22d0bbfeefdc52c879 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 25 Feb 2020 22:40:13 -0600 Subject: [PATCH 16/59] Refactor ndim to be len(self._shape), closes #13 --- lappy/core/array.py | 87 +++++++++++++++++++++-------------------- lappy/core/basic_ops.py | 1 - lappy/core/broadcast.py | 1 - 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 77cbea3..82d192d 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -298,27 +298,27 @@ class Array(LazyObjectBase): super(Array, self).__init__(**kwargs) self.domain_expr = domain_expr - self.ndim = kwargs.pop("ndim", None) + ndim = kwargs.pop("ndim", None) - if self.ndim is None and shape is not None: + if ndim is None and shape is not None: # infer ndim from given shape # NOTE: use fixed ndim only shape = to_lappy_shape(shape) - self.ndim = int(len(shape)) + ndim = int(len(shape)) - if self.ndim is None: + if ndim is None: raise ValueError("ndim cannot be determined") - if self.ndim > 0: - self.inames = kwargs.pop("inames", self._make_default_inames()) + if ndim > 0: + self.inames = kwargs.pop("inames", self._make_default_inames(ndim)) else: self.inames = () if self.expr is None: - self.expr = self._make_default_expr() + self.expr = self._make_default_expr(ndim) if shape is None: - self._shape = self._make_default_shape() + self._shape = self._make_default_shape(ndim) else: self._shape = to_lappy_shape(shape) @@ -326,18 +326,7 @@ class Array(LazyObjectBase): self.is_integral = kwargs.pop("is_integral", False) self.assumptions = kwargs.pop("assumptions", None) - # scalars - if self.shape == () or self.ndim == 0: - assert self.shape == () and self.ndim == 0 - - # arrays with concrete ndim - try: - ndim_int = int(self.ndim) - assert ndim_int == len(self.shape) - except ValueError: - pass - - # integer arrays + # only integer arrays can have assumptions if self.assumptions is not None: assert self.is_integral @@ -363,6 +352,10 @@ class Array(LazyObjectBase): return tuple(s.value if s.value is not None else s.name for s in self._shape) + @property + def ndim(self): + return len(self._shape) + @property def size(self): return self._shadow_size() @@ -504,36 +497,36 @@ class Array(LazyObjectBase): value=self.value[ind], ) - def _make_default_inames(self): + def _make_default_inames(self, *args): """Make the default inames, simply as lookup expressions. """ - return self._make_axis_indices() + return self._make_axis_indices(*args) - def _make_default_shape(self): + def _make_default_shape(self, ndim): """Make a default shape based of name and ndim """ # zero-dimensional, aka a scalar - if self.ndim == 0: + if ndim == 0: return () # symbolic-dimensions - if isinstance(self.ndim, Unsigned): - assert self.ndim.ndim == 0 + if isinstance(ndim, Unsigned): + assert ndim.ndim == 0 return Lookup(Variable(self.name), "shape") # constant positive dimensions - assert self.ndim > 0 + assert ndim > 0 if len(self.name) > 1 and self.name[:2] == '__': name_prefix = '' else: name_prefix = '__' names = [name_prefix + self.name + '_shape_%d' % d - for d in range(self.ndim)] + for d in range(ndim)] return tuple( Unsigned( name=names[d], expr=var(names[d])) - for d in range(self.ndim)) + for d in range(ndim)) def copy(self, stateless=False): """Returns a new copy of self, where the attributes undergo a shallow @@ -1058,13 +1051,20 @@ class Array(LazyObjectBase): self.value = lp_res[self.name] return self.value - def _make_default_expr(self): + def _make_default_expr(self, *args): """The default expression is the array it self. + + :param ndim: optional, used when the array shape is not set. """ + if hasattr(self, '_shape'): + ndim = self.ndim + else: + ndim, = args + sym_arr = var(self.name) - if self.ndim > 0: + if ndim > 0: # array - sym_indices = self._make_axis_indices() + sym_indices = self._make_axis_indices(*args) expr = Subscript(sym_arr, index=sym_indices) else: # scalar @@ -1417,30 +1417,31 @@ class Array(LazyObjectBase): data_map[k] = v return data_map - def _make_axis_indices(self): + def _make_axis_indices(self, *args): """Make the inames as expressions based of array name and ndim + + :param ndim: optional, used when the array shape is not set yet. """ # zero-dimensional, aka a scalar - if self.ndim == 0: + if hasattr(self, '_shape'): + ndim = self.ndim + else: + ndim, = args + + if ndim == 0: return () # ndim is None (up to further inference) - if self.ndim is None: - # return Lookup(Variable(self.name), "inames") - return var('__%s_inames' % self.name) - - # symbolic-dimensions - if isinstance(self.ndim, Unsigned): - assert self.ndim.ndim == 0 + if ndim is None: # return Lookup(Variable(self.name), "inames") return var('__%s_inames' % self.name) # constant positive dimensions - assert self.ndim > 0 + assert ndim > 0 # shape = Lookup(Variable(self.name), "inames") # return tuple(Subscript(shape, d) for d in range(self.ndim)) return tuple(var('__%s_inames_%d' % (self.name, d)) - for d in range(self.ndim)) + for d in range(ndim)) @staticmethod def _create_indices(shape): diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index bcc7636..11df3fc 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -518,7 +518,6 @@ def reshape(array, newshape, order='C', name=None, inames=None): range(array.ndim + iaxis)) new_arr = array.with_name(name) - new_arr.ndim = new_ndim new_arr.inames = inames new_arr.preconditions = new_precond diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 482ec19..1aeaf7e 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -226,7 +226,6 @@ class BroadcastResult(object): brdc_arr = base_arr.with_name(name) assert isinstance(self.ndim, int) and self.ndim >= 0 - brdc_arr.ndim = self.ndim brdc_arr._shape = lappy_shape # make new inames -- GitLab From ff613270b5ede255d6f52f6280d78bb5ac01c11b Mon Sep 17 00:00:00 2001 From: xywei Date: Thu, 5 Mar 2020 23:17:00 -0600 Subject: [PATCH 17/59] Fix an issue related to boolean values of a lazy array --- lappy/core/array.py | 9 +++++++++ lappy/core/basic_ops.py | 17 +++++++++-------- lappy/core/broadcast.py | 9 +++++++-- lappy/core/ufuncs.py | 4 ++++ test/test_indexing.py | 9 +++++++++ 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 82d192d..2c8df51 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -204,6 +204,15 @@ class LazyObjectBase(object): else: return self.name == other.name + def __bool__(self): + """If then value is not known, the boolean value is always false. + (Think of it as a simplified trinary logic). + """ + if self.value is None: + return False + + return bool(self.value) + @property def value(self): if self.env is None: diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 11df3fc..7cc9ab5 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -24,6 +24,7 @@ from pymbolic import var, evaluate, substitute from pymbolic.primitives import Variable, Product from numpy import AxisError +from lappy.core.ufuncs import minimum, maximum from lappy.core.array import ( Array, Unsigned, to_lappy_shape, to_lappy_unsigned, isscalar, get_internal_name_prefix) @@ -92,7 +93,7 @@ class SubArrayFactory(object): # {{{ constructor - def __init__(self, base, selobj): + def __init__(self, base, selobj=None): """ :param base: the base array. :param selobj: the selection object. @@ -104,6 +105,8 @@ class SubArrayFactory(object): selist = list(selobj) elif isinstance(selobj, (int, Array, list, np.ndarray)): selist = [selobj, ] + elif selobj is None: + selist = [] else: raise TypeError() @@ -172,9 +175,7 @@ class SubArrayFactory(object): :param iaxis: the index of the axis being sliced. """ assert isinstance(slc, slice) - dim = self.base_arr.shape[iaxis] - if isinstance(dim, str): - dim = var(dim) + dim = self.base_arr._shape[iaxis] step_none = slc.step is None if step_none: @@ -195,11 +196,11 @@ class SubArrayFactory(object): stop += dim if step > 0: - start = 0 if start_none else max(0, min(dim, start)) - stop = dim if stop_none else max(start, min(dim, stop)) + start = 0 if start_none else maximum(0, minimum(dim, start)) + stop = dim if stop_none else maximum(start, minimum(dim, stop)) else: - start = dim - 1 if start_none else max(-1, min(dim - 1, start)) - stop = -1 if stop_none else max(-1, min(start, stop)) + start = dim - 1 if start_none else maximum(-1, minimum(dim - 1, start)) + stop = -1 if stop_none else maximum(-1, minimum(start, stop)) return slice(start, stop, step) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 1aeaf7e..1101e62 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -24,7 +24,7 @@ from functools import partial from pymbolic import evaluate, substitute, var from pymbolic.primitives import Product, Expression, Max -from lappy.core.array import to_lappy_shape +from lappy.core.array import to_lappy_shape, to_lappy_array from lappy.core.tools import is_nonnegative_int from lappy.core.preconditions import EvaluationPrecondition @@ -64,13 +64,14 @@ class BroadcastResult(object): NOTE: only fixed (statically known) ndims are supported """ + def __init__(self, array_list): for arr in array_list: if not np.isscalar(arr.ndim): raise ValueError( "cannot broadcast %s with variable ndim" % str(arr)) - self.base_arrays = list(array_list) + self.base_arrays = array_list self.preconditions = [] self._broadcase_base_arrays() self._make_broadcast_arrays() @@ -277,4 +278,8 @@ class BroadcastResult(object): def broadcast(*arrays): """Creates an object that mimics broadcasting. """ + arrays = list(arrays) + for aid in range(len(arrays)): + if np.isscalar(arrays[aid]): + arrays[aid] = to_lappy_array(arrays[aid]) return BroadcastResult(arrays) diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index ad4b6aa..49f8976 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -22,6 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from pymbolic import var +from pymbolic.primitives import Min, Max from lappy.core.array import Array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast @@ -387,4 +388,7 @@ mod = remainder = BinaryOperation(lambda x, y: x % y) # floating point mod, and returns the same sign with x fmod = BinaryOperation(lambda x, y: FloatingPointRemainder(x, y)) +minimum = BinaryOperation(lambda x, y: Min([x, y])) +maximum = BinaryOperation(lambda x, y: Max([x, y])) + # }}} End binary ufuncs diff --git a/test/test_indexing.py b/test/test_indexing.py index 6f085eb..d8867f8 100644 --- a/test/test_indexing.py +++ b/test/test_indexing.py @@ -36,3 +36,12 @@ def test_subarr_out_ndim(): print(subfac.ndim) print(arr.inames) print(arr.shape) + + +def test_slice_completion(): + arr = la.ndarray('A', ndim=2) + subfac = SubArrayFactory(arr) + sl = subfac._complete_slice(slice(None, -10, -1), 0) + + a = sl.stop > 0 + print(a.expr) -- GitLab From 1fb8c64feadffeec5158d3e862ae6b29a6beaedb Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 6 Mar 2020 17:27:37 -0600 Subject: [PATCH 18/59] Conditionals [skip ci] --- lappy/core/basic_ops.py | 26 ++++++++-------- lappy/core/broadcast.py | 4 +-- lappy/core/conditional.py | 62 +++++++++++++++++++++++++++++++++++++++ lappy/core/primitives.py | 1 + lappy/core/ufuncs.py | 2 ++ test/test_indexing.py | 5 ++-- 6 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 lappy/core/conditional.py diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 7cc9ab5..a62f8af 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -25,6 +25,7 @@ from pymbolic.primitives import Variable, Product from numpy import AxisError from lappy.core.ufuncs import minimum, maximum +from lappy.core.conditional import conditional from lappy.core.array import ( Array, Unsigned, to_lappy_shape, to_lappy_unsigned, isscalar, get_internal_name_prefix) @@ -183,24 +184,25 @@ class SubArrayFactory(object): else: step = slc.step + start = slc.start start_none = slc.start is None if not start_none: - start = slc.start - if start < 0: - start += dim + start = conditional(start < 0, start + dim, start) + stop = slc.stop stop_none = slc.stop is None if not stop_none: - stop = slc.stop - if stop < 0: - stop += dim + stop = conditional(stop < 0, stop + dim, stop) - if step > 0: - start = 0 if start_none else maximum(0, minimum(dim, start)) - stop = dim if stop_none else maximum(start, minimum(dim, stop)) - else: - start = dim - 1 if start_none else maximum(-1, minimum(dim - 1, start)) - stop = -1 if stop_none else maximum(-1, minimum(start, stop)) + start_p = 0 if start_none else maximum(0, minimum(dim, start)) + stop_p = dim if stop_none else maximum(start_p, minimum(dim, stop)) + + start_m = dim - 1 if start_none \ + else maximum(-1, minimum(dim - 1, start)) + stop_m = -1 if stop_none else maximum(-1, minimum(start_m, stop)) + + start = conditional(step > 0, start_p, start_m) + stop = conditional(step > 0, stop_p, stop_m) return slice(start, stop, step) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 1101e62..3bdf11d 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -94,13 +94,13 @@ class BroadcastResult(object): else: # same name must have the same runtime values # (may have different expressions) - def check_name_valule_consistency(context, s): + def check_name_value_consistency(context, s): s_val = evaluate(s.expr, context) another_s_val = evaluate( expr_map[var(s.name)], context) return s_val == another_s_val self.preconditions.append(EvaluationPrecondition( - partial(check_name_valule_consistency, s=s))) + partial(check_name_value_consistency, s=s))) # implicitly reshape to the same ndim # (right-aligned) diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py new file mode 100644 index 0000000..e837420 --- /dev/null +++ b/lappy/core/conditional.py @@ -0,0 +1,62 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from pymbolic.primitives import If +from lappy.core.array import Array, to_lappy_array + + +def conditional(cond, con, alt, name=None): + """If-then-else conditional expression for arrays. + + :param cond: The condition. + :param con: The consequent. + :param alt: The alternative. + :param name: (Optional) name of the resulting array. + + Behaves like the ternary operator ?: in C. + """ + if not isinstance(cond, Array): + cond = to_lappy_array(cond) + + if not isinstance(con, Array): + con = to_lappy_array(con) + + if not isinstance(alt, Array): + alt = to_lappy_array(alt) + + # cond, con and alt must have the same shape + assert cond.ndim == con.ndim + assert con.ndim == alt.ndim + + if name is None: + new_arr = cond.copy() + else: + new_arr = cond.with_name(name) + + new_interm = cond.intermediaries.copy() + + new_expr = If(cond.expr, con.expr, alt.expr) + + # FIXME + return new_arr diff --git a/lappy/core/primitives.py b/lappy/core/primitives.py index 403c205..73fc68c 100644 --- a/lappy/core/primitives.py +++ b/lappy/core/primitives.py @@ -26,6 +26,7 @@ from six.moves import intern from pymbolic.primitives import AlgebraicLeaf from pymbolic.primitives import Expression +from pymbolic.primitives import If from pymbolic.primitives import QuotientBase from pymbolic.primitives import Variable diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 49f8976..54fb01b 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -261,6 +261,8 @@ class BinaryOperation(UFunc): new_dtype = self.dtype for ka, va in a.arguments.items(): + if va is None: + continue if ka in b.arguments.keys(): if not va.ndim == b.arguments[ka].ndim: raise ValueError("argument %s has different ndims in %s and %s" diff --git a/test/test_indexing.py b/test/test_indexing.py index d8867f8..879f557 100644 --- a/test/test_indexing.py +++ b/test/test_indexing.py @@ -43,5 +43,6 @@ def test_slice_completion(): subfac = SubArrayFactory(arr) sl = subfac._complete_slice(slice(None, -10, -1), 0) - a = sl.stop > 0 - print(a.expr) + print(repr(arr.size())) + + 1/0 -- GitLab From 6546d7a9f2607eeea6cb8ad2a36a65805e2bd9a6 Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 7 Mar 2020 23:52:38 -0600 Subject: [PATCH 19/59] Add utils for merging states and related housekeeping [skip ci] --- lappy/core/array.py | 10 ++++- lappy/core/conditional.py | 32 ++++++++++++---- lappy/core/tools.py | 81 +++++++++++++++++++++++++++++++++++++-- lappy/core/ufuncs.py | 7 ++-- 4 files changed, 116 insertions(+), 14 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 2c8df51..08b45bc 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -83,7 +83,7 @@ class LazyObjectBase(object): .. attribute:: value - the actual data. None if not yet evaluated + the actual data. None if not yet evaluated. .. attribute:: arguments @@ -213,6 +213,14 @@ class LazyObjectBase(object): return bool(self.value) + @property + def is_stateless(self): + return self.env is None + + @property + def has_state(self): + return self.env is not None + @property def value(self): if self.env is None: diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index e837420..a90bd4e 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -45,18 +45,36 @@ def conditional(cond, con, alt, name=None): if not isinstance(alt, Array): alt = to_lappy_array(alt) - # cond, con and alt must have the same shape - assert cond.ndim == con.ndim + # con and alt must have the same shape assert con.ndim == alt.ndim - if name is None: - new_arr = cond.copy() + # cond can be a scalar or of the same shape as con/alt + if cond.ndim > 0: + assert cond.ndim == con.ndim + + obj = dict() + + # currently does not support dtype expressions + if con.dtype is None: + if alt.dtype is None: + new_dtype = None + else: + new_dtype = alt.dtype else: - new_arr = cond.with_name(name) + assert (alt.dtype is None) or (alt.dtype == con.dtype) + new_dtype = con.dtype + obj['dtype'] = new_dtype + + assert con.dtype == alt.dtype + + obj['ndim'] = con.ndim + obj['name'] = name - new_interm = cond.intermediaries.copy() + obj['intermediaries'] = cond.intermediaries.copy() + obj['intermediaries'].update(con.intermediaries) + obj['intermediaries'].update(alt.intermediaries) new_expr = If(cond.expr, con.expr, alt.expr) # FIXME - return new_arr + return arr_class(**obj) diff --git a/lappy/core/tools.py b/lappy/core/tools.py index 8e685b4..4414a73 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -25,14 +25,17 @@ import numpy as np from lappy.core.array import LazyObjectBase -def check_and_merge_envs(*envs): +def check_and_merge_envs(*arr_list): """Merge captured environments. + Enviroments (captured lists) are dictionaries with names as keys, and + data (numpy arrays, pyopencl arrays etc.) as values. + Raises exception when there are conflicts. """ new_env = dict() - for env in envs: - for key, val in env.items(): + for arr in arr_list: + for key, val in arr.env.items(): if key in new_env: if new_env[key] is val: pass @@ -49,6 +52,78 @@ def check_and_merge_envs(*envs): return new_env +def check_and_merge_args(*arr_list): + """Merge argument lists. + + Argument lists are dictionaries with names as keys, and symbolic object + (Array, Scalar, etc.) as values. Members of the list are all stateless. + + Returns the merged argument list. + Raises exception when there are conflicts + """ + margs = dict() + for arr in arr_list: + for name, arg in arr.arguments.items(): + if name in margs: + arg2 = margs[name] # Arrays fully determined by their names + assert arg2.name == arg.name + else: + if arg is None: + margs[name] = arr.as_stateless() + else: + assert name == arg.name + margs[name] = arg + return margs + + +def check_and_merge_bound_args(*arr_list): + """Merge bound argument lists. + + BALs are dictionaries with names as keys, and symbolic object + (Array, Scalar, etc.) as values. Members of the list are all stateless. + + Returns the merged argument list. + Raises exception when there are conflicts + """ + margs = dict() + for arr in arr_list: + for name, arg in arr.bound_arguments.items(): + if name in margs: + arg2 = margs[name] # Arrays fully determined by their names + assert arg2.name == arg.name + else: + if arg is None: + margs[name] = arr.as_stateless() + else: + assert name == arg.name + margs[name] = arg + return margs + + +def check_and_merge_interms(*arr_list): + """Merge bound intermediary lists. + + ILs are dictionaries with names as keys, and symbolic object + (Array, Scalar, etc.) as values. Members of the list are all stateless. + + Returns the merged argument list. + Raises exception when there are conflicts + """ + margs = dict() + for arr in arr_list: + for name, arg in arr.intermediaries.items(): + if name in margs: + arg2 = margs[name] # Arrays fully determined by their names + assert arg2.name == arg.name + else: + if arg is None: + margs[name] = arr.as_stateless() + else: + assert name == arg.name + margs[name] = arg + return margs + + def is_nonnegative_int(num): if not np.isscalar(num): return False diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 54fb01b..5afa991 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -26,7 +26,8 @@ from pymbolic.primitives import Min, Max from lappy.core.array import Array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast -from lappy.core.tools import check_and_merge_envs +from lappy.core.tools import ( + check_and_merge_envs, check_and_merge_interms) class UFunc(object): @@ -280,8 +281,8 @@ class BinaryOperation(UFunc): new_bound_arglist = a.bound_arguments.copy() new_bound_arglist.update(b.bound_arguments) - new_env = check_and_merge_envs(a.env, b.env) - new_interm = check_and_merge_envs(a.intermediaries, b.intermediaries) + new_env = check_and_merge_envs(a, b) + new_interm = check_and_merge_interms(a, b) if name in new_env: raise ValueError("The name %s is ambiguous" % name) -- GitLab From 3c19842175d16a3656b05fca440683988351c88a Mon Sep 17 00:00:00 2001 From: xywei Date: Sun, 8 Mar 2020 23:16:29 -0500 Subject: [PATCH 20/59] Clean up legacy code, add initial implementation of conditionals --- lappy/core/array.py | 237 ++------------------------------------ lappy/core/conditional.py | 74 +++++++++++- lappy/core/primitives.py | 1 - lappy/core/tools.py | 24 +++- test/test_indexing.py | 5 +- 5 files changed, 98 insertions(+), 243 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 08b45bc..776bd52 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -10,9 +10,6 @@ import pyopencl as cl import numpy as np import loopy as lp -from operator import mul -import functools - import islpy as isl import pymbolic from pymbolic import var, evaluate @@ -75,7 +72,10 @@ class LazyObjectBase(object): the name of the object, should be a Python identifier and do not start with double underscore '__' (which is reserved - for internal use). + for internal use). A lazy object's symbolic information is + uniquely determined by the name, that is, if two lazy objects + share the same name, they are treated as the same computation + (but can have different captured environments). .. attribute:: expr @@ -270,14 +270,14 @@ class Array(LazyObjectBase): .. attribute:: shape - overall shape of array expression. When ndim is given, - the shape itself can be a tuple of :class:`Unsigned`, - otherwise an :class:`Array` with proper assumptions and - integral dtype. + overall shape of array expression. Tuple of integers and strings. + Behind the scene, ``_shape`` stores a tuple of :class:`Unsigned`, + and the ``shape`` data is formed on query to allow shape inference. .. attribute:: inames index names for iterating over the array. + Tuple of :class:`pymbolic.Variable`'s. .. attribute:: domain_expr @@ -1305,63 +1305,6 @@ class Array(LazyObjectBase): # }}} End comparisons - @staticmethod - def _generic_constructor(obj, data=None): - if data is None: - data = obj - shape = list(obj.shape) - name = "__array_" + str(Array._counter) - _, indices = Array._create_indices(shape) - expr = var(name)[tuple(indices)] - data_mapping = {name: data} - shapes = {name: (shape, 0)} - global_index = Array._init_indices(shape, "index") - original_index = {name: indices} - Array._counter += 1 - new_array = {"expr": expr, "data_mapping": data_mapping, - "shape": shape, "shapes": shapes, - "global_index": global_index, - "original_index": original_index} - return new_array - - @staticmethod - def _generate_oned_index(new_shape): - '''Generates a 1D index for the array based off the new shape''' - temp_idx = [] - _, reshape_indices = Array._create_indices(new_shape) - for i in range(len(new_shape) - 1): - idx = reshape_indices[i] * functools.reduce(mul, new_shape[i + 1:], 1) - temp_idx.append(idx) - temp_idx.append(reshape_indices[len(reshape_indices) - 1]) - oned_idx = sum(temp_idx) - return oned_idx - - @staticmethod - def _generate_reshape_index(oned_idx, child_index, self_shape): - '''Generates the indices for the reshaped array using the 1D index''' - diff_shape = len(self_shape) - len(child_index) - if diff_shape < 0: - diff_shape = 0 - new_indices = [] - oned_index = pymbolic.substitute(oned_idx, {}) - for i in range(diff_shape): - block_size = functools.reduce(mul, self_shape[i + 1:], 1) - index = oned_index // block_size - oned_index = oned_index - (index * block_size) - for i in range(len(child_index) - 1): - block_size = functools.reduce(mul, self_shape[i + diff_shape + 1:], 1) - index = oned_index // block_size - if child_index[i] == 0: - new_indices.append(0) - else: - new_indices.append(index) - oned_index = oned_index - (index * block_size) - if child_index[len(child_index) - 1] == 0: - new_indices.append(0) - else: - new_indices.append(oned_index % self_shape[len(self_shape) - 1]) - return new_indices - def _generate_index_domains(self): """Make the loop domain for looping over the array's index space. For this method to work, ndim must be concrete. @@ -1383,57 +1326,6 @@ class Array(LazyObjectBase): & v[inames[i]].lt_set(v[shape_names[i]])) return s.get_basic_sets() - @staticmethod - def _transpose_list_args(indices, args): - '''Transposes the axes of the array based off the given args''' - new_indices = [] - for i in range(len(indices)): - if args[i] >= len(indices): - raise AssertionError("Given argument does not match with shape axes") - new_indices.append(indices[args[i]]) - return new_indices - - @staticmethod - def _add_shapes(obj1, obj2): - '''Adds two shapes of arrays together, following the rules of broadcasting''' - shape = list(obj1) - min_dims = min(len(obj1), len(obj2)) - first_idx = len(obj1) - 1 - second_idx = len(obj2) - 1 - for j in range(min_dims): - # If the dimensions are equal or one of the dimensions is 1, then - # broadcasting is legal. - first_shape_dim = shape[first_idx] - second_shape_dim = obj2[second_idx] - - if ( - first_shape_dim == 1 - or second_shape_dim == 1 - or (first_shape_dim == second_shape_dim)): - if first_idx <= (len(shape) - 1): - shape[first_idx] = max(first_shape_dim, second_shape_dim) - else: - shape.insert(0, max(first_shape_dim, second_shape_dim)) - else: - return None - first_idx -= 1 - second_idx -= 1 - # take care of the rest of the dims once we have filled min_dims - if len(shape) < len(obj2): - for k in range(second_idx, -1, -1): - shape.insert(0, obj2[k]) - return shape - - @staticmethod - def _add_dicts(obj1, obj2): - '''Combines two dictionaries''' - data_map = {} - for k, v in obj1.items(): - data_map[k] = v - for k, v in obj2.items(): - data_map[k] = v - return data_map - def _make_axis_indices(self, *args): """Make the inames as expressions based of array name and ndim @@ -1458,120 +1350,11 @@ class Array(LazyObjectBase): # shape = Lookup(Variable(self.name), "inames") # return tuple(Subscript(shape, d) for d in range(self.ndim)) return tuple(var('__%s_inames_%d' % (self.name, d)) - for d in range(ndim)) - - @staticmethod - def _create_indices(shape): - ''' - Generates standard indices for an array based off the shape. - Note that a 0 is used for axes of shape 1. - Indexing starts from the "back" of the array. - ''' - names = [] - indices = [] - idx = 0 - for i in range(len(shape) - 1, -1, -1): - name = "index_" + str(idx) - if shape[i] == 1: - indices.insert(0, 0) - else: - indices.insert(0, var(name)) - names.insert(0, name) - idx += 1 - return names, indices - - @staticmethod - def _init_indices(set, type): - ''' - Initializes a set of indices for the array, based on the given set and type: - "index" for array expression or "s" for shape indices - i.e. [index_2, index_1, index_0] or [s2, s1, s0] - Note that this function ignores using 0 for axes of shape 1. - ''' - indices = [] - idx = 0 - for i in range(len(set) - 1, -1, -1): - name = type + "_" + str(idx) - indices.insert(0, var(name)) - idx += 1 - return indices - - @staticmethod - def _check_data(data_mapping): - '''Returns True if the array expression has data for all arrays''' - for k, v in data_mapping.items(): - if v is None: - return False - return True - - @staticmethod - def _check_shape(shape): - '''Returns True if the shape has defined values for all indices''' - for i in range(len(shape)): - if not isinstance(shape[i], int): - return False - return True - - @staticmethod - def _update_shape(shapes): - ''' - Updates the shape based off the shapes of each array in the - array expression - ''' - new_shape = [] - for k, v in shapes.items(): - if v is None: - return None - if v[1] == 0: - shape = v[0] - else: - shape = v[1] - new_shape = Array._add_shapes(new_shape, shape) - if new_shape is None: - return None - return new_shape - - @staticmethod - def _transpose_indices(global_index, args): - ''' - Creates a dictionary mapping between the global index and the corresponding - global index in the transposed array - ''' - transpose_dict = {} - for i in range(len(args)): - transpose_dict[global_index[i]] = global_index[args.index(i)] - return transpose_dict - - @staticmethod - def _generate_args(global_index): - ''' - Generates the args representing the permutation of the axes for the - global index. Permutation of axes is defined as any variation from the - standard indices originally assigned - ''' - args = [] - for i in range(len(global_index) - 1, -1, -1): - name = "index_" + str(i) - args.append(global_index.index(var(name))) - return args - - @staticmethod - def _update_zero_indices(old_index, new_shape): - ''' - Generates a dictionary mapping between the old index of the array - and the new shape, using 0's for shapes of 1 - ''' - expr_dict = {} - for i in range(len(old_index)): - if new_shape[i] == 1: - expr_dict[old_index[i]] = 0 - else: - expr_dict[old_index[i]] = old_index[i] - return expr_dict - + for d in range(ndim)) # }}} End array class + ndarray = Array # {{{ special subtypes of array diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index a90bd4e..96193e9 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -22,8 +22,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from pymbolic import var, substitute from pymbolic.primitives import If from lappy.core.array import Array, to_lappy_array +from lappy.core.tools import ( + check_and_merge_precs, check_and_merge_envs, check_and_merge_args, + check_and_merge_bargs, check_and_merge_interms) def conditional(cond, con, alt, name=None): @@ -45,7 +49,7 @@ def conditional(cond, con, alt, name=None): if not isinstance(alt, Array): alt = to_lappy_array(alt) - # con and alt must have the same shape + # con and alt must have the same ndim assert con.ndim == alt.ndim # cond can be a scalar or of the same shape as con/alt @@ -54,6 +58,13 @@ def conditional(cond, con, alt, name=None): obj = dict() + if con.domain_expr == alt.domain_expr: + obj['domain_expr'] = con.domain_expr + else: + # TODO: check/add precondition that although the + # expressions differ, the resulting domains are the same + raise NotImplementedError() + # currently does not support dtype expressions if con.dtype is None: if alt.dtype is None: @@ -66,15 +77,66 @@ def conditional(cond, con, alt, name=None): obj['dtype'] = new_dtype assert con.dtype == alt.dtype + arr_class = Array obj['ndim'] = con.ndim + + if name is None: + name = '__if_(%s)_then_(%s)_else_(%s)' % ( + cond.name, con.name, alt.name) + obj['name'] = name + obj['inames'] = tuple( + var('__(%s)_inames_%d' % (name, d)) + for d in range(obj['ndim'])) + + iname_maps = dict() + for iaxis in range(obj['ndim']): + iname_maps[con.inames[iaxis]] = obj['inames'][iaxis] + iname_maps[alt.inames[iaxis]] = obj['inames'][iaxis] + if cond.ndim > 0: + iname_maps[cond.inames[iaxis]] = obj['inames'][iaxis] + + # replace old inames with the same (new) ones + obj['expr'] = substitute( + If(cond.expr, con.expr, alt.expr), + iname_maps) + + # con and alt must have the same shape, cond can be of the same shape, + # or be a scalar of shape (, ). + if all(s1.name == s2.name for s1, s2 in zip(con._shape, alt._shape)): + if cond.ndim > 0: + if all(s0.name == s1.name + for s0, s1 in zip(cond._shape, con._shape)): + obj['shape'] = cond._shape + else: + # TODO: check/add preconditions as needed + raise NotImplementedError() + else: + obj['shape'] = con._shape + else: + # TODO: check/add preconditions as needed + raise NotImplementedError() + + obj['env'] = check_and_merge_envs(cond, con, alt) + obj['arguments'] = check_and_merge_args(cond, con, alt) + obj['bound_arguments'] = check_and_merge_bargs(cond, con, alt) + obj['preconditions'] = check_and_merge_precs(cond, con, alt) + + obj['intermediaries'] = check_and_merge_interms(cond, con, alt) + obj['intermediaries'][cond.name] = cond.as_stateless() + obj['intermediaries'][con.name] = con.as_stateless() + obj['intermediaries'][alt.name] = alt.as_stateless() - obj['intermediaries'] = cond.intermediaries.copy() - obj['intermediaries'].update(con.intermediaries) - obj['intermediaries'].update(alt.intermediaries) + obj['is_integral'] = con.is_integral & alt.is_integral - new_expr = If(cond.expr, con.expr, alt.expr) + if not obj['is_integral']: + obj['assumptions'] = None + else: + if con.assumptions is None and alt.assumptions is None: + obj['assumptions'] = None + else: + # FIXME: compute the intersection of assumptions of con and alt + raise NotImplementedError() - # FIXME return arr_class(**obj) diff --git a/lappy/core/primitives.py b/lappy/core/primitives.py index 73fc68c..403c205 100644 --- a/lappy/core/primitives.py +++ b/lappy/core/primitives.py @@ -26,7 +26,6 @@ from six.moves import intern from pymbolic.primitives import AlgebraicLeaf from pymbolic.primitives import Expression -from pymbolic.primitives import If from pymbolic.primitives import QuotientBase from pymbolic.primitives import Variable diff --git a/lappy/core/tools.py b/lappy/core/tools.py index 4414a73..481404d 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -65,7 +65,7 @@ def check_and_merge_args(*arr_list): for arr in arr_list: for name, arg in arr.arguments.items(): if name in margs: - arg2 = margs[name] # Arrays fully determined by their names + arg2 = margs[name] assert arg2.name == arg.name else: if arg is None: @@ -76,7 +76,7 @@ def check_and_merge_args(*arr_list): return margs -def check_and_merge_bound_args(*arr_list): +def check_and_merge_bargs(*arr_list): """Merge bound argument lists. BALs are dictionaries with names as keys, and symbolic object @@ -89,7 +89,7 @@ def check_and_merge_bound_args(*arr_list): for arr in arr_list: for name, arg in arr.bound_arguments.items(): if name in margs: - arg2 = margs[name] # Arrays fully determined by their names + arg2 = margs[name] assert arg2.name == arg.name else: if arg is None: @@ -113,17 +113,31 @@ def check_and_merge_interms(*arr_list): for arr in arr_list: for name, arg in arr.intermediaries.items(): if name in margs: - arg2 = margs[name] # Arrays fully determined by their names + arg2 = margs[name] assert arg2.name == arg.name else: if arg is None: - margs[name] = arr.as_stateless() + raise ValueError("self reference in the intermediary list") else: assert name == arg.name margs[name] = arg return margs +def check_and_merge_precs(*arr_list): + """Merge precondition lists. + + PLs are lists of :class:`EvaluationPrecondition` objects. + + Returns the merged argument list. + """ + mprecs = list() + for arr in arr_list: + mprecs.extend(arr.preconditions) + + return mprecs + + def is_nonnegative_int(num): if not np.isscalar(num): return False diff --git a/test/test_indexing.py b/test/test_indexing.py index 879f557..6cdd8b4 100644 --- a/test/test_indexing.py +++ b/test/test_indexing.py @@ -42,7 +42,4 @@ def test_slice_completion(): arr = la.ndarray('A', ndim=2) subfac = SubArrayFactory(arr) sl = subfac._complete_slice(slice(None, -10, -1), 0) - - print(repr(arr.size())) - - 1/0 + assert sl.step == -1 -- GitLab From 6c10f9368c99c2f5caaa258ae49a5da58da291f4 Mon Sep 17 00:00:00 2001 From: xywei Date: Sun, 8 Mar 2020 23:25:07 -0500 Subject: [PATCH 21/59] Rename intermediaries --> temporaries, closes #12 --- lappy/core/array.py | 22 +++++++++++----------- lappy/core/basic_ops.py | 6 +++--- lappy/core/conditional.py | 10 +++++----- lappy/core/tools.py | 8 ++++---- lappy/core/ufuncs.py | 10 +++++----- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 776bd52..5415dca 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -100,7 +100,7 @@ class LazyObjectBase(object): array). The data is looked up in the captured environment by its name (bound arguments are also stateless). - .. attribute:: intermediaries + .. attribute:: temporaries (conceptual) intermediate lazy objects (objects produced in the process of making the array expression). Those objects should be stored @@ -129,7 +129,7 @@ class LazyObjectBase(object): self.name = kwargs.pop("name", None) self.expr = kwargs.pop("expr") self.bound_arguments = kwargs.pop("bound_arguments", dict()) - self.intermediaries = kwargs.pop("intermediaries", dict()) + self.temporaries = kwargs.pop("temporaries", dict()) self.env = kwargs.pop("env", default_env()) self.preconditions = kwargs.pop("preconditions", list()) @@ -240,7 +240,7 @@ class LazyObjectBase(object): 'expr': self.expr, 'arguments': self.arguments, 'bound_arguments': self.bound_arguments, - 'intermediaries': self.intermediaries, + 'temporaries': self.temporaries, 'env': self.env} # }}} End base lazy obj class @@ -567,7 +567,7 @@ class Array(LazyObjectBase): domain_expr=self.domain_expr, arguments=self.arguments.copy(), bound_arguments=self.bound_arguments.copy(), - intermediaries=self.intermediaries.copy(), + temporaries=self.temporaries.copy(), env=env, preconditions=list(self.preconditions), assumptions=cp_assumptions, @@ -591,8 +591,8 @@ class Array(LazyObjectBase): raise ValueError("Name %s is already taken by an argument" % name) elif name in self.bound_arguments: raise ValueError("Name %s is already taken by a (bound) argument" % name) - elif name in self.intermediaries: - raise ValueError("Name %s is already taken by an intermediary" % name) + elif name in self.temporaries: + raise ValueError("Name %s is already taken by a temporary" % name) if name in self.env: raise ValueError( @@ -648,7 +648,7 @@ class Array(LazyObjectBase): - self.inames - self.arguments - self.bound_arguments - - self.intermediaries + - self.temporaries - self.env """ return False @@ -759,7 +759,7 @@ class Array(LazyObjectBase): delta_dict[s.name] = (s.value, s_val) s.value = s_val - # iterate through arguments and intermediaries and + # iterate through arguments and temporaries and # propagate the assignments to all symbols of the same name # # (they may not refer to the same object, but may have the same name) @@ -776,7 +776,7 @@ class Array(LazyObjectBase): # same Unsigned obj assert sym_s.value == delta_dict[sym_s.name][1] - for imd in new_arr.intermediaries.values(): + for imd in new_arr.temporaries.values(): for sym_s in imd._shape: if sym_s.name in delta_dict: if sym_s.value == delta_dict[sym_s.name][0]: # old value @@ -875,7 +875,7 @@ class Array(LazyObjectBase): if s.value is not None: data_map[s.name] = s.value for arg in chain(self.arguments.values(), - self.bound_arguments.values(), self.intermediaries.values()): + self.bound_arguments.values(), self.temporaries.values()): if arg is None: continue # skip self assert isinstance(arg, Array) @@ -901,7 +901,7 @@ class Array(LazyObjectBase): if shape_val.value is None: shapeval_expansion_list.append(shape_val) data_map[argname] = shape_val.value - for arr_imd in self.intermediaries.values(): + for arr_imd in self.temporaries.values(): if argname in arr_imd._shape_names(): shape_val = arr_imd._shape[ arr_imd._shape_names().index(argname)] diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index a62f8af..f00ca76 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -248,7 +248,7 @@ class SubArrayFactory(object): new_name = self.base_arr._make_default_name() - new_interm = self.base_arr.intermediaries.copy() + new_interm = self.base_arr.temporaries.copy() new_interm[self.base_arr.name] = self.base_arr.as_stateless() new_expr = rhs_index_exprs # TODO @@ -263,7 +263,7 @@ class SubArrayFactory(object): 'bound_arguments': self.base_arr.bound_arguments.copy(), 'env': self.base_arr.env.copy(), 'preconditions': self.base_arr.preconditions, - 'intermediaries': new_interm, + 'temporaries': new_interm, 'ndim': self.ndim, 'shape': new_shape, 'dtype': self.base_arr.dtype, @@ -369,7 +369,7 @@ class SubArrayFactory(object): sub_arr.expr = new_expr # add base array as an intermediate object - sub_arr.intermediaries[self.base_arr.name] = \ + sub_arr.temporaries[self.base_arr.name] = \ self.base_arr.as_stateless() raise NotImplementedError() diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index 96193e9..064a3c0 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -27,7 +27,7 @@ from pymbolic.primitives import If from lappy.core.array import Array, to_lappy_array from lappy.core.tools import ( check_and_merge_precs, check_and_merge_envs, check_and_merge_args, - check_and_merge_bargs, check_and_merge_interms) + check_and_merge_bargs, check_and_merge_temps) def conditional(cond, con, alt, name=None): @@ -123,10 +123,10 @@ def conditional(cond, con, alt, name=None): obj['bound_arguments'] = check_and_merge_bargs(cond, con, alt) obj['preconditions'] = check_and_merge_precs(cond, con, alt) - obj['intermediaries'] = check_and_merge_interms(cond, con, alt) - obj['intermediaries'][cond.name] = cond.as_stateless() - obj['intermediaries'][con.name] = con.as_stateless() - obj['intermediaries'][alt.name] = alt.as_stateless() + obj['temporaries'] = check_and_merge_temps(cond, con, alt) + obj['temporaries'][cond.name] = cond.as_stateless() + obj['temporaries'][con.name] = con.as_stateless() + obj['temporaries'][alt.name] = alt.as_stateless() obj['is_integral'] = con.is_integral & alt.is_integral diff --git a/lappy/core/tools.py b/lappy/core/tools.py index 481404d..5368a0c 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -100,8 +100,8 @@ def check_and_merge_bargs(*arr_list): return margs -def check_and_merge_interms(*arr_list): - """Merge bound intermediary lists. +def check_and_merge_temps(*arr_list): + """Merge bound temporaries lists. ILs are dictionaries with names as keys, and symbolic object (Array, Scalar, etc.) as values. Members of the list are all stateless. @@ -111,13 +111,13 @@ def check_and_merge_interms(*arr_list): """ margs = dict() for arr in arr_list: - for name, arg in arr.intermediaries.items(): + for name, arg in arr.temporaries.items(): if name in margs: arg2 = margs[name] assert arg2.name == arg.name else: if arg is None: - raise ValueError("self reference in the intermediary list") + raise ValueError("self reference in the temporaries list") else: assert name == arg.name margs[name] = arg diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 5afa991..251c788 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -27,7 +27,7 @@ from lappy.core.array import Array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast from lappy.core.tools import ( - check_and_merge_envs, check_and_merge_interms) + check_and_merge_envs, check_and_merge_temps) class UFunc(object): @@ -114,7 +114,7 @@ class UnaryOperation(UFunc): else: new_dtype = self.dtype - new_interm = a.intermediaries.copy() + new_interm = a.temporaries.copy() if name in new_interm: raise ValueError("The name %s is ambiguous" % name) new_interm[a.name] = a.as_stateless() @@ -128,7 +128,7 @@ class UnaryOperation(UFunc): 'bound_arguments': a.bound_arguments.copy(), 'env': a.env.copy(), 'preconditions': list(a.preconditions), - 'intermediaries': new_interm, + 'temporaries': new_interm, 'ndim': a.ndim, 'shape': a.shape, 'dtype': new_dtype, 'is_integral': a.is_integral and self.preserve_integral, @@ -282,7 +282,7 @@ class BinaryOperation(UFunc): new_bound_arglist.update(b.bound_arguments) new_env = check_and_merge_envs(a, b) - new_interm = check_and_merge_interms(a, b) + new_interm = check_and_merge_temps(a, b) if name in new_env: raise ValueError("The name %s is ambiguous" % name) @@ -305,7 +305,7 @@ class BinaryOperation(UFunc): 'domain_expr': a.domain_expr, 'arguments': new_arglist, 'bound_arguments': new_bound_arglist, - 'intermediaries': new_interm, + 'temporaries': new_interm, 'env': new_env, 'preconditions': a.preconditions + b.preconditions + bres.preconditions, 'ndim': bres.ndim, 'shape': bres._shape_exprs, -- GitLab From b471128fb0134e8826a9112c41a619e861dc79d2 Mon Sep 17 00:00:00 2001 From: xywei Date: Sun, 8 Mar 2020 23:26:28 -0500 Subject: [PATCH 22/59] Remove more symbolic ndim bits #10 --- lappy/core/broadcast.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 3bdf11d..0fa6926 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -66,11 +66,6 @@ class BroadcastResult(object): """ def __init__(self, array_list): - for arr in array_list: - if not np.isscalar(arr.ndim): - raise ValueError( - "cannot broadcast %s with variable ndim" - % str(arr)) self.base_arrays = array_list self.preconditions = [] self._broadcase_base_arrays() -- GitLab From d991188caef48d06881d81e68fd20fb84a805208 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 11:41:41 -0500 Subject: [PATCH 23/59] Clean up ufunc api and remove legacy code --- .gitignore | 2 + lappy/core/array.py | 1087 ++++++++++++++++++++-------------------- lappy/lib/__init__.py | 23 + test/test_broadcast.py | 2 +- 4 files changed, 565 insertions(+), 549 deletions(-) create mode 100644 lappy/lib/__init__.py diff --git a/.gitignore b/.gitignore index 684d34f..6c1ff43 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *~ __pycache__ *.egg-info + +!lappy/lib diff --git a/lappy/core/array.py b/lappy/core/array.py index 5415dca..5d52fd3 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -14,7 +14,7 @@ import islpy as isl import pymbolic from pymbolic import var, evaluate from pymbolic.primitives import ( - Expression, Substitution, Lookup, Subscript, Variable, Product) + Expression, Lookup, Subscript, Variable, Product) from pymbolic.mapper.evaluator import UnknownVariableError from pprint import pformat @@ -349,201 +349,7 @@ class Array(LazyObjectBase): # }}} End constructor - def _to_repr_dict(self): - repr_dict = super(Array, self)._to_repr_dict() - repr_dict.update({ - 'ndim': self.ndim, - 'inames': self.inames, - 'domain': self.domain_expr, - 'shape': self.shape, - 'dtype': self.dtype, - 'is_integral': self.is_integral, - 'assumptions': self.assumptions}) - return repr_dict - - @property - def shape(self): - """Returns the shape as a tuple of ints if the value is known, or - strings of names otherwise. - """ - return tuple(s.value if s.value is not None else s.name - for s in self._shape) - - @property - def ndim(self): - return len(self._shape) - - @property - def size(self): - return self._shadow_size() - - def _shadow_size(self, axes=None): - """When axes is None, computes the full size. Otherwise computes - the size of the projected array on the given axes. - """ - if axes is None: - axes = list(range(self.ndim)) - else: - # so that generators can be understood - axes = list(axes) - shape_vars = tuple( - var(self._shape[ax].name) if isinstance(self._shape[ax], Array) - else str(self._shape[ax]) - for ax in axes) - size_expr = Product(shape_vars) - shape_ctx = {s.name: s.value for s in self._shape - if isinstance(s, Array) and s.value is not None} - try: - return evaluate(size_expr, shape_ctx) - except UnknownVariableError: - return size_expr - - @property - def is_leaf(self): - """Return True if the array expression is a flat-out read. - """ - return self.expr == self._make_default_expr() - - def reshape(self, newshape, order='C', name=None, inames=None): - """Reshape. - """ - from lappy.core.basic_ops import reshape - return reshape( - self, newshape=newshape, - order=order, name=name, inames=inames) - - def transpose(self, axes=None, name=None): - """Transpose. - """ - from lappy.core.basic_ops import transpose - return transpose(self, axes, name) - - T = transpose - - def __getitem__(self, indices): - """Returns a new array by sub-indexing the current array. - - :param idx: a tuple of indices or slices. - """ - if not isinstance(indices, tuple): - # masking - assert isinstance(indices, Array) - assert indices.ndim == self.ndim - from lappy.core.basic_ops import MaskedArrayFactory - arr_fac = MaskedArrayFactory(self, indices) - else: - # indexing / mixture of indexing and masking - assert isinstance(indices, tuple) - # FIXME only if no mixed use of indexing and masking - from lappy.core.basic_ops import SubArrayFactory - arr_fac = SubArrayFactory(self, indices) - - sub_arr = arr_fac() - - return sub_arr - - def __iter__(self): - """Iterator support if ndim and shape are all fixed. - Its behavior mimics numpy.ndarray.__iter__() - """ - if isinstance(self.ndim, Array): - raise ValueError("Cannot iterate an array with vaiable ndim: %s)" - % str(self.ndim)) - if isinstance(self.shape, Array): - raise ValueError("Cannot iterate an array with vaiable shape: %s" - % str(self.shape)) - if isinstance(self.shape[0], Array): - raise ValueError("Cannot iterate an array with vaiable shape: %s " - "(where the first component is a variable)" - % str(self.shape)) - - sym_indices = self._make_axis_indices() - for idx in range(self.shape[0]): - if self.assumptions is None: - cp_assumptions = None - else: - cp_assumptions = self.assumptions.copy() - # FIXME: project assumptions & arguments - yield self.__class__( - name=self.name + str(list([idx])), - ndim=self.ndim - 1, - shape=self.shape[1:], dtype=self.dtype, - expr=Substitution(self.expr, sym_indices[0], idx), - is_integral=self.is_integral, - arguments=self.arguments.copy(), - bound_arguments=self.bound_arguments.copy(), - env=self.env.copy(), - preconditions=list(self.preconditions), - assumptions=cp_assumptions, - value=self.value[idx], - ) - - def nditer(self): - """Flat iterator support if ndim and shape are all fixed. - Its behavior mimics numpy.ndarray.nditer() - """ - if isinstance(self.ndim, Array): - raise ValueError("Cannot iterate an array with vaiable ndim: %s)" - % str(self.ndim)) - if isinstance(self.shape, Array): - raise ValueError("Cannot iterate an array with vaiable shape: %s" - % str(self.shape)) - if any([isinstance(lenaxis, Array) for lenaxis in self.shape]): - raise ValueError("Cannot iterate an array with vaiable shape: %s " - "(contains variable components)" - % str(self.shape)) - - from pytools import indices_in_shape - sym_indices = self._make_axis_indices() - for ind in indices_in_shape(self.shape): - if self.assumptions is None: - cp_assumptions = None - else: - cp_assumptions = self.assumptions.copy() - # FIXME: project assumptions & arguments - yield self.__class__( - name=self.name + str(ind), - ndim=0, shape=(), dtype=self.dtype, - expr=Substitution(self.expr, sym_indices, ind), - is_integral=self.is_integral, - arguments=self.arguments.copy(), - bound_arguments=self.bound_arguments.copy(), - env=self.env.copy(), - preconditions=list(self.preconditions), - assumptions=cp_assumptions, - value=self.value[ind], - ) - - def _make_default_inames(self, *args): - """Make the default inames, simply as lookup expressions. - """ - return self._make_axis_indices(*args) - - def _make_default_shape(self, ndim): - """Make a default shape based of name and ndim - """ - # zero-dimensional, aka a scalar - if ndim == 0: - return () - - # symbolic-dimensions - if isinstance(ndim, Unsigned): - assert ndim.ndim == 0 - return Lookup(Variable(self.name), "shape") - - # constant positive dimensions - assert ndim > 0 - if len(self.name) > 1 and self.name[:2] == '__': - name_prefix = '' - else: - name_prefix = '__' - names = [name_prefix + self.name + '_shape_%d' % d - for d in range(ndim)] - return tuple( - Unsigned( - name=names[d], - expr=var(names[d])) - for d in range(ndim)) + # {{{ copy constructors, with_xxx(), as_xxx() def copy(self, stateless=False): """Returns a new copy of self, where the attributes undergo a shallow @@ -630,7 +436,7 @@ class Array(LazyObjectBase): assert len(inames) == len(self.inames) for name in inames: - if self.has_name(name): + if self._has_name(name): raise ValueError("name %s is already taken") iname_map = { @@ -641,109 +447,37 @@ class Array(LazyObjectBase): new_arr.expr = pymbolic.substitute(self.expr, iname_map) return new_arr - def has_name(self, name): - """Check whether a given name is taken by one of the following: - - - self.name - - self.inames - - self.arguments - - self.bound_arguments - - self.temporaries - - self.env - """ - return False - - def _shape_names(self): - """Shape information, stringified to be legal names. - """ - for s in self._shape: - assert isinstance(s, Unsigned) - return tuple(s.name for s in self._shape) - - @property - def _shape_str(self): - """A shape description string used for loopy kernel construction. - """ - shape_names = self._shape_names() - return ', '.join(shape_names) - - @property - def _shape_expr(self): - """Values for the shape (if not None), otherwise expressions. - """ - return tuple( - s.value if s.value is not None else s.expr - for s in self._shape) - - def check_preconditions(self, context): - """Call checkers in the list of preconditions. - - :param context: a dict which is passed to all checkers. - """ - failed_checks = [] - for checker in self.preconditions: - try: - res = checker(context) - if res is not None: - if isinstance(res, bool): - if res: - pass - else: - raise PreconditionNotMetError( - "precondition checker failed: %s" % str(checker)) - else: - raise ValueError( - "cannot understand the return value " - "of the precondition checker %s" % str(checker)) - except Exception as e: # noqa: W0703 - print(e) - failed_checks.append(checker) - - err_msgs = [] - for fc in failed_checks: - msg = ("Precondition %s not met, which was imposed at\n\n" - % str(checker)) - if hasattr(fc, "frame"): - msg = msg + '\n'.join(format_list(extract_stack( - fc.frame))) - err_msgs.append(msg) - if len(failed_checks) > 0: - precon_err_msg = ( - "%d out of %d preconditions are not met" - % (len(failed_checks), len(self.preconditions))) - for im, msg in enumerate(err_msgs): - precon_err_msg = (precon_err_msg + '\n\n' - + '[%d / %d]: ' % (im + 1, len(failed_checks)) + msg) - raise PreconditionNotMetError(precon_err_msg) - def with_dtype(self, dtype, new_name=None): """Returns a copy of self with the specified dtype. """ raise NotImplementedError() - def with_shape_data(self, shape_data, new_name=None): - """Returns a copy of the array with concrete shape. Assuming concrete ndim. - ndim is unchanged by assigning the shape data, meaning that the inputs, when - not None, must be positive. - - :param shape_data: a tuple of shape data. Use None to skip setting certain - axes. - - NOTE: if the shape has nontrivial expression (not just a reference to a - name), lappy will not solve the equation for the "independent variables". - The behavior in such cases is undefined. + def with_shape_data(self, shape_data, + new_name=None, allow_complex_expr=False): + """Returns a copy of the array with concrete shape. ``ndim`` is + unchanged by assigning the shape data, meaning that the inputs, + when not None, must be positive. + + :param shape_data: a tuple of shape data. Use None to skip setting + certain axes. + + NOTE: if the shape has nontrivial expression (not just a reference + to a name), lappy will not solve the equation for the "independent + variables". The behavior in such cases is undefined. For example, if + the shape has an expression ``(n * m) % p``, this method will assign + a value for the whole expression, and the contraint among ``m, n, p`` + given by this expression is ignored and may cause unpredictable + issues. """ - if not isinstance(shape_data, (list, tuple)): - raise ValueError() - if self.ndim != len(shape_data): - raise ValueError() + assert isinstance(shape_data, (list, tuple)) + assert self.ndim == len(shape_data) - for s in self._shape: - if False: # TODO: test for nontrivial exprs - warnings.warn( - "the shape variable %s of %s has nontrivial expression, " - " and setting its value may yield undefined behavior" - % (s.name, self.name)) + def is_trivial(expr): + # TODO: better ways of spotting nontrivial exprs? + return expr.__class__.init_arg_names == ('name',) + + assert allow_complex_expr or all( + is_trivial(s.expr) for s in self._shape) if new_name is None: new_name = 'bound%d_' % self.__class__._counter + self.name @@ -801,7 +535,8 @@ class Array(LazyObjectBase): if key in self.bound_arguments.keys(): # bounding cannot be undone # (since un-inference is not possible) - raise ValueError("argument named %s is already bound" % key) + raise ValueError("argument named %s is already bound" + % key) raise ValueError("argument named %s is not accepted" % key) @@ -832,274 +567,87 @@ class Array(LazyObjectBase): # rename value of self new_arr.env[new_name] = new_arr.env.pop(self.name, None) - return new_arr - def get_data_mapping(self, knl=None): - """Make a data mapping using known data, tailored for giving inputs to - the loopy kernel. Returns all known information if knl is None. + # }}} End copy constructors, with_xxx(), as_xxx() - :param knl: the loopy kernel + # {{{ public (numpy-compliant) api + + @property + def shape(self): + """Returns the shape as a tuple of ints if the value is known, or + strings of names otherwise. """ - data_map = {} - shapeval_expansion_list = [] + return tuple(s.value if s.value is not None else s.name + for s in self._shape) - # gather captured data - for arr_name, varr in self.env.items(): - if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: - continue + @property + def ndim(self): + return len(self._shape) - if arr_name == self.name: - # only specify output shape, and let the backend to do the malloc - for out_s in self._shape: - if out_s.value is None: - shapeval_expansion_list.append(out_s) - data_map[out_s.name] = out_s.value + @property + def size(self): + return self._shadow_size() - if isinstance(varr, Array) and varr.value is not None: - data_map[arr_name] = varr.value - elif isinstance(varr, np.ndarray): - data_map[arr_name] = varr - elif varr is None: - pass # self - else: - raise RuntimeError("unrecogonized captured variable %s" % arr_name) + def reshape(self, newshape, order='C', name=None, inames=None): + """Reshape. + """ + from lappy.core.basic_ops import reshape + return reshape( + self, newshape=newshape, + order=order, name=name, inames=inames) - # try to get as much extra data that loopy wants as possible - # also evaluates the shape expressions - if knl is None: - # gather all known shapes - for s in self._shape: - if s.name in data_map: - continue - if s.value is not None: - data_map[s.name] = s.value - for arg in chain(self.arguments.values(), - self.bound_arguments.values(), self.temporaries.values()): - if arg is None: - continue # skip self - assert isinstance(arg, Array) - for s in arg._shape: - if s.value is None: - shapeval_expansion_list.append(s) - data_map[s.name] = s.value - else: - for argname, arg in knl.arg_dict.items(): - if argname in data_map: - continue - if argname in self._shape_names(): - shape_val = self._shape[self._shape_names().index(argname)] - if shape_val.value is None: - shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value - for arr_arg in self.bound_arguments.values(): - if arr_arg is None: - continue - if argname in arr_arg._shape_names(): - shape_val = arr_arg._shape[ - arr_arg._shape_names().index(argname)] - if shape_val.value is None: - shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value - for arr_imd in self.temporaries.values(): - if argname in arr_imd._shape_names(): - shape_val = arr_imd._shape[ - arr_imd._shape_names().index(argname)] - if shape_val.value is None: - shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value - # no need to search arguments - assert len(self.arguments) == 0 - - # evaluate shape expressions - for se in shapeval_expansion_list: - try: - seval = evaluate(se.expr, data_map) - except UnknownVariableError: - warnings.warn( - "cannot get value for %s prior to calling the loopy kernel" - % se.name) - seval = None - data_map[se.name] = seval - - # purge None-valued entries - entries_to_purge = [] - for key, val in data_map.items(): - if val is None: - entries_to_purge.append(key) - for key in entries_to_purge: - data_map.pop(key) - return data_map - - def eval(self, shape_dtype=np.int32): - """Evaluates the array expression after binding all the arguments. - Note that this function has side effects: it sets self.value to the - evaluation result. If the results is available, it is returned without - re-computing. + def transpose(self, axes=None, name=None): + """Transpose. """ - if self.value is not None: - logging.info("cache hit when evaluating %s" % self.name) - return self.value - - if len(self.arguments) > 0: - raise ValueError("cannot evaluate a partially-bound expression " - "(missing data for %s)" % ', '.join(self.arguments.keys())) - - # Step 1: - # identify codegen statements (loopy assignments) - # and raise exception if ther are multiple statements - # FIXME - n_insns = 1 - if n_insns > 1: - raise TypeError("the array %s cannot be evaluated without temporaries " - "(use the compiler instead) " % self.name) - - # Step 2: - # make the initial loop domain by multiplying the index space of - # the array with hidden axes (e.g. in the case of inner products) - pass - - # Step 3: - # make the conditional expression that accounts for - # - # - sliced LHS A[1:n-1:2, :] = ... - # - # - conditional assignment A[A>0] = ... - # - # Note that this is done on the RHS, e.g. - # A[i, j] = if((i >= 1) and (i < n - 1) and ((i - 1) % 2 == 0), ..., ...) - # A[i, j] = if(A[i, j] > 0, ..., ...) - # FIXME - rhs_expr = self.expr - - # Step 4 - # Account for fancy indexing on the LHS A[B] = ... - # by adding a separate loop domain over the index space of B - # and making corresponding LHS expressions - # FIXME - lhs_expr = var(self.name)[self.inames] - loop_domain = self._generate_index_domains() - - # Step 5 - # Make and run loopy kernel - # - # FIXME: collect all assumptions and pass to loopy - # - kernel_args = [] - - # array args - for arr_name, varr in self.env.items(): - if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: - continue - if arr_name == self.name: - continue # handle self later - elif arr_name in self.arguments.keys(): - if self.arguments[arr_name] is None: - pass # self - else: - assert isinstance(self.arguments[arr_name], Array) - kernel_args.append(lp.GlobalArg(arr_name, - shape=self.arguments[arr_name]._shape_str)) - elif arr_name in self.bound_arguments.keys(): - if self.bound_arguments[arr_name] is None: - pass # self - else: - assert isinstance(self.bound_arguments[arr_name], Array) - kernel_args.append(lp.GlobalArg(arr_name, - shape=self.bound_arguments[arr_name]._shape_str)) - else: - raise RuntimeError("cannot find shape information of %s" % arr_name) - - # output array args - kernel_args.append(lp.GlobalArg(self.name, shape=self._shape_str)) - - # let loopy do the inference - kernel_args.append('...') - - knl = lp.make_kernel( - loop_domain, - [ - lp.Assignment(lhs_expr, rhs_expr) - ], - kernel_args, - lang_version=(2018, 2)) - - knl = lp.set_options(knl, return_dict=True) - - # help with dtype inference - extra_dtype_dict = {} - for argname, arg in knl.arg_dict.items(): - if arg.dtype is None: - # array args - if argname == self.name: - if self.dtype is not None: - extra_dtype_dict[argname] = self.dtype - continue - - if argname in self.arguments: - if self.arguments[argname].dtype is not None: - extra_dtype_dict[argname] = self.arguments[argname].dtype - continue - elif argname in self.bound_arguments: - if self.bound_arguments[argname].dtype is not None: - extra_dtype_dict[argname] = \ - self.bound_arguments[argname].dtype - continue - - # shape args - if argname in self._shape_names(): - extra_dtype_dict[argname] = shape_dtype - for arr_arg in self.bound_arguments.values(): - if arr_arg is None: - continue - if argname in arr_arg._shape_names(): - extra_dtype_dict[argname] = shape_dtype - - knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) - data_map = self.get_data_mapping(knl) - - # TODO: to test everything that the kernel sees, - # let loopy infer more context first (e.g. shape of input arrays) - self.check_preconditions(context=data_map) - - queue = cl.CommandQueue(self.env['cl_ctx']) - evt, lp_res = knl(queue, **data_map) + from lappy.core.basic_ops import transpose + return transpose(self, axes, name) - self.value = lp_res[self.name] - return self.value + T = transpose - def _make_default_expr(self, *args): - """The default expression is the array it self. + def __getitem__(self, indices): + """Returns a new array by sub-indexing the current array. - :param ndim: optional, used when the array shape is not set. + :param idx: a tuple of indices or slices. """ - if hasattr(self, '_shape'): - ndim = self.ndim + if not isinstance(indices, tuple): + # masking + assert isinstance(indices, Array) + assert indices.ndim == self.ndim + from lappy.core.basic_ops import MaskedArrayFactory + arr_fac = MaskedArrayFactory(self, indices) else: - ndim, = args + # indexing / mixture of indexing and masking + assert isinstance(indices, tuple) + # FIXME only if no mixed use of indexing and masking + from lappy.core.basic_ops import SubArrayFactory + arr_fac = SubArrayFactory(self, indices) - sym_arr = var(self.name) - if ndim > 0: - # array - sym_indices = self._make_axis_indices(*args) - expr = Subscript(sym_arr, index=sym_indices) - else: - # scalar - expr = sym_arr - return expr + sub_arr = arr_fac() + return sub_arr - def _set_shape_values(self, shape): - """Instantiate shape with concrete values. + def __iter__(self): + """Iterator support if ndim and shape are all fixed. + Its behavior mimics numpy.ndarray.__iter__() + """ + raise NotImplementedError() - This method should not be called from a user. Users should use - Array.with_shape_data() instead. + def nditer(self): + """Flat iterator support if ndim and shape are all fixed. + Its behavior mimics numpy.ndarray.nditer() """ - for s in tuple(shape): - assert int(s) == s - assert s >= 0 - for s, sym_s in zip(shape, self._shape): - assert isinstance(sym_s, Unsigned) - sym_s.value = s + raise NotImplementedError() + + def __array__(self): + """(For numpy’s dispatch). When converting to a numpy array with + ``numpy.array`` or ``numpy.asarray``, the lazy array is evaluated + and the result is returned. + """ + res = self.eval() + if isinstance(res, np.ndarray): + return res + else: + return res.get() # {{{ unary operations @@ -1246,6 +794,10 @@ class Array(LazyObjectBase): # {{{ comparisons def __eq__(self, other): + """Test for equality. Returns ``True`` if two Arrays have + the same name. Returns a new Array with comparison expression + otherwise. + """ if not isinstance(other, Array): warnings.warn( "Implicit conversion of %s to Lappy array" @@ -1253,10 +805,14 @@ class Array(LazyObjectBase): other_arr = to_lappy_array(other) else: other_arr = other - from lappy.core.ufuncs import equal - return equal(self, other_arr) - def __ne__(self, other): + if self.name == other_arr.name: + return True + else: + from lappy.core.ufuncs import equal + return equal(self, other_arr) + + def __ne__(self, other): return not self == other def __le__(self, other): @@ -1305,6 +861,30 @@ class Array(LazyObjectBase): # }}} End comparisons + # }}} End public (numpy-compliant) api + + # {{{ private api + + def _make_default_expr(self, *args): + """The default expression is the array it self. + + :param ndim: optional, used when the array shape is not set. + """ + if hasattr(self, '_shape'): + ndim = self.ndim + else: + ndim, = args + + sym_arr = var(self.name) + if ndim > 0: + # array + sym_indices = self._make_axis_indices(*args) + expr = Subscript(sym_arr, index=sym_indices) + else: + # scalar + expr = sym_arr + return expr + def _generate_index_domains(self): """Make the loop domain for looping over the array's index space. For this method to work, ndim must be concrete. @@ -1352,8 +932,419 @@ class Array(LazyObjectBase): return tuple(var('__%s_inames_%d' % (self.name, d)) for d in range(ndim)) -# }}} End array class + def _to_repr_dict(self): + repr_dict = super(Array, self)._to_repr_dict() + repr_dict.update({ + 'ndim': self.ndim, + 'inames': self.inames, + 'domain': self.domain_expr, + 'shape': self.shape, + 'dtype': self.dtype, + 'is_integral': self.is_integral, + 'assumptions': self.assumptions}) + return repr_dict + + def _shadow_size(self, axes=None): + """When axes is None, computes the full size. Otherwise computes + the size of the projected array on the given axes. + """ + if axes is None: + axes = list(range(self.ndim)) + else: + # so that generators can be understood + axes = list(axes) + shape_vars = tuple( + var(self._shape[ax].name) if isinstance(self._shape[ax], Array) + else str(self._shape[ax]) + for ax in axes) + size_expr = Product(shape_vars) + shape_ctx = {s.name: s.value for s in self._shape + if isinstance(s, Array) and s.value is not None} + try: + return evaluate(size_expr, shape_ctx) + except UnknownVariableError: + return size_expr + + @property + def _is_leaf(self): + """Return True if the array expression is a flat-out read. + """ + return self.expr == self._make_default_expr() + + def _make_default_inames(self, *args): + """Make the default inames, simply as lookup expressions. + """ + return self._make_axis_indices(*args) + + def _make_default_shape(self, ndim): + """Make a default shape based of name and ndim + """ + # zero-dimensional, aka a scalar + if ndim == 0: + return () + + # symbolic-dimensions + if isinstance(ndim, Unsigned): + assert ndim.ndim == 0 + return Lookup(Variable(self.name), "shape") + + # constant positive dimensions + assert ndim > 0 + if len(self.name) > 1 and self.name[:2] == '__': + name_prefix = '' + else: + name_prefix = '__' + names = [name_prefix + self.name + '_shape_%d' % d + for d in range(ndim)] + return tuple( + Unsigned( + name=names[d], + expr=var(names[d])) + for d in range(ndim)) + def _has_name(self, name): + """Check whether a given name is taken by one of the following: + + - self.name + - self.inames + - self.arguments + - self.bound_arguments + - self.temporaries + - self.env + """ + if isinstance(name, Variable): + name = name.name + else: + assert isinstance(name, str) + has_name = ( + (name == self.name) + or (var(name) in self.inames) + or (name in self.arguments.keys()) + or (name in self.bound_arguments.keys()) + or (name in self.temporaries.keys()) + or (name in self.env.keys()) + ) + return has_name + + def _shape_names(self): + """Shape information, stringified to be legal names. + """ + for s in self._shape: + assert isinstance(s, Unsigned) + return tuple(s.name for s in self._shape) + + @property + def _shape_str(self): + """A shape description string used for loopy kernel construction. + """ + shape_names = self._shape_names() + return ', '.join(shape_names) + + @property + def _shape_expr(self): + """Values for the shape (if not None), otherwise expressions. + """ + return tuple( + s.value if s.value is not None else s.expr + for s in self._shape) + + # }}} End private api + + # {{{ evaluation related + + def _check_preconditions(self, context): + """Call checkers in the list of preconditions. + + :param context: a dict which is passed to all checkers. + """ + failed_checks = [] + for checker in self.preconditions: + try: + res = checker(context) + if res is not None: + if isinstance(res, bool): + if res: + pass + else: + raise PreconditionNotMetError( + "precondition checker failed: %s" + % str(checker)) + else: + raise ValueError( + "cannot understand the return value " + "of the precondition checker %s" + % str(checker)) + except Exception as e: # noqa: W0703 + print(e) + failed_checks.append(checker) + + err_msgs = [] + for fc in failed_checks: + msg = ("Precondition %s not met, which was imposed at\n\n" + % str(checker)) + if hasattr(fc, "frame"): + msg = msg + '\n'.join(format_list(extract_stack( + fc.frame))) + err_msgs.append(msg) + if len(failed_checks) > 0: + precon_err_msg = ( + "%d out of %d preconditions are not met" + % (len(failed_checks), len(self.preconditions))) + for im, msg in enumerate(err_msgs): + precon_err_msg = (precon_err_msg + '\n\n' + + '[%d / %d]: ' % (im + 1, len(failed_checks)) + msg) + raise PreconditionNotMetError(precon_err_msg) + + def _set_shape_values(self, shape): + """Instantiate shape with concrete values. + + This method should not be called from a user. Users should use + Array.with_shape_data() instead. + """ + for s in tuple(shape): + assert int(s) == s + assert s >= 0 + for s, sym_s in zip(shape, self._shape): + assert isinstance(sym_s, Unsigned) + sym_s.value = s + + def _get_data_mapping(self, knl=None): + """Make a data mapping using known data, tailored for giving inputs to + the loopy kernel. Returns all known information if knl is None. + + :param knl: the loopy kernel + """ + data_map = {} + shapeval_expansion_list = [] + + # gather captured data + for arr_name, varr in self.env.items(): + if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: + continue + + if arr_name == self.name: + # only specify output shape, and let the backend to do the malloc + for out_s in self._shape: + if out_s.value is None: + shapeval_expansion_list.append(out_s) + data_map[out_s.name] = out_s.value + + if isinstance(varr, Array) and varr.value is not None: + data_map[arr_name] = varr.value + elif isinstance(varr, np.ndarray): + data_map[arr_name] = varr + elif varr is None: + pass # self + else: + raise RuntimeError("unrecogonized captured variable %s" % arr_name) + + # try to get as much extra data that loopy wants as possible + # also evaluates the shape expressions + if knl is None: + # gather all known shapes + for s in self._shape: + if s.name in data_map: + continue + if s.value is not None: + data_map[s.name] = s.value + for arg in chain(self.arguments.values(), + self.bound_arguments.values(), self.temporaries.values()): + if arg is None: + continue # skip self + assert isinstance(arg, Array) + for s in arg._shape: + if s.value is None: + shapeval_expansion_list.append(s) + data_map[s.name] = s.value + else: + for argname, arg in knl.arg_dict.items(): + if argname in data_map: + continue + if argname in self._shape_names(): + shape_val = self._shape[self._shape_names().index(argname)] + if shape_val.value is None: + shapeval_expansion_list.append(shape_val) + data_map[argname] = shape_val.value + for arr_arg in self.bound_arguments.values(): + if arr_arg is None: + continue + if argname in arr_arg._shape_names(): + shape_val = arr_arg._shape[ + arr_arg._shape_names().index(argname)] + if shape_val.value is None: + shapeval_expansion_list.append(shape_val) + data_map[argname] = shape_val.value + for arr_imd in self.temporaries.values(): + if argname in arr_imd._shape_names(): + shape_val = arr_imd._shape[ + arr_imd._shape_names().index(argname)] + if shape_val.value is None: + shapeval_expansion_list.append(shape_val) + data_map[argname] = shape_val.value + # no need to search arguments + assert len(self.arguments) == 0 + + # evaluate shape expressions + for se in shapeval_expansion_list: + try: + seval = evaluate(se.expr, data_map) + except UnknownVariableError: + warnings.warn( + "cannot get value for %s prior to calling the loopy kernel" + % se.name) + seval = None + data_map[se.name] = seval + + # purge None-valued entries + entries_to_purge = [] + for key, val in data_map.items(): + if val is None: + entries_to_purge.append(key) + for key in entries_to_purge: + data_map.pop(key) + return data_map + + def eval(self, shape_dtype=np.int32): + """Evaluates the array expression after binding all the arguments. + Note that this function has side effects: it sets self.value to the + evaluation result. If the results is available, it is returned without + re-computing. + """ + if self.value is not None: + logging.info("cache hit when evaluating %s" % self.name) + return self.value + + if len(self.arguments) > 0: + raise ValueError("cannot evaluate a partially-bound expression " + "(missing data for %s)" % ', '.join(self.arguments.keys())) + + # Step 1: + # identify codegen statements (loopy assignments) + # and raise exception if ther are multiple statements + # FIXME + n_insns = 1 + if n_insns > 1: + raise TypeError("the array %s cannot be evaluated without temporaries " + "(use the compiler instead) " % self.name) + + # Step 2: + # make the initial loop domain by multiplying the index space of + # the array with hidden axes (e.g. in the case of inner products) + pass + + # Step 3: + # make the conditional expression that accounts for + # + # - sliced LHS A[1:n-1:2, :] = ... + # + # - conditional assignment A[A>0] = ... + # + # Note that this is done on the RHS, e.g. + # A[i, j] = if((i >= 1) and (i < n - 1) and ((i - 1) % 2 == 0), ..., ...) + # A[i, j] = if(A[i, j] > 0, ..., ...) + # FIXME + rhs_expr = self.expr + + # Step 4 + # Account for fancy indexing on the LHS A[B] = ... + # by adding a separate loop domain over the index space of B + # and making corresponding LHS expressions + # FIXME + lhs_expr = var(self.name)[self.inames] + loop_domain = self._generate_index_domains() + + # Step 5 + # Make and run loopy kernel + # + # FIXME: collect all assumptions and pass to loopy + # + kernel_args = [] + + # array args + for arr_name, varr in self.env.items(): + if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: + continue + if arr_name == self.name: + continue # handle self later + elif arr_name in self.arguments.keys(): + if self.arguments[arr_name] is None: + pass # self + else: + assert isinstance(self.arguments[arr_name], Array) + kernel_args.append(lp.GlobalArg(arr_name, + shape=self.arguments[arr_name]._shape_str)) + elif arr_name in self.bound_arguments.keys(): + if self.bound_arguments[arr_name] is None: + pass # self + else: + assert isinstance(self.bound_arguments[arr_name], Array) + kernel_args.append(lp.GlobalArg(arr_name, + shape=self.bound_arguments[arr_name]._shape_str)) + else: + raise RuntimeError("cannot find shape information of %s" % arr_name) + + # output array args + kernel_args.append(lp.GlobalArg(self.name, shape=self._shape_str)) + + # let loopy do the inference + kernel_args.append('...') + + knl = lp.make_kernel( + loop_domain, + [ + lp.Assignment(lhs_expr, rhs_expr) + ], + kernel_args, + lang_version=(2018, 2)) + + knl = lp.set_options(knl, return_dict=True) + + # help with dtype inference + extra_dtype_dict = {} + for argname, arg in knl.arg_dict.items(): + if arg.dtype is None: + # array args + if argname == self.name: + if self.dtype is not None: + extra_dtype_dict[argname] = self.dtype + continue + + if argname in self.arguments: + if self.arguments[argname].dtype is not None: + extra_dtype_dict[argname] = self.arguments[argname].dtype + continue + elif argname in self.bound_arguments: + if self.bound_arguments[argname].dtype is not None: + extra_dtype_dict[argname] = \ + self.bound_arguments[argname].dtype + continue + + # shape args + if argname in self._shape_names(): + extra_dtype_dict[argname] = shape_dtype + for arr_arg in self.bound_arguments.values(): + if arr_arg is None: + continue + if argname in arr_arg._shape_names(): + extra_dtype_dict[argname] = shape_dtype + + knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) + data_map = self._get_data_mapping(knl) + + # TODO: to test everything that the kernel sees, + # let loopy infer more context first (e.g. shape of input arrays) + self._check_preconditions(context=data_map) + + queue = cl.CommandQueue(self.env['cl_ctx']) + evt, lp_res = knl(queue, **data_map) + + self.value = lp_res[self.name] + return self.value + + # }}} End evaluation related + + +# }}} End array class ndarray = Array diff --git a/lappy/lib/__init__.py b/lappy/lib/__init__.py new file mode 100644 index 0000000..6705d8f --- /dev/null +++ b/lappy/lib/__init__.py @@ -0,0 +1,23 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" diff --git a/test/test_broadcast.py b/test/test_broadcast.py index 92ebe90..5aaed76 100644 --- a/test/test_broadcast.py +++ b/test/test_broadcast.py @@ -78,7 +78,7 @@ def test_symbolic_broadcast_rules(shapes): # inform lappy with actual shapes to evaluate exprs for i in range(len(laarrs)): laarrs[i] = laarrs[i].with_shape_data(shapes[i]) - arr_contexts = [arr.get_data_mapping() for arr in laarrs] + arr_contexts = [arr._get_data_mapping() for arr in laarrs] broadcast_ctx = {} for ctx in arr_contexts: broadcast_ctx.update(ctx) -- GitLab From 37abbfece456cb8109b86a87c118f9fce6db8d9b Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 17:33:23 -0500 Subject: [PATCH 24/59] Improve mental model of stateless objects --- experiments/finite_difference.py | 12 +++ lappy/__init__.py | 4 +- lappy/core/array.py | 171 +++++++++++++++++++------------ lappy/core/broadcast.py | 8 +- test/test_api_array.py | 8 +- 5 files changed, 130 insertions(+), 73 deletions(-) create mode 100644 experiments/finite_difference.py diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py new file mode 100644 index 0000000..bab049e --- /dev/null +++ b/experiments/finite_difference.py @@ -0,0 +1,12 @@ +import lappy as lap +import numpy as np + +nx = 5 +x_data = np.linspace(0, 1, nx) + +x = lap.to_lappy_array(x_data) + + +print(x) + +import pudb; pu.db diff --git a/lappy/__init__.py b/lappy/__init__.py index ce2c953..719687b 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -22,6 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from lappy.core import ndarray +from lappy.core import ndarray, to_lappy_array -__all__ = ["ndarray", ] +__all__ = ["ndarray", "to_lappy_array"] diff --git a/lappy/core/array.py b/lappy/core/array.py index 5d52fd3..9c1caab 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -47,7 +47,6 @@ def default_env(): for all stateless arrays. """ return { - "isl_ctx": isl.DEFAULT_CONTEXT, "cl_ctx": None, "cu_ctx": None } @@ -145,7 +144,7 @@ class LazyObjectBase(object): def _make_default_name(self): name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) self.__class__._counter += 1 - print("A default is created %s" % name) + # print("A default is created %s" % name) return name def __str__(self): @@ -232,6 +231,8 @@ class LazyObjectBase(object): @value.setter def value(self, val): + if self.is_stateless: + raise TypeError("cannot set the value of a stateless object") self.env[self.name] = val def _to_repr_dict(self): @@ -270,9 +271,8 @@ class Array(LazyObjectBase): .. attribute:: shape - overall shape of array expression. Tuple of integers and strings. - Behind the scene, ``_shape`` stores a tuple of :class:`Unsigned`, - and the ``shape`` data is formed on query to allow shape inference. + overall shape of array expression. + Tuple of :class:`Unsigned` with length equal to ``ndim``. .. attribute:: inames @@ -335,9 +335,20 @@ class Array(LazyObjectBase): self.expr = self._make_default_expr(ndim) if shape is None: - self._shape = self._make_default_shape(ndim) + lapshape = self._make_default_shape(ndim) + self.shape = tuple(s.as_stateless() for s in lapshape) else: - self._shape = to_lappy_shape(shape) + lapshape = to_lappy_shape(shape) + self.shape = tuple(s.as_stateless() for s in lapshape) + for s in lapshape: + if s.value is not None: + if s.name in self.env: + warnings.warn("captured value for %s is overwritten " + "(%s --> %s)" % ( + s.name, + str(self.env[s.name]), + str(s.value))) + self.env[s.name] = s.value self.dtype = kwargs.pop("dtype", None) self.is_integral = kwargs.pop("is_integral", False) @@ -361,14 +372,14 @@ class Array(LazyObjectBase): cp_assumptions = self.assumptions.copy() if stateless: - env = default_env() + env = None else: env = self.env.copy() return self.__class__( name=self.name, ndim=self.ndim, inames=self.inames, - shape=self._shape, dtype=self.dtype, + shape=self.shape, dtype=self.dtype, expr=self.expr, is_integral=self.is_integral, domain_expr=self.domain_expr, arguments=self.arguments.copy(), @@ -477,7 +488,7 @@ class Array(LazyObjectBase): return expr.__class__.init_arg_names == ('name',) assert allow_complex_expr or all( - is_trivial(s.expr) for s in self._shape) + is_trivial(s.expr) for s in self.shape) if new_name is None: new_name = 'bound%d_' % self.__class__._counter + self.name @@ -485,7 +496,7 @@ class Array(LazyObjectBase): new_arr = self.with_name(new_name) delta_dict = {} - for s, s_val in zip(new_arr._shape, shape_data): + for s, s_val in zip(new_arr.shape, shape_data): if s_val is None: continue else: @@ -501,7 +512,7 @@ class Array(LazyObjectBase): for arg in new_arr.arguments.values(): if arg is None: continue - for sym_s in arg._shape: + for sym_s in arg.shape: if sym_s.name in delta_dict: if sym_s.value == delta_dict[sym_s.name][0]: # old value sym_s.value = delta_dict[sym_s.name][1] @@ -511,7 +522,7 @@ class Array(LazyObjectBase): assert sym_s.value == delta_dict[sym_s.name][1] for imd in new_arr.temporaries.values(): - for sym_s in imd._shape: + for sym_s in imd.shape: if sym_s.name in delta_dict: if sym_s.value == delta_dict[sym_s.name][0]: # old value sym_s.value = delta_dict[sym_s.name][1] @@ -551,19 +562,16 @@ class Array(LazyObjectBase): else: new_arr.bound_arguments[key].dtype = val.dtype if hasattr(val, 'shape'): - if key == self.name: - new_arr._set_shape_values(val.shape) - else: - new_arr.bound_arguments[key]._set_shape_values(val.shape) + new_arr._set_shape_values(val.shape, key) if hasattr(val, 'ndim'): if key == self.name: assert new_arr.ndim == val.ndim - assert len(new_arr._shape) == val.ndim + assert len(new_arr.shape) == val.ndim else: assert new_arr.bound_arguments[key].ndim == val.ndim assert len(val.shape) == val.ndim - new_arr.env[key] = val + new_arr.env[key] = val # actual data # rename value of self new_arr.env[new_name] = new_arr.env.pop(self.name, None) @@ -573,17 +581,9 @@ class Array(LazyObjectBase): # {{{ public (numpy-compliant) api - @property - def shape(self): - """Returns the shape as a tuple of ints if the value is known, or - strings of names otherwise. - """ - return tuple(s.value if s.value is not None else s.name - for s in self._shape) - @property def ndim(self): - return len(self._shape) + return len(self.shape) @property def size(self): @@ -865,12 +865,19 @@ class Array(LazyObjectBase): # {{{ private api + def _get_shape_vals_or_strs(self): + """Returns the shape as a tuple of ints if the value is known, or + strings of names otherwise. + """ + return tuple(self.env[s.name] if s.name in self.env else s.name + for s in self.shape) + def _make_default_expr(self, *args): """The default expression is the array it self. :param ndim: optional, used when the array shape is not set. """ - if hasattr(self, '_shape'): + if hasattr(self, 'shape'): ndim = self.ndim else: ndim, = args @@ -912,7 +919,7 @@ class Array(LazyObjectBase): :param ndim: optional, used when the array shape is not set yet. """ # zero-dimensional, aka a scalar - if hasattr(self, '_shape'): + if hasattr(self, 'shape'): ndim = self.ndim else: ndim, = args @@ -954,11 +961,11 @@ class Array(LazyObjectBase): # so that generators can be understood axes = list(axes) shape_vars = tuple( - var(self._shape[ax].name) if isinstance(self._shape[ax], Array) - else str(self._shape[ax]) + var(self.shape[ax].name) if isinstance(self.shape[ax], Array) + else str(self.shape[ax]) for ax in axes) size_expr = Product(shape_vars) - shape_ctx = {s.name: s.value for s in self._shape + shape_ctx = {s.name: s.value for s in self.shape if isinstance(s, Array) and s.value is not None} try: return evaluate(size_expr, shape_ctx) @@ -1029,9 +1036,9 @@ class Array(LazyObjectBase): def _shape_names(self): """Shape information, stringified to be legal names. """ - for s in self._shape: + for s in self.shape: assert isinstance(s, Unsigned) - return tuple(s.name for s in self._shape) + return tuple(s.name for s in self.shape) @property def _shape_str(self): @@ -1046,7 +1053,7 @@ class Array(LazyObjectBase): """ return tuple( s.value if s.value is not None else s.expr - for s in self._shape) + for s in self.shape) # }}} End private api @@ -1095,18 +1102,40 @@ class Array(LazyObjectBase): + '[%d / %d]: ' % (im + 1, len(failed_checks)) + msg) raise PreconditionNotMetError(precon_err_msg) - def _set_shape_values(self, shape): + def _set_shape_values(self, shape, name=None): """Instantiate shape with concrete values. + Use ``name`` to indicate an argument, bound argument, or temporary + to work with. If name is None, the function has same effects as + ``with_shape_data``. + This method should not be called from a user. Users should use Array.with_shape_data() instead. """ for s in tuple(shape): assert int(s) == s assert s >= 0 - for s, sym_s in zip(shape, self._shape): + + if name is None or name == self.name: + sym_shape = self.shape + else: + if name in self.arguments: + sym_shape = self.arguments[name].shape + elif name in self.bound_arguments: + sym_shape = self.bound_arguments[name].shape + elif name in self.temporaries: + sym_shape = self.temporaries[name].shape + else: + raise ValueError() + + for s, sym_s in zip(shape, sym_shape): assert isinstance(sym_s, Unsigned) - sym_s.value = s + if sym_s.name in self.env: + warnings.warn("captured value for %s is overwritten " + "(%s --> %s)" % ( + sym_s.name, + str(self.env[sym_s.name]), str(s))) + self.env[sym_s.name] = s def _get_data_mapping(self, knl=None): """Make a data mapping using known data, tailored for giving inputs to @@ -1119,22 +1148,25 @@ class Array(LazyObjectBase): # gather captured data for arr_name, varr in self.env.items(): - if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: + if arr_name in ['cl_ctx', 'cu_ctx']: continue if arr_name == self.name: # only specify output shape, and let the backend to do the malloc - for out_s in self._shape: - if out_s.value is None: + for out_s in self.shape: + if out_s.name not in self.env: shapeval_expansion_list.append(out_s) data_map[out_s.name] = out_s.value - if isinstance(varr, Array) and varr.value is not None: - data_map[arr_name] = varr.value elif isinstance(varr, np.ndarray): data_map[arr_name] = varr + elif varr is None: pass # self + + elif np.isscalar(varr): + data_map[arr_name] = varr + else: raise RuntimeError("unrecogonized captured variable %s" % arr_name) @@ -1142,7 +1174,7 @@ class Array(LazyObjectBase): # also evaluates the shape expressions if knl is None: # gather all known shapes - for s in self._shape: + for s in self.shape: if s.name in data_map: continue if s.value is not None: @@ -1152,8 +1184,8 @@ class Array(LazyObjectBase): if arg is None: continue # skip self assert isinstance(arg, Array) - for s in arg._shape: - if s.value is None: + for s in arg.shape: + if s.name not in self.env: shapeval_expansion_list.append(s) data_map[s.name] = s.value else: @@ -1161,41 +1193,52 @@ class Array(LazyObjectBase): if argname in data_map: continue if argname in self._shape_names(): - shape_val = self._shape[self._shape_names().index(argname)] - if shape_val.value is None: + shape_val = self.shape[self._shape_names().index(argname)] + if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) data_map[argname] = shape_val.value for arr_arg in self.bound_arguments.values(): if arr_arg is None: continue if argname in arr_arg._shape_names(): - shape_val = arr_arg._shape[ + shape_val = arr_arg.shape[ arr_arg._shape_names().index(argname)] - if shape_val.value is None: + if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) data_map[argname] = shape_val.value for arr_imd in self.temporaries.values(): if argname in arr_imd._shape_names(): - shape_val = arr_imd._shape[ + shape_val = arr_imd.shape[ arr_imd._shape_names().index(argname)] - if shape_val.value is None: + if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) data_map[argname] = shape_val.value # no need to search arguments assert len(self.arguments) == 0 - # evaluate shape expressions + # Make sure not to include None's prior to evaluating expressions + data_map = self._purge_nones(data_map) + for se in shapeval_expansion_list: try: seval = evaluate(se.expr, data_map) except UnknownVariableError: - warnings.warn( - "cannot get value for %s prior to calling the loopy kernel" - % se.name) - seval = None - data_map[se.name] = seval + if 0: + warnings.warn( + "cannot get value for %s prior to calling " + "the loopy kernel" % se.name) + continue + + if se.name in data_map: + assert (seval is None) or (seval == data_map[se.name]) + else: + data_map[se.name] = seval - # purge None-valued entries + return data_map + + def _purge_nones(self, data_map): + """Purge None-valued entries from a data map. + """ entries_to_purge = [] for key, val in data_map.items(): if val is None: @@ -1262,7 +1305,7 @@ class Array(LazyObjectBase): # array args for arr_name, varr in self.env.items(): - if arr_name in ['isl_ctx', 'cl_ctx', 'cu_ctx']: + if arr_name in ['cl_ctx', 'cu_ctx']: continue if arr_name == self.name: continue # handle self later @@ -1281,7 +1324,8 @@ class Array(LazyObjectBase): kernel_args.append(lp.GlobalArg(arr_name, shape=self.bound_arguments[arr_name]._shape_str)) else: - raise RuntimeError("cannot find shape information of %s" % arr_name) + pass + # warnings.warn("cannot find shape information of %s" % arr_name) # output array args kernel_args.append(lp.GlobalArg(self.name, shape=self._shape_str)) @@ -1329,6 +1373,7 @@ class Array(LazyObjectBase): extra_dtype_dict[argname] = shape_dtype knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) + data_map = self._get_data_mapping(knl) # TODO: to test everything that the kernel sees, @@ -1398,7 +1443,7 @@ class Unsigned(Int): kwargs['name'] = name super(Unsigned, self).__init__(**kwargs) self.assumptions = isl.BasicSet.read_from_str( - self.env['isl_ctx'], + isl.DEFAULT_CONTEXT, "{ [%s] : %s >= 0 }" % ((self.name, ) * 2)) assert self.is_integral diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 0fa6926..775eed4 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -78,12 +78,12 @@ class BroadcastResult(object): self.ndim = max(ndims) # values when available, names otherwise - shapes = [arr.shape for arr in self.base_arrays] + shapes = [arr._get_shape_vals_or_strs() for arr in self.base_arrays] # gather shape expressions expr_map = {} for arr in self.base_arrays: - for s in arr._shape: + for s in arr.shape: if s.name not in expr_map: expr_map[var(s.name)] = s.expr else: @@ -203,7 +203,7 @@ class BroadcastResult(object): assert dim_offset >= 0 # ints and strs - in_shape = base_arr.shape + in_shape = base_arr._get_shape_vals_or_strs() if dim_offset == 0: if in_shape == self.shape: @@ -222,7 +222,7 @@ class BroadcastResult(object): brdc_arr = base_arr.with_name(name) assert isinstance(self.ndim, int) and self.ndim >= 0 - brdc_arr._shape = lappy_shape + brdc_arr.shape = lappy_shape # make new inames brdc_arr.inames = brdc_arr._make_default_inames() diff --git a/test/test_api_array.py b/test/test_api_array.py index 04f9bd1..34adb41 100644 --- a/test/test_api_array.py +++ b/test/test_api_array.py @@ -36,7 +36,7 @@ import pytest def test_construction_from_numpy_array(nparr): arr = to_lappy_array(nparr) assert isinstance(arr, Array) - assert arr.shape == nparr.shape + assert arr._get_shape_vals_or_strs() == nparr.shape assert arr.dtype == nparr.dtype @@ -62,14 +62,14 @@ def test_construction_abstract(name, shape, dtype): arr = Array(name=name, shape=shape, dtype=dtype) assert arr.name == name assert arr.ndim == len(shape) - assert arr.shape == normalized_shape + assert arr._get_shape_vals_or_strs() == normalized_shape assert arr.dtype == dtype # name and shape are positional args arr = Array(name, shape, dtype=dtype) assert arr.name == name assert arr.ndim == len(shape) - assert arr.shape == normalized_shape + assert arr._get_shape_vals_or_strs() == normalized_shape assert arr.dtype == dtype # shape given by a string @@ -77,5 +77,5 @@ def test_construction_abstract(name, shape, dtype): arr = Array(name, shape_str, dtype=dtype) assert arr.name == name assert arr.ndim == len(shape) - assert arr.shape == normalized_shape + assert arr._get_shape_vals_or_strs() == normalized_shape assert arr.dtype == dtype -- GitLab From 8a17160bc57392d3ed18fa8c1cddbef4b313ed45 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 18:28:48 -0500 Subject: [PATCH 25/59] Update the codebase to pass all tests --- experiments/finite_difference.py | 3 -- lappy/core/array.py | 68 +++++++++++++++++++++++--------- lappy/core/basic_ops.py | 12 ++++-- lappy/core/conditional.py | 8 ++-- test/test_broadcast.py | 3 ++ 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index bab049e..b2de96f 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -6,7 +6,4 @@ x_data = np.linspace(0, 1, nx) x = lap.to_lappy_array(x_data) - print(x) - -import pudb; pu.db diff --git a/lappy/core/array.py b/lappy/core/array.py index 9c1caab..3118722 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -152,7 +152,7 @@ class LazyObjectBase(object): type and name for user-defined names or the expression for the auto-generated names. """ - if self.value is not None: + if (not self.is_stateless) and (self.value is not None): return str(self.value) if len(self.name) >= 2 and self.name[:2] == '__': # auto-generated name @@ -222,9 +222,10 @@ class LazyObjectBase(object): @property def value(self): - if self.env is None: - warnings.warn("requesting value from a stateless array") - return None + if self.is_stateless: + raise TypeError( + "cannot ask for the value of a stateless object " + "(use _get_value() from the top-level object instead)") if self.name not in self.env.keys(): return None return self.env[self.name] @@ -341,7 +342,7 @@ class Array(LazyObjectBase): lapshape = to_lappy_shape(shape) self.shape = tuple(s.as_stateless() for s in lapshape) for s in lapshape: - if s.value is not None: + if (not s.is_stateless) and (s.value is not None): if s.name in self.env: warnings.warn("captured value for %s is overwritten " "(%s --> %s)" % ( @@ -390,6 +391,13 @@ class Array(LazyObjectBase): assumptions=cp_assumptions, ) + def with_state(self, env): + """Swap out the captured state. + """ + sarr = self.as_stateless() + sarr.env = env + return sarr + def as_stateless(self): """Returns a stateless copy of self. """ @@ -501,8 +509,8 @@ class Array(LazyObjectBase): continue else: assert isinstance(s_val, int) and s_val > 0 - delta_dict[s.name] = (s.value, s_val) - s.value = s_val + delta_dict[s.name] = (new_arr._get_value(s.name), s_val) + new_arr.env[s.name] = s_val # iterate through arguments and temporaries and # propagate the assignments to all symbols of the same name @@ -514,8 +522,8 @@ class Array(LazyObjectBase): continue for sym_s in arg.shape: if sym_s.name in delta_dict: - if sym_s.value == delta_dict[sym_s.name][0]: # old value - sym_s.value = delta_dict[sym_s.name][1] + if arg._get_value(sym_s.name) == delta_dict[sym_s.name][0]: + new_arr.env[sym_s.name] = delta_dict[sym_s.name][1] else: # the value is updated, since it points to the # same Unsigned obj @@ -865,6 +873,27 @@ class Array(LazyObjectBase): # {{{ private api + def _get_shape_vals(self, axis=None): + """Returns the shape values where available, None otherwise. + """ + return tuple(self.env[s.name] if s.name in self.env else None + for s in self.shape) + + def _get_value(self, name=None): + """Try to get the value of a captured variable. Returns None + if the process fails. + """ + if name is None: + return self.value + + if self.is_stateless: + return None + + if name in self.env: + return self.env[name] + else: + return None + def _get_shape_vals_or_strs(self): """Returns the shape as a tuple of ints if the value is known, or strings of names otherwise. @@ -965,10 +994,8 @@ class Array(LazyObjectBase): else str(self.shape[ax]) for ax in axes) size_expr = Product(shape_vars) - shape_ctx = {s.name: s.value for s in self.shape - if isinstance(s, Array) and s.value is not None} try: - return evaluate(size_expr, shape_ctx) + return evaluate(size_expr, self.env) except UnknownVariableError: return size_expr @@ -1052,7 +1079,7 @@ class Array(LazyObjectBase): """Values for the shape (if not None), otherwise expressions. """ return tuple( - s.value if s.value is not None else s.expr + self.env[s.name] if s.name in self.env else s.expr for s in self.shape) # }}} End private api @@ -1156,7 +1183,8 @@ class Array(LazyObjectBase): for out_s in self.shape: if out_s.name not in self.env: shapeval_expansion_list.append(out_s) - data_map[out_s.name] = out_s.value + else: + data_map[out_s.name] = self.env[out_s.name] elif isinstance(varr, np.ndarray): data_map[arr_name] = varr @@ -1187,7 +1215,8 @@ class Array(LazyObjectBase): for s in arg.shape: if s.name not in self.env: shapeval_expansion_list.append(s) - data_map[s.name] = s.value + elif arg._get_value(s.name) is not None: + data_map[s.name] = s.value else: for argname, arg in knl.arg_dict.items(): if argname in data_map: @@ -1196,7 +1225,8 @@ class Array(LazyObjectBase): shape_val = self.shape[self._shape_names().index(argname)] if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value + else: + data_map[argname] = self.env[shape_val.name] for arr_arg in self.bound_arguments.values(): if arr_arg is None: continue @@ -1205,14 +1235,16 @@ class Array(LazyObjectBase): arr_arg._shape_names().index(argname)] if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value + else: + data_map[argname] = self.env[shape_val.name] for arr_imd in self.temporaries.values(): if argname in arr_imd._shape_names(): shape_val = arr_imd.shape[ arr_imd._shape_names().index(argname)] if shape_val.name not in self.env: shapeval_expansion_list.append(shape_val) - data_map[argname] = shape_val.value + else: + data_map[argname] = self.env[shape_val.name] # no need to search arguments assert len(self.arguments) == 0 diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index f00ca76..aefac9c 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -176,7 +176,7 @@ class SubArrayFactory(object): :param iaxis: the index of the axis being sliced. """ assert isinstance(slc, slice) - dim = self.base_arr._shape[iaxis] + dim = self.base_arr.shape[iaxis].with_state(self.base_arr.env) step_none = slc.step is None if step_none: @@ -524,7 +524,13 @@ def reshape(array, newshape, order='C', name=None, inames=None): new_arr.inames = inames new_arr.preconditions = new_precond - new_arr._shape = to_lappy_shape(newshape) # noqa: W0212 + newshape = to_lappy_shape(newshape) + new_arr.shape = tuple(s.as_stateless() for s in newshape) + for s in newshape: + if s.value is not None: + # generated name should not conflict + assert s.name not in new_arr.env + new_arr.env[s.name] = s.value if name != array.name: # if the reference is no longer to self @@ -584,7 +590,7 @@ def transpose(array, axes=None, name=None): new_arr = array.with_name(name) new_arr.inames = tuple(array.inames[axes[i]] for i in range(ndim)) - new_arr._shape = tuple(array._shape[axes[i]] for i in range(ndim)) + new_arr.shape = tuple(array.shape[axes[i]] for i in range(ndim)) return new_arr # }}} End transpose diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index 064a3c0..869a6b9 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -104,16 +104,16 @@ def conditional(cond, con, alt, name=None): # con and alt must have the same shape, cond can be of the same shape, # or be a scalar of shape (, ). - if all(s1.name == s2.name for s1, s2 in zip(con._shape, alt._shape)): + if all(s1.name == s2.name for s1, s2 in zip(con.shape, alt.shape)): if cond.ndim > 0: if all(s0.name == s1.name - for s0, s1 in zip(cond._shape, con._shape)): - obj['shape'] = cond._shape + for s0, s1 in zip(cond.shape, con.shape)): + obj['shape'] = cond.shape else: # TODO: check/add preconditions as needed raise NotImplementedError() else: - obj['shape'] = con._shape + obj['shape'] = con.shape else: # TODO: check/add preconditions as needed raise NotImplementedError() diff --git a/test/test_broadcast.py b/test/test_broadcast.py index 5aaed76..eef6121 100644 --- a/test/test_broadcast.py +++ b/test/test_broadcast.py @@ -83,6 +83,9 @@ def test_symbolic_broadcast_rules(shapes): for ctx in arr_contexts: broadcast_ctx.update(ctx) + for arr in laarrs: + print(arr.env) + # shape and size are now expressions # evaluate those concretely with a sum -- GitLab From 477af0dafea72713620266771ed19b26fa06e271 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 19:19:28 -0500 Subject: [PATCH 26/59] Finite difference (numpy) --- experiments/finite_difference.py | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index b2de96f..c6e2b3e 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -1,9 +1,53 @@ -import lappy as lap +"""One-dimensional advection-diffusion by the FTCS scheme + + df/dt + U df/fx = D d2f/dx2 +""" import numpy as np -nx = 5 -x_data = np.linspace(0, 1, nx) +if __name__ == '__main__': + U = 1. + D = 0.05 + + nx = 200 + + length = 2. + h = length / (nx - 1) + dt = 0.0005 + final_time = 0.5 + nt = int(final_time / dt) + 1 + + print(nx, nt) + + # f = f(x, t) + f = np.zeros([nx, nt + 1]) + fe = np.zeros([nx, nt + 1]) + + # exact solution + def exact(time): + ex = np.exp(-4 * np.pi * np.pi * D * time) * ( + 0.5 * np.sin(2 * np.pi * (h * np.arange(nx) - time)) + ) + return ex + + # initial conditions + f[:, 0] = 0.5 * np.sin(2 * np.pi * h * np.arange(nx)) + fe[:, 0] = exact(0) + + for m in range(nt): + time = m * dt + + f0 = f[:, m] + fp = np.roll(f0, -1) + fm = np.roll(f0, 1) + + f[:, m + 1] = ( + f0 + - U * 0.5 * (dt / h) * (fp - fm) + + D * (dt / h**2) * (fp - 2 * f0 + fm) + ) -x = lap.to_lappy_array(x_data) + fe[:, m + 1] = exact(time) -print(x) + fs = f[:, nt] + fes = fe[:, nt] + print(time, np.linalg.norm(fs - fes) / np.linalg.norm(fes)) -- GitLab From 42688ac814710540c1d687f5142dbf3c7a1baad6 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 22:02:09 -0500 Subject: [PATCH 27/59] Add dtype promotion in broadcasting, minor bug fixes --- experiments/finite_difference.py | 70 ++++++++++++++++++++++---------- lappy/core/array.py | 14 +++++-- lappy/core/broadcast.py | 14 +++++-- lappy/core/ufuncs.py | 18 ++++++-- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index c6e2b3e..7cec1d2 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -3,51 +3,79 @@ df/dt + U df/fx = D d2f/dx2 """ import numpy as np +import pyopencl as cl -if __name__ == '__main__': +import lappy as lap +import lappy.core.ufuncs +import lappy.core.array + +from lappy import to_lappy_array + + +def solve(use_lappy): U = 1. D = 0.05 nx = 200 + nt = 1 length = 2. h = length / (nx - 1) - dt = 0.0005 - final_time = 0.5 - nt = int(final_time / dt) + 1 + dt = 1e-4 + final_time = dt * nt print(nx, nt) - # f = f(x, t) - f = np.zeros([nx, nt + 1]) - fe = np.zeros([nx, nt + 1]) + xx = np.arange(nx) * h + + if use_lappy: + env = lap.core.array.default_env() + env['cl_ctx'] = cl.create_some_context() + xx = to_lappy_array(xx, base_env=env) + sin = lap.core.ufuncs.sin + exp = lap.core.ufuncs.exp + + def roll(x, y): + return x + + else: + sin = np.sin + exp = np.exp + roll = np.roll # exact solution def exact(time): - ex = np.exp(-4 * np.pi * np.pi * D * time) * ( - 0.5 * np.sin(2 * np.pi * (h * np.arange(nx) - time)) - ) - return ex + ex = exp(-4 * np.pi * np.pi * D * time) * ( + 0.5 * sin(2 * np.pi * (xx - time))) + return xx - time # initial conditions - f[:, 0] = 0.5 * np.sin(2 * np.pi * h * np.arange(nx)) - fe[:, 0] = exact(0) + f = 0.5 * sin(2 * np.pi * xx) + fe = exact(0) for m in range(nt): + print(m) time = m * dt - f0 = f[:, m] - fp = np.roll(f0, -1) - fm = np.roll(f0, 1) + f0 = f + fp = roll(f0, -1) + fm = roll(f0, 1) - f[:, m + 1] = ( + f = ( f0 - U * 0.5 * (dt / h) * (fp - fm) + D * (dt / h**2) * (fp - 2 * f0 + fm) ) - fe[:, m + 1] = exact(time) + fe = exact(time) + + # print(fe.arguments, fe.env) + print(fe.expr) + fe.eval() + 1/0 - fs = f[:, nt] - fes = fe[:, nt] - print(time, np.linalg.norm(fs - fes) / np.linalg.norm(fes)) + print(time, np.linalg.norm(f - fe) / np.linalg.norm(fe)) + + +if __name__ == '__main__': + solve(use_lappy=True) diff --git a/lappy/core/array.py b/lappy/core/array.py index 3118722..d24f2b2 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -144,7 +144,6 @@ class LazyObjectBase(object): def _make_default_name(self): name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) self.__class__._counter += 1 - # print("A default is created %s" % name) return name def __str__(self): @@ -423,7 +422,7 @@ class Array(LazyObjectBase): raise ValueError( "Name %s is already taken by a captured variable" % name) - if self.name in self.env: + if (self.name in self.env) and rename_arguments: new_arr.env[name] = new_arr.env.pop(self.name) # when the argument is self-referencing @@ -455,8 +454,11 @@ class Array(LazyObjectBase): assert len(inames) == len(self.inames) for name in inames: - if self._has_name(name): - raise ValueError("name %s is already taken") + if self._has_name(name.name) and (name not in self.inames): + raise ValueError( + "name %s is already taken" + % name + ) iname_map = { iname: new_iname @@ -1412,6 +1414,10 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) + if 0: + print(knl) + print(data_map) + queue = cl.CommandQueue(self.env['cl_ctx']) evt, lp_res = knl(queue, **data_map) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 775eed4..88ef369 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -241,16 +241,24 @@ class BroadcastResult(object): (None, ) * dim_offset + base_arr._shape_expr): if inm is not None: expr_mapping[inm] = new_inm % in_shape_i + brdc_arr.expr = substitute(brdc_arr.expr, expr_mapping) - # update argument list + # update argument list if base_arr was there # if there was a self reference, the metadata needs to be captured for argname in base_arr.arguments.keys(): if base_arr.arguments[argname] is None: - brdc_arr.arguments[argname] = base_arr + brdc_arr.arguments[argname] = base_arr.as_stateless() for argname in base_arr.bound_arguments.keys(): if base_arr.bound_arguments[argname] is None: - brdc_arr.bound_arguments[argname] = base_arr + brdc_arr.bound_arguments[argname] = base_arr.as_stateless() + + # if not, insert the base arr as an argument + if base_arr.value is None: + brdc_arr.arguments[base_arr.name] = base_arr.as_stateless() + else: + brdc_arr.bound_arguments[base_arr.name] = \ + base_arr.as_stateless() self.broadcast_arrays.append(brdc_arr) diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 251c788..9fd4172 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -21,9 +21,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import numpy as np + from pymbolic import var from pymbolic.primitives import Min, Max -from lappy.core.array import Array +from lappy.core.array import Array, to_lappy_array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast from lappy.core.tools import ( @@ -108,6 +110,9 @@ class UnaryOperation(UFunc): def __call__(self, a, name=None): new_assumptions = None + if not isinstance(a, Array): + a = to_lappy_array(a) + # non-typed ufunc: pass-through of input's dtype if self.dtype is None: new_dtype = a.dtype @@ -246,18 +251,23 @@ class BinaryOperation(UFunc): raise NotImplementedError() def __call__(self, a, b, name=None): + + if not isinstance(a, Array): + a = to_lappy_array(a) + + if not isinstance(b, Array): + b = to_lappy_array(b) + bres = broadcast(a, b) a, b = bres.broadcast_arrays if self.dtype is None: - # FIXME: allow numpy type promotion rules if a.dtype is None: new_dtype = b.dtype elif b.dtype is None: new_dtype = a.dtype else: - assert a.dtype == b.dtype - new_dtype = a.dtype + new_dtype = np.result_type(a.dtype, b.dtype) else: new_dtype = self.dtype -- GitLab From 68047482f9f5fcc81767057de8c2fa01e1e07313 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 9 Mar 2020 22:30:35 -0500 Subject: [PATCH 28/59] Use ValueArg when the argument is a scalar --- experiments/finite_difference.py | 2 +- lappy/core/array.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index 7cec1d2..53ffe52 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -47,7 +47,7 @@ def solve(use_lappy): def exact(time): ex = exp(-4 * np.pi * np.pi * D * time) * ( 0.5 * sin(2 * np.pi * (xx - time))) - return xx - time + return exp(-4 * np.pi * np.pi * D * time) # initial conditions f = 0.5 * sin(2 * np.pi * xx) diff --git a/lappy/core/array.py b/lappy/core/array.py index d24f2b2..4553657 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -925,9 +925,9 @@ class Array(LazyObjectBase): def _generate_index_domains(self): """Make the loop domain for looping over the array's index space. - For this method to work, ndim must be concrete. """ - assert np.isscalar(self.ndim) and abs(self.ndim - int(self.ndim)) == 0 + if self.ndim == 0: + return None inames = tuple(ina.name for ina in self.inames) shape_names = self._shape_names() @@ -1268,6 +1268,15 @@ class Array(LazyObjectBase): else: data_map[se.name] = seval + # Remove keyward arguments not needed by the kernel + rm_args = [] + if knl is not None: + for argname in data_map.keys(): + if argname not in knl.arg_dict: + rm_args.append(argname) + for rmarg in rm_args: + del data_map[rmarg] + return data_map def _purge_nones(self, data_map): @@ -1355,8 +1364,11 @@ class Array(LazyObjectBase): pass # self else: assert isinstance(self.bound_arguments[arr_name], Array) - kernel_args.append(lp.GlobalArg(arr_name, - shape=self.bound_arguments[arr_name]._shape_str)) + if self.bound_arguments[arr_name].ndim == 0: + kernel_args.append(lp.ValueArg(arr_name)) + else: + kernel_args.append(lp.GlobalArg(arr_name, + shape=self.bound_arguments[arr_name]._shape_str)) else: pass # warnings.warn("cannot find shape information of %s" % arr_name) @@ -1414,7 +1426,7 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) - if 0: + if 1: print(knl) print(data_map) -- GitLab From ba30bbf83def369e90a2495eb6602483860cf3c4 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 10 Mar 2020 09:17:30 -0500 Subject: [PATCH 29/59] Make eval() compatible with scalars --- experiments/finite_difference.py | 2 + lappy/core/array.py | 65 ++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index 53ffe52..d3c827e 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -71,7 +71,9 @@ def solve(use_lappy): # print(fe.arguments, fe.env) print(fe.expr) + fe.env['cl_ctx'] = env['cl_ctx'] fe.eval() + print(fe.value) 1/0 print(time, np.linalg.norm(f - fe) / np.linalg.norm(fe)) diff --git a/lappy/core/array.py b/lappy/core/array.py index 4553657..d40078b 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -927,7 +927,13 @@ class Array(LazyObjectBase): """Make the loop domain for looping over the array's index space. """ if self.ndim == 0: - return None + # make a set with a single element for scalar output + return [ + isl.BasicSet.read_from_str( + isl.DEFAULT_CONTEXT, + "{ [%s] : %s >= 0 and %s < 1 }" + % ((self.name + '_dummy_iname', ) * 3)) + ] inames = tuple(ina.name for ina in self.inames) shape_names = self._shape_names() @@ -1336,7 +1342,11 @@ class Array(LazyObjectBase): # by adding a separate loop domain over the index space of B # and making corresponding LHS expressions # FIXME - lhs_expr = var(self.name)[self.inames] + if self.ndim == 0: + lhs_expr = var(self.name)[var(self.name + '_dummy_iname')] + else: + lhs_expr = var(self.name)[self.inames] + loop_domain = self._generate_index_domains() # Step 5 @@ -1357,24 +1367,53 @@ class Array(LazyObjectBase): pass # self else: assert isinstance(self.arguments[arr_name], Array) - kernel_args.append(lp.GlobalArg(arr_name, - shape=self.arguments[arr_name]._shape_str)) + kernel_args.append( + lp.GlobalArg( + arr_name, + shape=self.arguments[arr_name]._shape_str)) elif arr_name in self.bound_arguments.keys(): if self.bound_arguments[arr_name] is None: pass # self else: assert isinstance(self.bound_arguments[arr_name], Array) if self.bound_arguments[arr_name].ndim == 0: - kernel_args.append(lp.ValueArg(arr_name)) + kernel_args.append( + lp.ValueArg( + arr_name, + self.bound_arguments[arr_name].dtype)) else: - kernel_args.append(lp.GlobalArg(arr_name, - shape=self.bound_arguments[arr_name]._shape_str)) + kernel_args.append( + lp.GlobalArg( + arr_name, + shape=self.bound_arguments[ + arr_name]._shape_str)) + elif arr_name in self.temporaries.keys(): + assert isinstance(self.temporaries[arr_name], Array) + if self.temporaries[arr_name].ndim == 0: + kernel_args.append( + lp.ValueArg( + arr_name, + self.temporaries[arr_name].dtype)) + else: + kernel_args.append( + lp.GlobalArg( + arr_name, + shape=self.temporaries[ + arr_name]._shape_str)) else: pass - # warnings.warn("cannot find shape information of %s" % arr_name) + # warnings.warn( + # "cannot find shape information of %s" % arr_name) # output array args - kernel_args.append(lp.GlobalArg(self.name, shape=self._shape_str)) + if self.ndim == 0: + kernel_args.append( + lp.GlobalArg( + self.name, shape=(1, ), dtype=self.dtype)) + else: + kernel_args.append( + lp.GlobalArg( + self.name, shape=self._shape_str, dtype=self.dtype)) # let loopy do the inference kernel_args.append('...') @@ -1426,14 +1465,18 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) - if 1: + if 0: print(knl) print(data_map) queue = cl.CommandQueue(self.env['cl_ctx']) evt, lp_res = knl(queue, **data_map) - self.value = lp_res[self.name] + if self.ndim == 0: + self.value = lp_res[self.name][0] + else: + self.value = lp_res[self.name] + return self.value # }}} End evaluation related -- GitLab From 76b6b374f9f4558bddbdd406908d21897e357cf0 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 10 Mar 2020 09:17:54 -0500 Subject: [PATCH 30/59] Make eval() compatible with scalars --- experiments/finite_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index d3c827e..9837f33 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -47,7 +47,7 @@ def solve(use_lappy): def exact(time): ex = exp(-4 * np.pi * np.pi * D * time) * ( 0.5 * sin(2 * np.pi * (xx - time))) - return exp(-4 * np.pi * np.pi * D * time) + return ex # initial conditions f = 0.5 * sin(2 * np.pi * xx) -- GitLab From bd1838659492badec7d76f10370de771ed65d2d0 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 10 Mar 2020 13:18:46 -0500 Subject: [PATCH 31/59] Add dtype capture in to_lappy_unsigned --- experiments/finite_difference.py | 11 +++- lappy/core/array.py | 107 ++++++++++++++++++++----------- lappy/core/broadcast.py | 17 +++-- 3 files changed, 90 insertions(+), 45 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index 9837f33..6d457f6 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -47,7 +47,7 @@ def solve(use_lappy): def exact(time): ex = exp(-4 * np.pi * np.pi * D * time) * ( 0.5 * sin(2 * np.pi * (xx - time))) - return ex + return (xx - time) # initial conditions f = 0.5 * sin(2 * np.pi * xx) @@ -70,8 +70,15 @@ def solve(use_lappy): fe = exact(time) # print(fe.arguments, fe.env) - print(fe.expr) fe.env['cl_ctx'] = env['cl_ctx'] + print(fe.expr) + print(fe.arguments.keys()) + print(fe.bound_arguments.keys()) + print(fe.temporaries.keys()) + + for arg in fe.arguments.values(): + print(arg.expr) + fe.eval() print(fe.value) 1/0 diff --git a/lappy/core/array.py b/lappy/core/array.py index d40078b..55201ba 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -13,8 +13,7 @@ import loopy as lp import islpy as isl import pymbolic from pymbolic import var, evaluate -from pymbolic.primitives import ( - Expression, Lookup, Subscript, Variable, Product) +from pymbolic.primitives import Lookup, Subscript, Variable, Product from pymbolic.mapper.evaluator import UnknownVariableError from pprint import pformat @@ -1068,6 +1067,44 @@ class Array(LazyObjectBase): ) return has_name + def _find_name(self, name): + """Find the symbol by name. Returns None if not found. + """ + if isinstance(name, Variable): + name = name.name + else: + assert isinstance(name, str) + if name == self.name: + return self + elif name in self.arguments: + return self.arguments[name] + elif name in self.bound_arguments: + return self.bound_arguments[name] + elif name in self.temporaries: + return self.temporaries[name] + else: + for arg in self.arguments.values(): + if arg is None: + continue + an = arg._find_name(name) + if an is not None: + return an + for barg in self.bound_arguments.values(): + if barg is None: + continue + an = barg._find_name(name) + if an is not None: + return an + for tmp in self.temporaries.values(): + an = tmp._find_name(name) + if an is not None: + return an + for s in self.shape: + an = s._find_name(name) + if an is not None: + return an + return None + def _shape_names(self): """Shape information, stringified to be legal names. """ @@ -1357,6 +1394,8 @@ class Array(LazyObjectBase): kernel_args = [] # array args + # FIXME: instead of looping over env, better + # only loop over symbolic caputer lists. for arr_name, varr in self.env.items(): if arr_name in ['cl_ctx', 'cu_ctx']: continue @@ -1370,6 +1409,7 @@ class Array(LazyObjectBase): kernel_args.append( lp.GlobalArg( arr_name, + dtype=self.arguments[arr_name].dtype, shape=self.arguments[arr_name]._shape_str)) elif arr_name in self.bound_arguments.keys(): if self.bound_arguments[arr_name] is None: @@ -1380,11 +1420,13 @@ class Array(LazyObjectBase): kernel_args.append( lp.ValueArg( arr_name, - self.bound_arguments[arr_name].dtype)) + dtype=self.bound_arguments[ + arr_name].dtype)) else: kernel_args.append( lp.GlobalArg( arr_name, + dtype=self.bound_arguments[arr_name].dtype, shape=self.bound_arguments[ arr_name]._shape_str)) elif arr_name in self.temporaries.keys(): @@ -1393,11 +1435,12 @@ class Array(LazyObjectBase): kernel_args.append( lp.ValueArg( arr_name, - self.temporaries[arr_name].dtype)) + dtype=self.temporaries[arr_name].dtype)) else: kernel_args.append( lp.GlobalArg( arr_name, + dtype=self.temporaries[arr_name].dtype, shape=self.temporaries[ arr_name]._shape_str)) else: @@ -1433,20 +1476,10 @@ class Array(LazyObjectBase): for argname, arg in knl.arg_dict.items(): if arg.dtype is None: # array args - if argname == self.name: - if self.dtype is not None: - extra_dtype_dict[argname] = self.dtype - continue - - if argname in self.arguments: - if self.arguments[argname].dtype is not None: - extra_dtype_dict[argname] = self.arguments[argname].dtype - continue - elif argname in self.bound_arguments: - if self.bound_arguments[argname].dtype is not None: - extra_dtype_dict[argname] = \ - self.bound_arguments[argname].dtype - continue + sym_arg = self._find_name(argname) + if sym_arg is not None: + print(argname, arg, arg.dtype, sym_arg.dtype) + extra_dtype_dict[argname] = sym_arg.dtype # shape args if argname in self._shape_names(): @@ -1465,7 +1498,7 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) - if 0: + if 1: print(knl) print(data_map) @@ -1573,6 +1606,7 @@ def to_lappy_array(arr_like, name=None, base_env=None): arr = arr_class(name=name, dtype=dtype, env=base_env, value=arr_like) arr.env[arr.name] = arr_like arr.arguments = dict() + return arr if isinstance(arr_like, np.ndarray): @@ -1596,7 +1630,7 @@ def to_lappy_array(arr_like, name=None, base_env=None): raise ValueError("Cannot convert the input to a Lappy Array.") -def to_lappy_unsigned(unsigned_like, name=None, base_env=None): +def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): """Do nothing if the array is already an :class:`Unsigned`. Make an :class:`Unsigned` with captured values for an actual number. """ @@ -1604,15 +1638,23 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None): base_env = default_env() if unsigned_like is None: - return Unsigned(name=name, env=base_env) + return Unsigned(name=name, env=base_env, dtype=dtype) if np.isscalar(unsigned_like): + try: + unsigned_like = int(unsigned_like) + except ValueError: + # a string of name, not value + return Unsigned(name=unsigned_like, env=base_env, dtype=dtype) + if abs(int(unsigned_like)) - unsigned_like == 0: - lunsigned = Unsigned(name=name, value=unsigned_like, env=base_env) + lunsigned = Unsigned( + name=name, value=unsigned_like, env=base_env, dtype=dtype) lunsigned.env[lunsigned.name] = unsigned_like return lunsigned else: - raise ValueError("%s is not a non-negative integer" % str(unsigned_like)) + raise ValueError( + "%s is not a non-negative integer" % str(unsigned_like)) if isinstance(unsigned_like, Array): assert unsigned_like.ndim == 0 @@ -1623,12 +1665,13 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None): if not isinstance(unsigned_like, Unsigned): unsigned_like = Unsigned( name=unsigned_like.name, value=unsigned_like.value, + dtype=dtype, env=unsigned_like.env.copy()) return unsigned_like if isinstance(unsigned_like, pymbolic.primitives.Expression): - return Unsigned(expr=unsigned_like) + return Unsigned(expr=unsigned_like, dtype=dtype) raise ValueError("Cannot convert the input to a Lappy Unsigned.") @@ -1663,9 +1706,9 @@ def to_lappy_shape(shape): int_c = int(c) if not int_c == abs(int_c): raise ValueError("cannot use %s as a shape parameter" % str(c)) - components[i] = Unsigned(name=None, value=int(c)) + components[i] = to_lappy_unsigned(int(c)) except ValueError: - components[i] = Unsigned(c) + components[i] = to_lappy_unsigned(c) return tuple(components) assert isinstance(shape, (tuple, list)) @@ -1679,17 +1722,7 @@ def to_lappy_shape(shape): "integer" % str(c)) else: - try: - int_c = int(c) - if not int_c == abs(int_c): - raise ValueError("cannot use %s as a shape parameter" % str(c)) - components[i] = Unsigned(name=None, value=int(c)) - except ValueError: - assert isinstance(c, str) - components[i] = Unsigned(c) - except TypeError: - assert isinstance(c, Expression) - components[i] = Unsigned(expr=c) + components[i] = to_lappy_unsigned(c, name=None) return tuple(components) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 88ef369..5f45d13 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -244,21 +244,26 @@ class BroadcastResult(object): brdc_arr.expr = substitute(brdc_arr.expr, expr_mapping) + # {{{ handle information flow from the base arr + + base_arr_passed_through = False + # update argument list if base_arr was there # if there was a self reference, the metadata needs to be captured for argname in base_arr.arguments.keys(): if base_arr.arguments[argname] is None: brdc_arr.arguments[argname] = base_arr.as_stateless() + base_arr_passed_through = True for argname in base_arr.bound_arguments.keys(): if base_arr.bound_arguments[argname] is None: brdc_arr.bound_arguments[argname] = base_arr.as_stateless() + base_arr_passed_through = True - # if not, insert the base arr as an argument - if base_arr.value is None: - brdc_arr.arguments[base_arr.name] = base_arr.as_stateless() - else: - brdc_arr.bound_arguments[base_arr.name] = \ - base_arr.as_stateless() + # if not, insert the base arr as temps + if not base_arr_passed_through: + brdc_arr.temporaries[base_arr.name] = base_arr.as_stateless() + + # }}} End handle information flow from the base arr self.broadcast_arrays.append(brdc_arr) -- GitLab From 420c7436317edf170bd092733035c5d97505469c Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 10 Mar 2020 13:56:21 -0500 Subject: [PATCH 32/59] Remove temps from kernel args --- experiments/finite_difference.py | 22 +++++----------------- lappy/core/array.py | 17 ++--------------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index 6d457f6..12ed422 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -17,7 +17,7 @@ def solve(use_lappy): D = 0.05 nx = 200 - nt = 1 + nt = 2 length = 2. h = length / (nx - 1) @@ -36,6 +36,7 @@ def solve(use_lappy): exp = lap.core.ufuncs.exp def roll(x, y): + # FIXME return x else: @@ -47,7 +48,7 @@ def solve(use_lappy): def exact(time): ex = exp(-4 * np.pi * np.pi * D * time) * ( 0.5 * sin(2 * np.pi * (xx - time))) - return (xx - time) + return ex # initial conditions f = 0.5 * sin(2 * np.pi * xx) @@ -69,21 +70,8 @@ def solve(use_lappy): fe = exact(time) - # print(fe.arguments, fe.env) - fe.env['cl_ctx'] = env['cl_ctx'] - print(fe.expr) - print(fe.arguments.keys()) - print(fe.bound_arguments.keys()) - print(fe.temporaries.keys()) - - for arg in fe.arguments.values(): - print(arg.expr) - - fe.eval() - print(fe.value) - 1/0 - - print(time, np.linalg.norm(f - fe) / np.linalg.norm(fe)) + return f.value + # print(time, np.linalg.norm(f - fe) / np.linalg.norm(fe)) if __name__ == '__main__': diff --git a/lappy/core/array.py b/lappy/core/array.py index 55201ba..2132d62 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1430,19 +1430,7 @@ class Array(LazyObjectBase): shape=self.bound_arguments[ arr_name]._shape_str)) elif arr_name in self.temporaries.keys(): - assert isinstance(self.temporaries[arr_name], Array) - if self.temporaries[arr_name].ndim == 0: - kernel_args.append( - lp.ValueArg( - arr_name, - dtype=self.temporaries[arr_name].dtype)) - else: - kernel_args.append( - lp.GlobalArg( - arr_name, - dtype=self.temporaries[arr_name].dtype, - shape=self.temporaries[ - arr_name]._shape_str)) + continue # do not add temps as kernel args else: pass # warnings.warn( @@ -1478,7 +1466,6 @@ class Array(LazyObjectBase): # array args sym_arg = self._find_name(argname) if sym_arg is not None: - print(argname, arg, arg.dtype, sym_arg.dtype) extra_dtype_dict[argname] = sym_arg.dtype # shape args @@ -1498,7 +1485,7 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) - if 1: + if 0: print(knl) print(data_map) -- GitLab From 2eecb271030ddc99304a44be069185eb05b5d5b3 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 10 Mar 2020 20:31:20 -0500 Subject: [PATCH 33/59] Add lazy arange (need multi-stage eval) --- .gitlab-ci.yml | 2 +- experiments/finite_difference.py | 91 +++++++++++-------- experiments/lazy_arange.py | 18 ++++ lappy/__init__.py | 5 ++ lappy/core/array.py | 74 ++++++++++++++-- lappy/core/function_base.py | 146 +++++++++++++++++++++++++++++++ lappy/core/tools.py | 5 ++ lappy/core/ufuncs.py | 25 ++++-- 8 files changed, 314 insertions(+), 52 deletions(-) create mode 100644 experiments/lazy_arange.py create mode 100644 lappy/core/function_base.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4c09dc7..ee5f898 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ Python 3 POCL: Flake8: script: - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-flake8.sh - - ". ./prepare-and-run-flake8.sh lappy test" + - ". ./prepare-and-run-flake8.sh lappy test experiments" tags: - python3 except: diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index 12ed422..d2c484a 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -12,52 +12,68 @@ import lappy.core.array from lappy import to_lappy_array -def solve(use_lappy): - U = 1. - D = 0.05 - - nx = 200 - nt = 2 - - length = 2. - h = length / (nx - 1) - dt = 1e-4 +def solve_analytic( + nx=200, nt=2, dt=1e-4, + U=1., D=0.05, length=2.): # noqa + """Exact solution. + """ final_time = dt * nt - - print(nx, nt) - + h = length / (nx - 1) xx = np.arange(nx) * h - if use_lappy: - env = lap.core.array.default_env() - env['cl_ctx'] = cl.create_some_context() - xx = to_lappy_array(xx, base_env=env) - sin = lap.core.ufuncs.sin - exp = lap.core.ufuncs.exp - - def roll(x, y): - # FIXME - return x - - else: - sin = np.sin - exp = np.exp - roll = np.roll + sin = np.sin + exp = np.exp - # exact solution def exact(time): ex = exp(-4 * np.pi * np.pi * D * time) * ( 0.5 * sin(2 * np.pi * (xx - time))) return ex + return exact(final_time) + + +def solve_numpy( + nx=200, nt=2, dt=1e-4, + U=1., D=0.05, length=2.): # noqa + + h = length / (nx - 1) + xx = np.arange(nx) * h + # initial conditions - f = 0.5 * sin(2 * np.pi * xx) - fe = exact(0) + f = 0.5 * np.sin(2 * np.pi * xx) for m in range(nt): - print(m) - time = m * dt + f0 = f + fp = np.roll(f0, -1) + fm = np.roll(f0, 1) + + f = ( + f0 + - U * 0.5 * (dt / h) * (fp - fm) + + D * (dt / h**2) * (fp - 2 * f0 + fm) + ) + return f + + +def solve_lappy( + nx=200, nt=2, dt=1e-4, + U=1., D=0.05, length=2.): # noqa + + h = length / (nx - 1) + xx = np.arange(nx) * h + + env = lap.core.array.default_env() + env['cl_ctx'] = cl.create_some_context() + xx = to_lappy_array(xx, base_env=env) + + def roll(x, y): + # FIXME + return x + # initial conditions + f = 0.5 * lap.sin(2 * np.pi * xx) + + for m in range(nt): f0 = f fp = roll(f0, -1) fm = roll(f0, 1) @@ -68,11 +84,10 @@ def solve(use_lappy): + D * (dt / h**2) * (fp - 2 * f0 + fm) ) - fe = exact(time) - - return f.value - # print(time, np.linalg.norm(f - fe) / np.linalg.norm(fe)) + return f if __name__ == '__main__': - solve(use_lappy=True) + fe = solve_analytic() + fn = solve_numpy() + fl = solve_lappy() diff --git a/experiments/lazy_arange.py b/experiments/lazy_arange.py new file mode 100644 index 0000000..5bfa512 --- /dev/null +++ b/experiments/lazy_arange.py @@ -0,0 +1,18 @@ +import pyopencl as cl +from lappy.core.function_base import arange + +ctx = cl.create_some_context() +a = arange(10) + +# pyopencl context +a.env['cl_ctx'] = ctx + +# eval shape first +s = a.shape[0] +s.env = a.env +s.eval() + +# eval arange +a.env[s.name] = s.value.get() +a.eval() +print(a.value) diff --git a/lappy/__init__.py b/lappy/__init__.py index 719687b..8b72597 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -25,3 +25,8 @@ THE SOFTWARE. from lappy.core import ndarray, to_lappy_array __all__ = ["ndarray", "to_lappy_array"] + + +from lappy.core.ufuncs import sin, cos, exp + +__all__ += ["sin", "cos", "exp"] diff --git a/lappy/core/array.py b/lappy/core/array.py index 2132d62..ed460f6 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -138,7 +138,14 @@ class LazyObjectBase(object): if 'value' in kwargs.keys(): self.value = kwargs.pop("value") - self.arguments = kwargs.pop("arguments", {self.name: None}) + if not self.is_stateless: + if self.value is None: + self.arguments = kwargs.pop("arguments", {self.name: None}) + else: + self.arguments = kwargs.pop("arguments", dict()) + self.bound_arguments[self.name] = None + else: + self.arguments = kwargs.pop("arguments", {self.name: None}) def _make_default_name(self): name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) @@ -1240,6 +1247,9 @@ class Array(LazyObjectBase): elif np.isscalar(varr): data_map[arr_name] = varr + elif isinstance(varr, cl.array.Array): + data_map[arr_name] = varr + else: raise RuntimeError("unrecogonized captured variable %s" % arr_name) @@ -1485,7 +1495,7 @@ class Array(LazyObjectBase): # let loopy infer more context first (e.g. shape of input arrays) self._check_preconditions(context=data_map) - if 0: + if 1: print(knl) print(data_map) @@ -1617,6 +1627,44 @@ def to_lappy_array(arr_like, name=None, base_env=None): raise ValueError("Cannot convert the input to a Lappy Array.") +def to_lappy_scalar(scalar_like, name=None, base_env=None, dtype=np.float64): + """Do nothing if the array is already an :class:`Scalar`. + Make an :class:`Scalar` with captured values for an actual number. + """ + if base_env is None: + base_env = default_env() + + if scalar_like is None: + return Scalar(name=name, env=base_env, dtype=dtype) + + if np.isscalar(scalar_like): + try: + scalar_like = float(scalar_like) + return Scalar(value=scalar_like, env=base_env, dtype=dtype) + except ValueError: + # a string of name, not value + assert isinstance(scalar_like, str) + return Scalar(name=scalar_like, env=base_env, dtype=dtype) + + if isinstance(scalar_like, Array): + assert scalar_like.ndim == 0 + assert scalar_like.shape == () + assert scalar_like.value is not None + + if not isinstance(scalar_like, Scalar): + scalar_like = Scalar( + name=scalar_like.name, value=scalar_like.value, + dtype=dtype, + env=scalar_like.env.copy()) + + return scalar_like + + if isinstance(scalar_like, pymbolic.primitives.Expression): + return Scalar(expr=scalar_like, dtype=dtype, env=base_env) + + raise ValueError("Cannot convert the input to a Lappy Scalar.") + + def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): """Do nothing if the array is already an :class:`Unsigned`. Make an :class:`Unsigned` with captured values for an actual number. @@ -1647,18 +1695,24 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): assert unsigned_like.ndim == 0 assert unsigned_like.shape == () assert unsigned_like.is_integral - assert unsigned_like.value is not None if not isinstance(unsigned_like, Unsigned): unsigned_like = Unsigned( - name=unsigned_like.name, value=unsigned_like.value, + name=unsigned_like.name, + value=unsigned_like.value, dtype=dtype, - env=unsigned_like.env.copy()) + env=unsigned_like.env.copy(), + expr=unsigned_like.expr, + arguments=unsigned_like.arguments.copy(), + bound_arguments=unsigned_like.bound_arguments.copy(), + temporaries=unsigned_like.temporaries.copy(), + preconditions=unsigned_like.preconditions + ) return unsigned_like if isinstance(unsigned_like, pymbolic.primitives.Expression): - return Unsigned(expr=unsigned_like, dtype=dtype) + return Unsigned(expr=unsigned_like, dtype=dtype, env=base_env) raise ValueError("Cannot convert the input to a Lappy Unsigned.") @@ -1737,4 +1791,12 @@ def get_internal_name_prefix(name): name_prefix = '__' return name_prefix + +def make_default_name(obj_class): + """Make a default name for the lazy object class. + """ + name = '%s%d' % (obj_class._name_prefix, obj_class._counter) + obj_class._counter += 1 + return name + # }}} End misc utils diff --git a/lappy/core/function_base.py b/lappy/core/function_base.py new file mode 100644 index 0000000..8514a41 --- /dev/null +++ b/lappy/core/function_base.py @@ -0,0 +1,146 @@ +from __future__ import division, absolute_import, print_function + +import numpy as np +from lappy.core.array import ( + default_env, + LazyObjectBase, Array, Scalar, Unsigned, + to_lappy_scalar, to_lappy_unsigned) +from lappy.core.ufuncs import power, floor +from lappy.core.tools import ( + check_and_merge_envs, check_and_merge_args, + check_and_merge_bargs, check_and_merge_temps, + check_and_merge_precs) + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +__all__ = ["arange", "linspace", "logspace"] + + +def arange(start, stop=None, step=1, dtype=None, + name=None, base_env=None): + """Return evenly spaced values within a given interval. + Behaves like :func:`numpy.arange`. + + ``` + numpy.arange([start, ]stop, [step, ]dtype=None) + ``` + + :param start: Start of interval. The interval includes this value. + The default start value is 0. + :param stop: End of interval. The interval does not include this value, + :param step: Spacing between values. The default step size is 1. + :param dtype: The type of the output array. + + :param name: Name of the output. + """ + if stop is None: # the pos 1 arg is treated as stop + stop = start + start = 0 + + if base_env is None: + base_env = default_env() + + start = to_lappy_scalar(start) + stop = to_lappy_scalar(stop) + step = to_lappy_scalar(step) + + obj = dict() + obj['ndim'] = 1 + obj['env'] = check_and_merge_envs(start, stop, step) + obj['arguments'] = check_and_merge_args(start, stop, step) + obj['bound_arguments'] = check_and_merge_bargs(start, stop, step) + obj['temporaries'] = check_and_merge_temps(start, stop, step) + obj['preconditions'] = check_and_merge_precs(start, stop, step) + + if name is not None: + obj['name'] = name + + num = to_lappy_unsigned( + floor((stop - start) / step)) + obj['shape'] = (num, ) + + if dtype is None: + dtype = np.result_type(start, stop, step) + obj['dtype'] = dtype + + arr = Array(**obj) + iname = arr.inames[0] + arr.expr = start.expr + step.expr * iname + + return arr + + +def linspace(start, stop, num=50, endpoint=True, retstep=False, + name=None, dtype=None): + """Return evenly spaced numbers over a specified interval. + Behaves like :func:`numpy.linspace`. + + :param start: The starting (Scalar) value. + :param stop: The end (Scalar) value. + :param num: Number of samples to generate. Default is 50. + :param endpoint: Whether the last point at ``stop`` is included. + :param retstep: If True, return the spacing as well. + + :param name: Name of the output. Auto-generate the name if None. + :param dtype: Data type of the output. + """ + if isinstance(endpoint, LazyObjectBase): + raise TypeError("boolean value 'endpoint' cannot be lazy.") + if isinstance(retstep, LazyObjectBase): + raise TypeError("boolean value 'retstep' cannot be lazy.") + if not isinstance(start, Scalar): + start = to_lappy_scalar(start) + if not isinstance(stop, Scalar): + stop = to_lappy_scalar(stop) + if not isinstance(num, Unsigned): + num = to_lappy_unsigned(stop) + + if endpoint: + if num == 1: + step = 0. + else: + step = (stop - start) / (num - 1) + y = arange(0, num) * step + start + y[-1] = stop + else: + step = (stop - start) / num + y = arange(0, num) * step + start + + if retstep: + return y, step + else: + return y + + +def logspace(start, stop, num=50, endpoint=True, base=10.0): + """Return numbers spaced evenly on a log scale. + Behaves like :func:`numpy.logspace`. + + :param start: The starting (Scalar) value. + :param stop: The end (Scalar) value. + :param num: Number of samples to generate. Default is 50. + :param endpoint: Whether the last point at ``stop`` is included. + :param base: The base of the log space. Default is 10.0. + """ + y = linspace(start, stop, num=num, endpoint=endpoint) + return power(base, y) diff --git a/lappy/core/tools.py b/lappy/core/tools.py index 5368a0c..86d26d1 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -24,6 +24,11 @@ THE SOFTWARE. import numpy as np from lappy.core.array import LazyObjectBase +__all__ = ["check_and_merge_envs", "check_and_merge_args", + "check_and_merge_bargs", "check_and_merge_temps", + "check_and_merge_precs", + "is_nonnegative_int", "is_constexpr"] + def check_and_merge_envs(*arr_list): """Merge captured environments. diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 9fd4172..2b0b28d 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -51,6 +51,10 @@ class UFunc(object): whether the output is integral for integral inputs. + .. attribute:: is_integral + + whether the output is always integral (regardless of inputs). + """ nargs = -1 @@ -93,10 +97,12 @@ class UFunc(object): return self.__call__(*args, **kwargs) - def __init__(self, expr_func, dtype=None, preserve_integral=False): + def __init__(self, expr_func, dtype=None, + preserve_integral=False, is_integral=False): self.dtype = dtype self.f = expr_func self.preserve_integral = preserve_integral + self.is_integral = is_integral def __call__(self, *args, **kwargs): raise NotImplementedError() @@ -136,7 +142,8 @@ class UnaryOperation(UFunc): 'temporaries': new_interm, 'ndim': a.ndim, 'shape': a.shape, 'dtype': new_dtype, - 'is_integral': a.is_integral and self.preserve_integral, + 'is_integral': + (a.is_integral and self.preserve_integral) or self.is_integral, 'assumptions': new_assumptions, } arr = Array(**obj) @@ -320,8 +327,9 @@ class BinaryOperation(UFunc): 'preconditions': a.preconditions + b.preconditions + bres.preconditions, 'ndim': bres.ndim, 'shape': bres._shape_exprs, 'dtype': new_dtype, - 'is_integral': all([ - a.is_integral, b.is_integral, self.preserve_integral]), + 'is_integral': + all([a.is_integral, b.is_integral, self.preserve_integral]) + or self.is_integral, 'assumptions': new_assumptions, } arr = Array(**obj) @@ -364,9 +372,12 @@ abs = absolute = UnaryOperation(lambda x: var('abs')(x), preserve_integral=True) fabs = UnaryOperation(lambda x: var('fabs')(x)) negative = UnaryOperation(lambda x: -x, preserve_integral=True) -floor = UnaryOperation(lambda x: var('floor')(x), preserve_integral=True) -ceil = UnaryOperation(lambda x: var('ceil')(x), preserve_integral=True) -around = UnaryOperation(lambda x: var('around')(x), preserve_integral=True) +floor = UnaryOperation(lambda x: var('floor')(x), + preserve_integral=True, is_integral=True) +ceil = UnaryOperation(lambda x: var('ceil')(x), + preserve_integral=True, is_integral=True) +around = UnaryOperation(lambda x: var('around')(x), + preserve_integral=True, is_integral=True) logical_not = UnaryOperation(lambda x: x.not_()) -- GitLab From 0effb0a5afb56eedc111760ead241cd5527b1be8 Mon Sep 17 00:00:00 2001 From: xywei Date: Thu, 19 Mar 2020 22:40:16 -0500 Subject: [PATCH 34/59] Rename assumptions to integer_domain --- lappy/core/array.py | 68 +++++++++++++++++++++++++++------------ lappy/core/basic_ops.py | 6 ++-- lappy/core/broadcast.py | 2 +- lappy/core/conditional.py | 8 ++--- lappy/core/ufuncs.py | 8 ++--- 5 files changed, 60 insertions(+), 32 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index ed460f6..280b3c5 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -58,6 +58,18 @@ class PreconditionNotMetError(Exception): class IndexError(Exception): pass + +class LazyObjectTag(object): + """Meta-data for stateless lazy objects in a namespace. + + .. attribute:: is_argument + + True if the object is an input parameter (not a temporary). + """ + def __init__(self, is_argument): + self.is_argument = is_argument + + # {{{ base lazy obj class @@ -106,10 +118,28 @@ class LazyObjectBase(object): .. attribute:: env - the captured environment, a :class:`dict`. The environment must be - fully known before runtime, i.e., cannot contain lazy objects. + the captured environment, a :class:`dict`. The environment is + fully known before runtime, i.e., does not contain lazy objects. + + If env is None, the object is called **stateless**. Stateless objects serve + as parts of a lazy object with state, e.g., as the input arguments. - If env and value are both None, the object is called **stateless**. + .. attribute:: namespace + + a dictionary with names as keys, and tuple of + ``` + (stateless_lazy_object, object_desc) + ``` + as values, where ``object_desc`` is an instance of :class:`LazyObjectTag`. + + The namespace associated with a lazy object contains all + lazy objects involved during the construction of itself. Stateless + lazy objects' namespaces should be None. + + In particular, to avoid duplications, ``self.namespace[self.name] = None``. + + TODO: refactor various namespaces (arguments, bound_arguments, temporaries) + to all wrappers around a single namespace. .. attribute:: preconditions @@ -301,10 +331,11 @@ class Array(LazyObjectBase): a :class:`bool` indicating whether array elements are integers - .. attribute:: assumptions + .. attribute:: integer_domain - assumptions represented as an isl set, well-defined only if - is_integral is True + an ISL set if is_integral is True, otherwise None. The set represents + the domain for the full array. It is used to express conditions like + non-negative integers, multiples of two, etc. """ _counter = 0 _name_prefix = '__lappy_array_' @@ -358,11 +389,10 @@ class Array(LazyObjectBase): self.dtype = kwargs.pop("dtype", None) self.is_integral = kwargs.pop("is_integral", False) - self.assumptions = kwargs.pop("assumptions", None) - - # only integer arrays can have assumptions - if self.assumptions is not None: - assert self.is_integral + if self.is_integral: + self.integer_domain = kwargs.pop("integer_domain", None) + else: + self.integer_domain = None # }}} End constructor @@ -372,10 +402,10 @@ class Array(LazyObjectBase): """Returns a new copy of self, where the attributes undergo a shallow copy (expect for the value). """ - if self.assumptions is None: - cp_assumptions = None + if self.integer_domain is None: + cp_integer_domain = None else: - cp_assumptions = self.assumptions.copy() + cp_integer_domain = self.integer_domain.copy() if stateless: env = None @@ -393,7 +423,7 @@ class Array(LazyObjectBase): temporaries=self.temporaries.copy(), env=env, preconditions=list(self.preconditions), - assumptions=cp_assumptions, + integer_domain=cp_integer_domain, ) def with_state(self, env): @@ -991,7 +1021,7 @@ class Array(LazyObjectBase): 'shape': self.shape, 'dtype': self.dtype, 'is_integral': self.is_integral, - 'assumptions': self.assumptions}) + 'integer_domain': self.integer_domain}) return repr_dict def _shadow_size(self, axes=None): @@ -1399,7 +1429,7 @@ class Array(LazyObjectBase): # Step 5 # Make and run loopy kernel # - # FIXME: collect all assumptions and pass to loopy + # FIXME: collect all integer_domain and pass to loopy # kernel_args = [] @@ -1565,7 +1595,7 @@ class Unsigned(Int): if name is not None: kwargs['name'] = name super(Unsigned, self).__init__(**kwargs) - self.assumptions = isl.BasicSet.read_from_str( + self.integer_domain = isl.BasicSet.read_from_str( isl.DEFAULT_CONTEXT, "{ [%s] : %s >= 0 }" % ((self.name, ) * 2)) @@ -1729,8 +1759,6 @@ def to_lappy_shape(shape): Unsigned objects. - Comma/space-separated string. - Nonnegativity assumption is added to the input where the shape is - parametrized. """ if shape is None: return tuple() diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index aefac9c..ea71cf0 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -27,7 +27,7 @@ from numpy import AxisError from lappy.core.ufuncs import minimum, maximum from lappy.core.conditional import conditional from lappy.core.array import ( - Array, Unsigned, to_lappy_shape, to_lappy_unsigned, + Array, Int, Unsigned, to_lappy_shape, to_lappy_unsigned, isscalar, get_internal_name_prefix) from lappy.core.preconditions import ( EvaluationPrecondition, @@ -268,7 +268,7 @@ class SubArrayFactory(object): 'shape': new_shape, 'dtype': self.base_arr.dtype, 'is_integral': self.base_arr.is_integral, - 'assumptions': self.base_arr.assumptions, + 'integer_domain': self.base_arr.integer_domain, } arr = Array(**obj) @@ -321,7 +321,7 @@ class SubArrayFactory(object): if self.ndim == 0: raise TypeError("invalid index to scalar variable") raise NotImplementedError() - elif isinstance(idx, Unsigned): + elif isinstance(idx, Int): # symbolic indexing if self.ndim == 0: raise TypeError("invalid index to scalar variable") diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 5f45d13..5795878 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -232,7 +232,7 @@ class BroadcastResult(object): # account for semi-dynamic broadcasting, i.e., passing shape value 1 # at runtime, by fetching from (out_iname % in_shape). # TODO: when it is known that such broadcasting cannot happen, this - # may be optimized out in loopy by imposing assumptions like + # may be optimized out in loopy by imposing constraints like # out_iname < in_shape expr_mapping = {} for inm, new_inm, in_shape_i in zip( diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index 869a6b9..5d90879 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -131,12 +131,12 @@ def conditional(cond, con, alt, name=None): obj['is_integral'] = con.is_integral & alt.is_integral if not obj['is_integral']: - obj['assumptions'] = None + obj['integer_domain'] = None else: - if con.assumptions is None and alt.assumptions is None: - obj['assumptions'] = None + if con.integer_domain is None and alt.integer_domain is None: + obj['integer_domain'] = None else: - # FIXME: compute the intersection of assumptions of con and alt + # FIXME: compute the intersection of integer_domain of con and alt raise NotImplementedError() return arr_class(**obj) diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 2b0b28d..cf2612a 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -114,7 +114,7 @@ class UnaryOperation(UFunc): nargs = 1 def __call__(self, a, name=None): - new_assumptions = None + new_integer_domain = None if not isinstance(a, Array): a = to_lappy_array(a) @@ -144,7 +144,7 @@ class UnaryOperation(UFunc): 'dtype': new_dtype, 'is_integral': (a.is_integral and self.preserve_integral) or self.is_integral, - 'assumptions': new_assumptions, + 'integer_domain': new_integer_domain, } arr = Array(**obj) @@ -314,7 +314,7 @@ class BinaryOperation(UFunc): # expression handling new_expr = self.f(a.expr, b.expr) - new_assumptions = None + new_integer_domain = None obj = { 'name': name, 'inames': a.inames, @@ -330,7 +330,7 @@ class BinaryOperation(UFunc): 'is_integral': all([a.is_integral, b.is_integral, self.preserve_integral]) or self.is_integral, - 'assumptions': new_assumptions, + 'integer_domain': new_integer_domain, } arr = Array(**obj) -- GitLab From 8f8abdb448b7147c32dde9148db1a97488ae937a Mon Sep 17 00:00:00 2001 From: xywei Date: Thu, 19 Mar 2020 23:56:38 -0500 Subject: [PATCH 35/59] Towards unification of namespaces [skip-ci] --- lappy/core/array.py | 276 ++++++++++++++++++-------------------------- 1 file changed, 112 insertions(+), 164 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 280b3c5..5035bf2 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -59,24 +59,13 @@ class IndexError(Exception): pass -class LazyObjectTag(object): - """Meta-data for stateless lazy objects in a namespace. - - .. attribute:: is_argument - - True if the object is an input parameter (not a temporary). - """ - def __init__(self, is_argument): - self.is_argument = is_argument - - # {{{ base lazy obj class class LazyObjectBase(object): """Base class for objects that are lazily evaluated. - Lazy objects are identified by their names. If not otherwise - specified, a unique name is generated on construction. + Lappy's lazy objects (Lapjects) are identified by their names. + If not otherwise specified, a unique name is generated. .. attribute:: name @@ -95,27 +84,6 @@ class LazyObjectBase(object): the actual data. None if not yet evaluated. - .. attribute:: arguments - - the formal argument list, a :class:`dict`, with keys :class:`str` - of lazy object names and values the corresponding lazy objects - that are stateless and whose values are not computable. - - As a special case, when the argument means "self", to avoid infinite - recursion, the value should be set to None. - - .. attribute:: bound_arguments - - the arguments that are bound to an input variables (e.g. a numpy - array). The data is looked up in the captured environment by its - name (bound arguments are also stateless). - - .. attribute:: temporaries - - (conceptual) intermediate lazy objects (objects produced in the - process of making the array expression). Those objects should be stored - as stateless and shares the same env as the current array. - .. attribute:: env the captured environment, a :class:`dict`. The environment is @@ -127,55 +95,78 @@ class LazyObjectBase(object): .. attribute:: namespace a dictionary with names as keys, and tuple of - ``` - (stateless_lazy_object, object_desc) - ``` - as values, where ``object_desc`` is an instance of :class:`LazyObjectTag`. + + ``(stateless_lazy_object, object_desc)`` + + as values, where ``object_desc`` is a dictionary of tags. Sample tags are: + + ``is_argument``: whether the object is an input argument. + ``is_bound``: whether the object's value is known. The namespace associated with a lazy object contains all lazy objects involved during the construction of itself. Stateless lazy objects' namespaces should be None. - In particular, to avoid duplications, ``self.namespace[self.name] = None``. + In particular, to avoid duplications, ``self.namespace[self.name][0] = None``. - TODO: refactor various namespaces (arguments, bound_arguments, temporaries) - to all wrappers around a single namespace. + Besides the input arguments, the namespace can also contain temporaries + produced in the process of assembling the expression. Those temporaries do + not translate to actual temporary variable in the kernel. But they could + contain useful information otherwise lost, e.g. user-specified names of the + intermediate results. .. attribute:: preconditions preconditions are checked at runtime, prior to calling the kernel. It is a list of callables, each of which can take one or more - arguments/bound_arguments. If a precondition fails, an - :class:`Exception` is thrown (by either the checker - function itself, or by returning False from the checker). + input arguments. If a precondition fails, an + :class:`Exception` is thrown prior to calling the kernel. """ _counter = 0 _name_prefix = '__lappy_object_' def __init__(self, **kwargs): - """The constructor is not intended for user invokation.""" + """Constructor. + + :param name: (optional) name of the lapject + :param expr: the expression + :param env: (optional) captured environment + :param namespace: (optional) list of symbols + :param value: (optional) value of the lapject + :param preconditions: dynamic pre-runtime checks + """ + self.name = kwargs.pop("name", None) self.expr = kwargs.pop("expr") - self.bound_arguments = kwargs.pop("bound_arguments", dict()) - self.temporaries = kwargs.pop("temporaries", dict()) - self.env = kwargs.pop("env", default_env()) - self.preconditions = kwargs.pop("preconditions", list()) if self.name is None: self.name = self._make_default_name() - # only set value when given (which overwrites the value given in env) - if 'value' in kwargs.keys(): - self.value = kwargs.pop("value") + self.namespace = kwargs.pop("namespace", None) + self.env = kwargs.pop("env", None) - if not self.is_stateless: - if self.value is None: - self.arguments = kwargs.pop("arguments", {self.name: None}) + # if either namespace or env is set, the obj has state + if self.namespace is None: + if self.env is not None: + self.namespace = {self.name: (None, dict())} + + if self.env is None: + if self.namespace is not None: + self.env = default_env() + + if 'value' in kwargs.keys(): + if self.is_stateless: + raise TypeError("cannot construct a stateless obj with value") + elif self.value is not None: + if self.value != kwargs['value']: + raise ValueError( + "got conflicting values set from env and value") + else: + pass else: - self.arguments = kwargs.pop("arguments", dict()) - self.bound_arguments[self.name] = None - else: - self.arguments = kwargs.pop("arguments", {self.name: None}) + self.value = kwargs.pop("value") + + self.preconditions = kwargs.pop("preconditions", list()) def _make_default_name(self): name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) @@ -249,11 +240,23 @@ class LazyObjectBase(object): @property def is_stateless(self): - return self.env is None + return (self.env is None and self.namespace is None) @property def has_state(self): - return self.env is not None + return (self.env is not None and self.namespace is not None) + + @property + def arguments(self): + """The input argument list. + """ + if self.is_stateless: + raise TypeError("stateless lapject has no arguments") + return { + key: val + for key, val[0] in self.namespace.items() + if val[1].get('is_argument', False) + } @property def value(self): @@ -275,9 +278,7 @@ class LazyObjectBase(object): return {'class': self.__class__, 'name': self.name, 'expr': self.expr, - 'arguments': self.arguments, - 'bound_arguments': self.bound_arguments, - 'temporaries': self.temporaries, + 'namespace': self.namespace, 'env': self.env} # }}} End base lazy obj class @@ -343,12 +344,19 @@ class Array(LazyObjectBase): # {{{ constructor def __init__(self, name=None, shape=None, domain_expr=None, **kwargs): - # default expr + """Constructor. + """ if 'expr' not in kwargs: - kwargs['expr'] = None + kwargs['expr'] = None # will make a default expr if name is not None: kwargs['name'] = name + + # Array shall always be constructed as stateful + # (may be converted to stateless later) + if 'env' not in kwargs or kwargs['env'] is None: + kwargs['env'] = default_env() + super(Array, self).__init__(**kwargs) self.domain_expr = domain_expr @@ -356,7 +364,6 @@ class Array(LazyObjectBase): if ndim is None and shape is not None: # infer ndim from given shape - # NOTE: use fixed ndim only shape = to_lappy_shape(shape) ndim = int(len(shape)) @@ -379,7 +386,7 @@ class Array(LazyObjectBase): self.shape = tuple(s.as_stateless() for s in lapshape) for s in lapshape: if (not s.is_stateless) and (s.value is not None): - if s.name in self.env: + if self.has_state and s.name in self.env: warnings.warn("captured value for %s is overwritten " "(%s --> %s)" % ( s.name, @@ -409,8 +416,10 @@ class Array(LazyObjectBase): if stateless: env = None + nms = None else: env = self.env.copy() + nms = self.namespace.copy() return self.__class__( name=self.name, ndim=self.ndim, @@ -418,10 +427,8 @@ class Array(LazyObjectBase): shape=self.shape, dtype=self.dtype, expr=self.expr, is_integral=self.is_integral, domain_expr=self.domain_expr, - arguments=self.arguments.copy(), - bound_arguments=self.bound_arguments.copy(), - temporaries=self.temporaries.copy(), env=env, + namespace=nms, preconditions=list(self.preconditions), integer_domain=cp_integer_domain, ) @@ -441,18 +448,18 @@ class Array(LazyObjectBase): def with_name(self, name, rename_arguments=False): """Rename the array object. Returns a new (shallow) copy of self with the new name. + + :param rename_arguments: If true, all occurrences of the old name will + be replaced. Otherwise, the new array takes the old array as an + arguments and its value is assigned to the new name. """ new_arr = self.copy() if name == self.name or name is None: warnings.warn("name is unchanged, just making a copy") return new_arr - if name in self.arguments: - raise ValueError("Name %s is already taken by an argument" % name) - elif name in self.bound_arguments: - raise ValueError("Name %s is already taken by a (bound) argument" % name) - elif name in self.temporaries: - raise ValueError("Name %s is already taken by a temporary" % name) + if name in self.namespace: + raise ValueError("Name %s is already taken" % name) if name in self.env: raise ValueError( @@ -463,21 +470,13 @@ class Array(LazyObjectBase): # when the argument is self-referencing if rename_arguments: - if self.name in self.arguments: - new_arr.arguments[self.name] = \ - new_arr.arguments.pop(self.name) - elif self.name in self.bound_arguments: - new_arr.bound_arguments[self.name] = \ - new_arr.bound_arguments.pop(self.name) + new_arr.namespace[name] = \ + new_arr.namespace.pop(self.name) new_arr.expr = pymbolic.substitute(new_arr.expr, {var(self.name): var(name)}) else: - if self.name in self.arguments: - new_arr.arguments.pop(self.name) - new_arr.arguments[self.name] = self.as_stateless() - elif self.name in self.bound_arguments: - new_arr.bound_arguments.pop(self.name) - new_arr.bound_arguments[self.name] = self.as_stateless() + new_arr.namespace.pop(self.name) + new_arr.namespace[self.name] = self.as_stateless() new_arr.name = name return new_arr @@ -588,25 +587,27 @@ class Array(LazyObjectBase): new_name = 'bound%d_' % self.__class__._counter + self.name for key in kwargs.keys(): - if key not in self.arguments.keys(): - if key in self.bound_arguments.keys(): - # bounding cannot be undone - # (since un-inference is not possible) - raise ValueError("argument named %s is already bound" - % key) + if key not in self.namespace: + raise ValueError("cannot bind an undefined name: %s" % key) - raise ValueError("argument named %s is not accepted" % key) + if key in self.env(): + # bounding cannot be undone + # (since un-inference is not possible) + raise ValueError("argument named %s is already bound" + % key) + + if not self.namespace[key][1].get('is_argument', False): + raise ValueError("trying to bind data to a non-argument") new_arr = self.copy() new_arr.name = new_name for key, val in kwargs.items(): - new_arr.bound_arguments[key] = new_arr.arguments.pop(key) if hasattr(val, 'dtype'): if key == self.name: new_arr.dtype = val.dtype else: - new_arr.bound_arguments[key].dtype = val.dtype + new_arr.namespace[key][0].dtype = val.dtype if hasattr(val, 'shape'): new_arr._set_shape_values(val.shape, key) if hasattr(val, 'ndim'): @@ -614,7 +615,7 @@ class Array(LazyObjectBase): assert new_arr.ndim == val.ndim assert len(new_arr.shape) == val.ndim else: - assert new_arr.bound_arguments[key].ndim == val.ndim + assert new_arr.namespace[key][1].ndim == val.ndim assert len(val.shape) == val.ndim new_arr.env[key] = val # actual data @@ -1080,68 +1081,6 @@ class Array(LazyObjectBase): expr=var(names[d])) for d in range(ndim)) - def _has_name(self, name): - """Check whether a given name is taken by one of the following: - - - self.name - - self.inames - - self.arguments - - self.bound_arguments - - self.temporaries - - self.env - """ - if isinstance(name, Variable): - name = name.name - else: - assert isinstance(name, str) - has_name = ( - (name == self.name) - or (var(name) in self.inames) - or (name in self.arguments.keys()) - or (name in self.bound_arguments.keys()) - or (name in self.temporaries.keys()) - or (name in self.env.keys()) - ) - return has_name - - def _find_name(self, name): - """Find the symbol by name. Returns None if not found. - """ - if isinstance(name, Variable): - name = name.name - else: - assert isinstance(name, str) - if name == self.name: - return self - elif name in self.arguments: - return self.arguments[name] - elif name in self.bound_arguments: - return self.bound_arguments[name] - elif name in self.temporaries: - return self.temporaries[name] - else: - for arg in self.arguments.values(): - if arg is None: - continue - an = arg._find_name(name) - if an is not None: - return an - for barg in self.bound_arguments.values(): - if barg is None: - continue - an = barg._find_name(name) - if an is not None: - return an - for tmp in self.temporaries.values(): - an = tmp._find_name(name) - if an is not None: - return an - for s in self.shape: - an = s._find_name(name) - if an is not None: - return an - return None - def _shape_names(self): """Shape information, stringified to be legal names. """ @@ -1703,18 +1642,26 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): base_env = default_env() if unsigned_like is None: - return Unsigned(name=name, env=base_env, dtype=dtype) + return Unsigned( + name=name, env=base_env, + namespace={name: (None, {'is_argument': True})}, + dtype=dtype) if np.isscalar(unsigned_like): try: unsigned_like = int(unsigned_like) except ValueError: # a string of name, not value - return Unsigned(name=unsigned_like, env=base_env, dtype=dtype) + return Unsigned( + name=unsigned_like, env=base_env, + namespace={name: (None, {'is_argument': True})}, + dtype=dtype) if abs(int(unsigned_like)) - unsigned_like == 0: lunsigned = Unsigned( - name=name, value=unsigned_like, env=base_env, dtype=dtype) + name=name, value=unsigned_like, env=base_env, + namespace={name: (None, {'is_argument': True})}, + dtype=dtype) lunsigned.env[lunsigned.name] = unsigned_like return lunsigned else: @@ -1732,6 +1679,7 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): value=unsigned_like.value, dtype=dtype, env=unsigned_like.env.copy(), + namespace=unsigned_like.namespace.copy(), expr=unsigned_like.expr, arguments=unsigned_like.arguments.copy(), bound_arguments=unsigned_like.bound_arguments.copy(), -- GitLab From 9a987366d6b234a03d705909fe7ff4a8afa3cabc Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 20 Mar 2020 22:33:02 -0500 Subject: [PATCH 36/59] Towards unification of namespaces [skip-ci] --- lappy/core/array.py | 302 +++++++++++++++++++++++++--------------- lappy/core/broadcast.py | 29 +--- 2 files changed, 194 insertions(+), 137 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 5035bf2..c211cb3 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -3,7 +3,6 @@ from __future__ import division, absolute_import, print_function import warnings import logging from traceback import format_list, extract_stack -from itertools import chain import pyopencl as cl @@ -107,7 +106,8 @@ class LazyObjectBase(object): lazy objects involved during the construction of itself. Stateless lazy objects' namespaces should be None. - In particular, to avoid duplications, ``self.namespace[self.name][0] = None``. + In particular, to avoid duplications, ``None`` is used wherever infinite + recursion occurs, e.g. ``self.namespace[self.name][0] = None``. Besides the input arguments, the namespace can also contain temporaries produced in the process of assembling the expression. Those temporaries do @@ -125,6 +125,8 @@ class LazyObjectBase(object): _counter = 0 _name_prefix = '__lappy_object_' + # {{{ constructor + def __init__(self, **kwargs): """Constructor. @@ -135,7 +137,7 @@ class LazyObjectBase(object): :param value: (optional) value of the lapject :param preconditions: dynamic pre-runtime checks """ - + self.name = kwargs.pop("name", None) self.expr = kwargs.pop("expr") @@ -155,7 +157,7 @@ class LazyObjectBase(object): self.env = default_env() if 'value' in kwargs.keys(): - if self.is_stateless: + if self.is_stateless: raise TypeError("cannot construct a stateless obj with value") elif self.value is not None: if self.value != kwargs['value']: @@ -168,10 +170,90 @@ class LazyObjectBase(object): self.preconditions = kwargs.pop("preconditions", list()) + # }}} End constructor + + # {{{ namespace/env utils + + def _decl(self, name, lapject, tags=None): + """Add symbol to the namespace. + """ + assert isinstance(name, str) + assert isinstance(lapject, LazyObjectBase) + + if name in self.namespace: + raise ValueError("name %s is taken" % name) + + if tags is None: + tags = dict() + else: + assert isinstance(tags, dict) + + self.namespace[name] = (lapject, tags) + + def _meld_namespace(self, namespace, stateless_self=None): + """Merge with another namespace. The other namespace may contain a + self-reference (reference to the name containing the namespace). + If so, the ``stateless_self`` is used to substitute ``None``. + + If there are multiple definitions of a name, the function will + try to merge the information from all sources. An exception is + raised if conflicting information is found. + """ + handled_selref = False + for nm, (lapject, tags) in namespace.items(): + if lapject is None: + if stateless_self is None: + raise ValueError("a stateless copy of the enclosing " + "lapject is needed when importing " + "a namespace with self-reference") + if handled_selref: + raise ValueError("illegal namespace (contains more " + "than one self-references)") + obj = stateless_self + handled_selref = True + else: + obj = lapject + + if nm in self.namespace: # try to merge information + raise NotImplementedError() + else: + self._decl(nm, obj, tags) + + def _meld_env(self, env): + """Merge with another env. + """ + for key, val in env.items(): + if key in self.env: + if self.env[key] is val: + pass # same value + elif val is None: + pass + elif self.env[key] is None: + self.env[key] = val + else: + raise ValueError( + "Trying to merge environments with conflicting " + "definitions of %s" % key) + else: + self.env[key] = val + + # }}} End namespace/env utils + + def __getitem__(self, member_name): + """Given a name, returns the lapject from the namespace, with the state + attached. + """ + assert isinstance(member_name, str) + if member_name not in self.namespace: + raise ValueError("name %s not found" % member_name) + if member_name == self.name: + return self + lapject = self.namespace[member_name][0].with_state( + env=self.env.copy(), namespace=self.namespace.copy()) + return lapject + def _make_default_name(self): - name = '%s%d' % (self.__class__._name_prefix, self.__class__._counter) - self.__class__._counter += 1 - return name + return make_default_name(self.__class__) def __str__(self): """Displays the value if available, otherwise display the @@ -253,9 +335,36 @@ class LazyObjectBase(object): if self.is_stateless: raise TypeError("stateless lapject has no arguments") return { - key: val - for key, val[0] in self.namespace.items() - if val[1].get('is_argument', False) + key: val[0] + for key, val in self.namespace.items() + if val[1].get('is_argument', False) and ( + key not in self.env) + } + + @property + def bound_arguments(self): + """The arguments that are bound to data. + """ + if self.is_stateless: + raise TypeError("stateless lapject has no arguments") + return { + key: val[0] + for key, val in self.namespace.items() + if val[1].get('is_argument', False) and ( + key in self.env) + } + + @property + def temporaries(self): + """The temporaries (internal nodes of the computation graph). + """ + if self.is_stateless: + raise TypeError("stateless lapject has no temporaries") + + return { + key: val[0] + for key, val in self.namespace.items() + if not val[1].get('is_argument', False) } @property @@ -370,6 +479,11 @@ class Array(LazyObjectBase): if ndim is None: raise ValueError("ndim cannot be determined") + try: + ndim = int(ndim) + except ValueError: + raise ValueError('ndim must be a known non-negative integer') + if ndim > 0: self.inames = kwargs.pop("inames", self._make_default_inames(ndim)) else: @@ -380,19 +494,15 @@ class Array(LazyObjectBase): if shape is None: lapshape = self._make_default_shape(ndim) - self.shape = tuple(s.as_stateless() for s in lapshape) + for s in lapshape: + self._decl(s.name, s, {'is_argument': True}) + self.shape = tuple(s.name for s in lapshape) else: lapshape = to_lappy_shape(shape) - self.shape = tuple(s.as_stateless() for s in lapshape) + self.shape = tuple(s.name for s in lapshape) for s in lapshape: - if (not s.is_stateless) and (s.value is not None): - if self.has_state and s.name in self.env: - warnings.warn("captured value for %s is overwritten " - "(%s --> %s)" % ( - s.name, - str(self.env[s.name]), - str(s.value))) - self.env[s.name] = s.value + self._meld_namespace(s.namespace, s.as_stateless()) + self._meld_env(s.env) self.dtype = kwargs.pop("dtype", None) self.is_integral = kwargs.pop("is_integral", False) @@ -408,13 +518,15 @@ class Array(LazyObjectBase): def copy(self, stateless=False): """Returns a new copy of self, where the attributes undergo a shallow copy (expect for the value). + + Note: the copy operation does not make copies of the captured array data. """ if self.integer_domain is None: cp_integer_domain = None else: cp_integer_domain = self.integer_domain.copy() - if stateless: + if stateless or self.is_stateless: env = None nms = None else: @@ -433,11 +545,12 @@ class Array(LazyObjectBase): integer_domain=cp_integer_domain, ) - def with_state(self, env): + def with_state(self, namespace=None, env=None): """Swap out the captured state. """ sarr = self.as_stateless() sarr.env = env + sarr.namespace = namespace return sarr def as_stateless(self): @@ -476,7 +589,8 @@ class Array(LazyObjectBase): {var(self.name): var(name)}) else: new_arr.namespace.pop(self.name) - new_arr.namespace[self.name] = self.as_stateless() + new_arr.namespace[self.name] = ( + self.as_stateless(), self.namespace[self.name][1]) new_arr.name = name return new_arr @@ -533,48 +647,18 @@ class Array(LazyObjectBase): return expr.__class__.init_arg_names == ('name',) assert allow_complex_expr or all( - is_trivial(s.expr) for s in self.shape) + is_trivial(self[s].expr) for s in self.shape) if new_name is None: new_name = 'bound%d_' % self.__class__._counter + self.name new_arr = self.with_name(new_name) - delta_dict = {} - for s, s_val in zip(new_arr.shape, shape_data): - if s_val is None: - continue + for s, sv in zip(self.shape, shape_data): + if s in new_arr.env and new_arr.env[s] != sv: + raise ValueError('found pre-existing shape data') else: - assert isinstance(s_val, int) and s_val > 0 - delta_dict[s.name] = (new_arr._get_value(s.name), s_val) - new_arr.env[s.name] = s_val - - # iterate through arguments and temporaries and - # propagate the assignments to all symbols of the same name - # - # (they may not refer to the same object, but may have the same name) - # - for arg in new_arr.arguments.values(): - if arg is None: - continue - for sym_s in arg.shape: - if sym_s.name in delta_dict: - if arg._get_value(sym_s.name) == delta_dict[sym_s.name][0]: - new_arr.env[sym_s.name] = delta_dict[sym_s.name][1] - else: - # the value is updated, since it points to the - # same Unsigned obj - assert sym_s.value == delta_dict[sym_s.name][1] - - for imd in new_arr.temporaries.values(): - for sym_s in imd.shape: - if sym_s.name in delta_dict: - if sym_s.value == delta_dict[sym_s.name][0]: # old value - sym_s.value = delta_dict[sym_s.name][1] - else: - # the value is updated, since it points to the - # same Unsigned obj - assert sym_s.value == delta_dict[sym_s.name][1] + new_arr.env[s] = sv return new_arr @@ -657,6 +741,10 @@ class Array(LazyObjectBase): :param idx: a tuple of indices or slices. """ + if isinstance(indices, str): + # get a member in the namespace with specified name + return super(Array, self).__getitem__(indices) + if not isinstance(indices, tuple): # masking assert isinstance(indices, Array) @@ -937,8 +1025,8 @@ class Array(LazyObjectBase): """Returns the shape as a tuple of ints if the value is known, or strings of names otherwise. """ - return tuple(self.env[s.name] if s.name in self.env else s.name - for s in self.shape) + return tuple( + self.env[s] if s in self.env else s for s in self.shape) def _make_default_expr(self, *args): """The default expression is the array it self. @@ -973,7 +1061,7 @@ class Array(LazyObjectBase): ] inames = tuple(ina.name for ina in self.inames) - shape_names = self._shape_names() + shape_names = self.shape assert len(inames) == self.ndim assert len(shape_names) == self.ndim @@ -1056,7 +1144,8 @@ class Array(LazyObjectBase): return self._make_axis_indices(*args) def _make_default_shape(self, ndim): - """Make a default shape based of name and ndim + """Make a default shape based of name and ndim. + Returns the shape tuple of ``Unsigned``. """ # zero-dimensional, aka a scalar if ndim == 0: @@ -1078,29 +1167,22 @@ class Array(LazyObjectBase): return tuple( Unsigned( name=names[d], - expr=var(names[d])) + expr=var(names[d]), + env=None, namespace=None) for d in range(ndim)) - def _shape_names(self): - """Shape information, stringified to be legal names. - """ - for s in self.shape: - assert isinstance(s, Unsigned) - return tuple(s.name for s in self.shape) - @property def _shape_str(self): """A shape description string used for loopy kernel construction. """ - shape_names = self._shape_names() - return ', '.join(shape_names) + return ', '.join(self.shape) @property def _shape_expr(self): """Values for the shape (if not None), otherwise expressions. """ return tuple( - self.env[s.name] if s.name in self.env else s.expr + self.env[s] if s in self.env else self[s].expr for s in self.shape) # }}} End private api @@ -1192,7 +1274,7 @@ class Array(LazyObjectBase): :param knl: the loopy kernel """ data_map = {} - shapeval_expansion_list = [] + expansion_list = [] # gather captured data for arr_name, varr in self.env.items(): @@ -1203,7 +1285,7 @@ class Array(LazyObjectBase): # only specify output shape, and let the backend to do the malloc for out_s in self.shape: if out_s.name not in self.env: - shapeval_expansion_list.append(out_s) + expansion_list.append(out_s) else: data_map[out_s.name] = self.env[out_s.name] @@ -1222,60 +1304,47 @@ class Array(LazyObjectBase): else: raise RuntimeError("unrecogonized captured variable %s" % arr_name) - # try to get as much extra data that loopy wants as possible - # also evaluates the shape expressions if knl is None: # gather all known shapes + for s in self.shape: - if s.name in data_map: + if s in data_map: continue - if s.value is not None: - data_map[s.name] = s.value - for arg in chain(self.arguments.values(), - self.bound_arguments.values(), self.temporaries.values()): + if self._get_value(s) is not None: + data_map[s] = self._get_value(s) + + for arg, _ in self.namespace.values(): if arg is None: continue # skip self assert isinstance(arg, Array) for s in arg.shape: - if s.name not in self.env: - shapeval_expansion_list.append(s) - elif arg._get_value(s.name) is not None: - data_map[s.name] = s.value + if s not in self.env: + expansion_list.append(self[s]) + elif arg._get_value(s) is not None: + data_map[s] = self.env[s] + else: + # try to get as much extra data that loopy wants as possible + # also evaluates the shape expressions + for argname, arg in knl.arg_dict.items(): if argname in data_map: continue - if argname in self._shape_names(): - shape_val = self.shape[self._shape_names().index(argname)] - if shape_val.name not in self.env: - shapeval_expansion_list.append(shape_val) - else: - data_map[argname] = self.env[shape_val.name] - for arr_arg in self.bound_arguments.values(): - if arr_arg is None: - continue - if argname in arr_arg._shape_names(): - shape_val = arr_arg.shape[ - arr_arg._shape_names().index(argname)] - if shape_val.name not in self.env: - shapeval_expansion_list.append(shape_val) - else: - data_map[argname] = self.env[shape_val.name] - for arr_imd in self.temporaries.values(): - if argname in arr_imd._shape_names(): - shape_val = arr_imd.shape[ - arr_imd._shape_names().index(argname)] - if shape_val.name not in self.env: - shapeval_expansion_list.append(shape_val) - else: - data_map[argname] = self.env[shape_val.name] - # no need to search arguments - assert len(self.arguments) == 0 + if argname in self.env: + data_map[argname] = self.env[argname] + elif argname in self.namespace: + # Value unknown, but has expression. + # Try to expand the expression on inputs before runtime + # (applies to scalar expression, like shape vars) + expansion_list.append(self[argname]) + + if len(self.arguments) > 0: + raise NotImplementedError() # Make sure not to include None's prior to evaluating expressions data_map = self._purge_nones(data_map) - for se in shapeval_expansion_list: + for se in expansion_list: try: seval = evaluate(se.expr, data_map) except UnknownVariableError: @@ -1448,12 +1517,12 @@ class Array(LazyObjectBase): extra_dtype_dict[argname] = sym_arg.dtype # shape args - if argname in self._shape_names(): + if argname in self.shape: extra_dtype_dict[argname] = shape_dtype for arr_arg in self.bound_arguments.values(): if arr_arg is None: continue - if argname in arr_arg._shape_names(): + if argname in arr_arg.shape: extra_dtype_dict[argname] = shape_dtype knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) @@ -1641,6 +1710,9 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): if base_env is None: base_env = default_env() + if name is None: + name = make_default_name(Unsigned) + if unsigned_like is None: return Unsigned( name=name, env=base_env, diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 5795878..70740fd 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -84,8 +84,8 @@ class BroadcastResult(object): expr_map = {} for arr in self.base_arrays: for s in arr.shape: - if s.name not in expr_map: - expr_map[var(s.name)] = s.expr + if s not in expr_map: + expr_map[var(s)] = arr[s].expr else: # same name must have the same runtime values # (may have different expressions) @@ -244,26 +244,11 @@ class BroadcastResult(object): brdc_arr.expr = substitute(brdc_arr.expr, expr_mapping) - # {{{ handle information flow from the base arr - - base_arr_passed_through = False - - # update argument list if base_arr was there - # if there was a self reference, the metadata needs to be captured - for argname in base_arr.arguments.keys(): - if base_arr.arguments[argname] is None: - brdc_arr.arguments[argname] = base_arr.as_stateless() - base_arr_passed_through = True - for argname in base_arr.bound_arguments.keys(): - if base_arr.bound_arguments[argname] is None: - brdc_arr.bound_arguments[argname] = base_arr.as_stateless() - base_arr_passed_through = True - - # if not, insert the base arr as temps - if not base_arr_passed_through: - brdc_arr.temporaries[base_arr.name] = base_arr.as_stateless() - - # }}} End handle information flow from the base arr + # capture the base array + brdc_arr.namespace[base_arr.name] = ( + base_arr.as_stateless(), + {'is_argument': False} + ) self.broadcast_arrays.append(brdc_arr) -- GitLab From 0588d629a1c686d6da9cfa4235ad7b6e27bf8623 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 20 Mar 2020 22:57:52 -0500 Subject: [PATCH 37/59] Towards unification of namespaces [skip-ci] --- lappy/core/array.py | 20 +++++++++-- lappy/core/basic_ops.py | 3 +- lappy/core/conditional.py | 20 +++++------ lappy/core/tools.py | 76 +-------------------------------------- lappy/core/ufuncs.py | 38 ++++---------------- 5 files changed, 37 insertions(+), 120 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index c211cb3..03a4d5f 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -215,7 +215,24 @@ class LazyObjectBase(object): obj = lapject if nm in self.namespace: # try to merge information - raise NotImplementedError() + sfobj = self.namespace[nm][0] + sftags = self.namespace[nm][1] + if sfobj.ndim != lapject.ndim: + raise ValueError("conflicting ndims") + if sfobj.shape != lapject.shape: + raise ValueError("conflicting shapes") + if sfobj.inames != lapject.inames: + raise ValueError("conflicting inames") + + # merge tags + for tag, tval in tags.items(): + if tag in self.namespace[nm][1]: + if tval != sftags[tag]: + raise ValueError( + "conflicting tag values: %s" % tag) + else: + sftags[tag] = tval + else: self._decl(nm, obj, tags) @@ -1640,7 +1657,6 @@ def to_lappy_array(arr_like, name=None, base_env=None): arr = arr_class(name=name, dtype=dtype, env=base_env, value=arr_like) arr.env[arr.name] = arr_like - arr.arguments = dict() return arr diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index ea71cf0..fed836f 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -176,7 +176,8 @@ class SubArrayFactory(object): :param iaxis: the index of the axis being sliced. """ assert isinstance(slc, slice) - dim = self.base_arr.shape[iaxis].with_state(self.base_arr.env) + dim_name = self.base_arr.shape[iaxis] + dim = self.base_arr[dim_name] step_none = slc.step is None if step_none: diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index 5d90879..afd690c 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -25,9 +25,7 @@ THE SOFTWARE. from pymbolic import var, substitute from pymbolic.primitives import If from lappy.core.array import Array, to_lappy_array -from lappy.core.tools import ( - check_and_merge_precs, check_and_merge_envs, check_and_merge_args, - check_and_merge_bargs, check_and_merge_temps) +from lappy.core.tools import check_and_merge_precs, check_and_merge_envs def conditional(cond, con, alt, name=None): @@ -119,14 +117,8 @@ def conditional(cond, con, alt, name=None): raise NotImplementedError() obj['env'] = check_and_merge_envs(cond, con, alt) - obj['arguments'] = check_and_merge_args(cond, con, alt) - obj['bound_arguments'] = check_and_merge_bargs(cond, con, alt) obj['preconditions'] = check_and_merge_precs(cond, con, alt) - - obj['temporaries'] = check_and_merge_temps(cond, con, alt) - obj['temporaries'][cond.name] = cond.as_stateless() - obj['temporaries'][con.name] = con.as_stateless() - obj['temporaries'][alt.name] = alt.as_stateless() + obj['namespace'] = None obj['is_integral'] = con.is_integral & alt.is_integral @@ -139,4 +131,10 @@ def conditional(cond, con, alt, name=None): # FIXME: compute the intersection of integer_domain of con and alt raise NotImplementedError() - return arr_class(**obj) + arr = arr_class(**obj) + + arr._meld_namespace(cond.namespace, cond.as_stateless()) + arr._meld_namespace(con.namespace, con.as_stateless()) + arr._meld_namespace(alt.namespace, alt.as_stateless()) + + return arr diff --git a/lappy/core/tools.py b/lappy/core/tools.py index 86d26d1..c62323a 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -24,9 +24,7 @@ THE SOFTWARE. import numpy as np from lappy.core.array import LazyObjectBase -__all__ = ["check_and_merge_envs", "check_and_merge_args", - "check_and_merge_bargs", "check_and_merge_temps", - "check_and_merge_precs", +__all__ = ["check_and_merge_envs", "check_and_merge_precs", "is_nonnegative_int", "is_constexpr"] @@ -57,78 +55,6 @@ def check_and_merge_envs(*arr_list): return new_env -def check_and_merge_args(*arr_list): - """Merge argument lists. - - Argument lists are dictionaries with names as keys, and symbolic object - (Array, Scalar, etc.) as values. Members of the list are all stateless. - - Returns the merged argument list. - Raises exception when there are conflicts - """ - margs = dict() - for arr in arr_list: - for name, arg in arr.arguments.items(): - if name in margs: - arg2 = margs[name] - assert arg2.name == arg.name - else: - if arg is None: - margs[name] = arr.as_stateless() - else: - assert name == arg.name - margs[name] = arg - return margs - - -def check_and_merge_bargs(*arr_list): - """Merge bound argument lists. - - BALs are dictionaries with names as keys, and symbolic object - (Array, Scalar, etc.) as values. Members of the list are all stateless. - - Returns the merged argument list. - Raises exception when there are conflicts - """ - margs = dict() - for arr in arr_list: - for name, arg in arr.bound_arguments.items(): - if name in margs: - arg2 = margs[name] - assert arg2.name == arg.name - else: - if arg is None: - margs[name] = arr.as_stateless() - else: - assert name == arg.name - margs[name] = arg - return margs - - -def check_and_merge_temps(*arr_list): - """Merge bound temporaries lists. - - ILs are dictionaries with names as keys, and symbolic object - (Array, Scalar, etc.) as values. Members of the list are all stateless. - - Returns the merged argument list. - Raises exception when there are conflicts - """ - margs = dict() - for arr in arr_list: - for name, arg in arr.temporaries.items(): - if name in margs: - arg2 = margs[name] - assert arg2.name == arg.name - else: - if arg is None: - raise ValueError("self reference in the temporaries list") - else: - assert name == arg.name - margs[name] = arg - return margs - - def check_and_merge_precs(*arr_list): """Merge precondition lists. diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index cf2612a..e891a91 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -28,8 +28,6 @@ from pymbolic.primitives import Min, Max from lappy.core.array import Array, to_lappy_array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast -from lappy.core.tools import ( - check_and_merge_envs, check_and_merge_temps) class UFunc(object): @@ -292,21 +290,6 @@ class BinaryOperation(UFunc): assert ka == va.name assert ka == b.arguments[ka].name - new_arglist = a.arguments.copy() - new_arglist.update(b.arguments) - - new_bound_arglist = a.bound_arguments.copy() - new_bound_arglist.update(b.bound_arguments) - - new_env = check_and_merge_envs(a, b) - new_interm = check_and_merge_temps(a, b) - - if name in new_env: - raise ValueError("The name %s is ambiguous" % name) - - new_interm[a.name] = a - new_interm[b.name] = b - # TODO: more elegant handling of inames assert len(a.inames) == len(b.inames) b = b.with_inames(a.inames) @@ -320,10 +303,8 @@ class BinaryOperation(UFunc): 'inames': a.inames, 'expr': new_expr, 'value': None, 'domain_expr': a.domain_expr, - 'arguments': new_arglist, - 'bound_arguments': new_bound_arglist, - 'temporaries': new_interm, - 'env': new_env, + 'namespace': None, + 'env': a.env.copy(), 'preconditions': a.preconditions + b.preconditions + bres.preconditions, 'ndim': bres.ndim, 'shape': bres._shape_exprs, 'dtype': new_dtype, @@ -334,16 +315,11 @@ class BinaryOperation(UFunc): } arr = Array(**obj) - # remove self reference - if a.name in arr.arguments: - arr.arguments[a.name] = a.as_stateless() - elif a.name in arr.bound_arguments: - arr.bound_arguments[a.name] = a.as_stateless() - - if b.name in arr.arguments: - arr.arguments[b.name] = b.as_stateless() - elif b.name in arr.bound_arguments: - arr.bound_arguments[b.name] = b.as_stateless() + arr._meld_namespace(a.namespace.copy(), a.as_stateless()) + print(arr.namespace.keys()) + print(b.namespace.keys()) + arr._meld_namespace(b.namespace.copy(), b.as_stateless()) + arr._meld_env(b.env.copy()) return arr -- GitLab From 3f0705e79a7a122147da3c683fbf2c4509ff2afa Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Mar 2020 16:06:08 -0500 Subject: [PATCH 38/59] Towards unification of namespaces [skip-ci] --- lappy/core/array.py | 79 ++++++++++++++++++----------------------- lappy/core/basic_ops.py | 8 ++--- lappy/core/broadcast.py | 1 + 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 03a4d5f..4c4f8b5 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -150,7 +150,7 @@ class LazyObjectBase(object): # if either namespace or env is set, the obj has state if self.namespace is None: if self.env is not None: - self.namespace = {self.name: (None, dict())} + self.namespace = {self.name: (None, {'is_argument': True})} if self.env is None: if self.namespace is not None: @@ -515,11 +515,14 @@ class Array(LazyObjectBase): self._decl(s.name, s, {'is_argument': True}) self.shape = tuple(s.name for s in lapshape) else: - lapshape = to_lappy_shape(shape) - self.shape = tuple(s.name for s in lapshape) - for s in lapshape: - self._meld_namespace(s.namespace, s.as_stateless()) - self._meld_env(s.env) + if all(isinstance(s, str) for s in shape): + self.shape = tuple(shape) + else: + lapshape = to_lappy_shape(shape) + self.shape = tuple(s.name for s in lapshape) + for s in lapshape: + self._meld_namespace(s.namespace, s.as_stateless()) + self._meld_env(s.env) self.dtype = kwargs.pop("dtype", None) self.is_integral = kwargs.pop("is_integral", False) @@ -553,7 +556,8 @@ class Array(LazyObjectBase): return self.__class__( name=self.name, ndim=self.ndim, inames=self.inames, - shape=self.shape, dtype=self.dtype, + shape=self.shape, + dtype=self.dtype, expr=self.expr, is_integral=self.is_integral, domain_expr=self.domain_expr, env=env, @@ -598,16 +602,19 @@ class Array(LazyObjectBase): if (self.name in self.env) and rename_arguments: new_arr.env[name] = new_arr.env.pop(self.name) - # when the argument is self-referencing + # new self-reference + new_arr.namespace[name] = new_arr.namespace.pop(self.name) + + # old self-reference if rename_arguments: - new_arr.namespace[name] = \ - new_arr.namespace.pop(self.name) new_arr.expr = pymbolic.substitute(new_arr.expr, {var(self.name): var(name)}) else: - new_arr.namespace.pop(self.name) new_arr.namespace[self.name] = ( self.as_stateless(), self.namespace[self.name][1]) + new_arr.namespace[name] = ( + None, self.namespace[self.name][1].copy()) + new_arr.namespace[name][1]['is_argument'] = False new_arr.name = name return new_arr @@ -691,17 +698,20 @@ class Array(LazyObjectBase): if key not in self.namespace: raise ValueError("cannot bind an undefined name: %s" % key) - if key in self.env(): + if key in self.env.keys(): # bounding cannot be undone # (since un-inference is not possible) raise ValueError("argument named %s is already bound" % key) if not self.namespace[key][1].get('is_argument', False): + print(self.namespace[key]) + print(key) raise ValueError("trying to bind data to a non-argument") new_arr = self.copy() new_arr.name = new_name + new_arr.namespace[new_name] = new_arr.namespace.pop(self.name) for key, val in kwargs.items(): if hasattr(val, 'dtype'): @@ -716,7 +726,7 @@ class Array(LazyObjectBase): assert new_arr.ndim == val.ndim assert len(new_arr.shape) == val.ndim else: - assert new_arr.namespace[key][1].ndim == val.ndim + assert new_arr.namespace[key][0].ndim == val.ndim assert len(val.shape) == val.ndim new_arr.env[key] = val # actual data @@ -1133,16 +1143,15 @@ class Array(LazyObjectBase): def _shadow_size(self, axes=None): """When axes is None, computes the full size. Otherwise computes the size of the projected array on the given axes. + + If the shasow size of not computable, returns an expression for it. """ if axes is None: axes = list(range(self.ndim)) else: # so that generators can be understood axes = list(axes) - shape_vars = tuple( - var(self.shape[ax].name) if isinstance(self.shape[ax], Array) - else str(self.shape[ax]) - for ax in axes) + shape_vars = tuple(var(self.shape[ax]) for ax in axes) size_expr = Product(shape_vars) try: return evaluate(size_expr, self.env) @@ -1250,14 +1259,7 @@ class Array(LazyObjectBase): raise PreconditionNotMetError(precon_err_msg) def _set_shape_values(self, shape, name=None): - """Instantiate shape with concrete values. - - Use ``name`` to indicate an argument, bound argument, or temporary - to work with. If name is None, the function has same effects as - ``with_shape_data``. - - This method should not be called from a user. Users should use - Array.with_shape_data() instead. + """Instantiate shape of a namespace member with concrete values. """ for s in tuple(shape): assert int(s) == s @@ -1265,24 +1267,19 @@ class Array(LazyObjectBase): if name is None or name == self.name: sym_shape = self.shape + elif name in self.namespace: + sym_shape = self[name].shape else: - if name in self.arguments: - sym_shape = self.arguments[name].shape - elif name in self.bound_arguments: - sym_shape = self.bound_arguments[name].shape - elif name in self.temporaries: - sym_shape = self.temporaries[name].shape - else: - raise ValueError() + raise ValueError() - for s, sym_s in zip(shape, sym_shape): - assert isinstance(sym_s, Unsigned) + for s, sym_s_name in zip(shape, sym_shape): + sym_s = self[sym_s_name] if sym_s.name in self.env: warnings.warn("captured value for %s is overwritten " "(%s --> %s)" % ( sym_s.name, str(self.env[sym_s.name]), str(s))) - self.env[sym_s.name] = s + self.env[sym_s_name] = s def _get_data_mapping(self, knl=None): """Make a data mapping using known data, tailored for giving inputs to @@ -1343,7 +1340,6 @@ class Array(LazyObjectBase): else: # try to get as much extra data that loopy wants as possible # also evaluates the shape expressions - for argname, arg in knl.arg_dict.items(): if argname in data_map: continue @@ -1355,9 +1351,6 @@ class Array(LazyObjectBase): # (applies to scalar expression, like shape vars) expansion_list.append(self[argname]) - if len(self.arguments) > 0: - raise NotImplementedError() - # Make sure not to include None's prior to evaluating expressions data_map = self._purge_nones(data_map) @@ -1408,10 +1401,6 @@ class Array(LazyObjectBase): logging.info("cache hit when evaluating %s" % self.name) return self.value - if len(self.arguments) > 0: - raise ValueError("cannot evaluate a partially-bound expression " - "(missing data for %s)" % ', '.join(self.arguments.keys())) - # Step 1: # identify codegen statements (loopy assignments) # and raise exception if ther are multiple statements @@ -1529,7 +1518,7 @@ class Array(LazyObjectBase): for argname, arg in knl.arg_dict.items(): if arg.dtype is None: # array args - sym_arg = self._find_name(argname) + sym_arg = self[argname] if sym_arg is not None: extra_dtype_dict[argname] = sym_arg.dtype diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index fed836f..a9be519 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -526,12 +526,10 @@ def reshape(array, newshape, order='C', name=None, inames=None): new_arr.preconditions = new_precond newshape = to_lappy_shape(newshape) - new_arr.shape = tuple(s.as_stateless() for s in newshape) + new_arr.shape = tuple(s.name for s in newshape) for s in newshape: - if s.value is not None: - # generated name should not conflict - assert s.name not in new_arr.env - new_arr.env[s.name] = s.value + new_arr._meld_namespace(s.namespace, s.as_stateless()) + new_arr._meld_env(s.env) if name != array.name: # if the reference is no longer to self diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 70740fd..a2d5cff 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -85,6 +85,7 @@ class BroadcastResult(object): for arr in self.base_arrays: for s in arr.shape: if s not in expr_map: + print(arr.namespace.keys()) expr_map[var(s)] = arr[s].expr else: # same name must have the same runtime values -- GitLab From 5b7480526a4f0c500983695efd3d25cd2b30ebbc Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Mar 2020 16:57:55 -0500 Subject: [PATCH 39/59] Towards unification of namespaces [skip-ci] --- lappy/core/array.py | 20 ++++++++++++++----- lappy/core/broadcast.py | 44 +++++++++++++++++++++++++++++++---------- lappy/core/ufuncs.py | 31 ++++++----------------------- test/test_broadcast.py | 2 +- 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 4c4f8b5..e445732 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -174,21 +174,31 @@ class LazyObjectBase(object): # {{{ namespace/env utils - def _decl(self, name, lapject, tags=None): + def _decl(self, lapject, tags=None): """Add symbol to the namespace. """ - assert isinstance(name, str) + if self.is_stateless: + raise TypeError( + "can only declare names inside a stateful " + "lappy object ") assert isinstance(lapject, LazyObjectBase) + name = lapject.name if name in self.namespace: raise ValueError("name %s is taken" % name) + if lapject.has_state: + self._meld_namespace(lapject.namespace, lapject.as_stateless()) + self._meld_env(lapject.env) + if tags is None: + tags = lapject.namespace[name][1].copy() + if tags is None: tags = dict() else: assert isinstance(tags, dict) - self.namespace[name] = (lapject, tags) + self.namespace[name] = (lapject.as_stateless(), tags) def _meld_namespace(self, namespace, stateless_self=None): """Merge with another namespace. The other namespace may contain a @@ -234,7 +244,7 @@ class LazyObjectBase(object): sftags[tag] = tval else: - self._decl(nm, obj, tags) + self.namespace[nm] = (obj.as_stateless(), tags) def _meld_env(self, env): """Merge with another env. @@ -512,7 +522,7 @@ class Array(LazyObjectBase): if shape is None: lapshape = self._make_default_shape(ndim) for s in lapshape: - self._decl(s.name, s, {'is_argument': True}) + self._decl(s) self.shape = tuple(s.name for s in lapshape) else: if all(isinstance(s, str) for s in shape): diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index a2d5cff..4502950 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -24,7 +24,7 @@ from functools import partial from pymbolic import evaluate, substitute, var from pymbolic.primitives import Product, Expression, Max -from lappy.core.array import to_lappy_shape, to_lappy_array +from lappy.core.array import to_lappy_shape, to_lappy_array, make_default_name from lappy.core.tools import is_nonnegative_int from lappy.core.preconditions import EvaluationPrecondition @@ -33,6 +33,10 @@ class BroadcastResult(object): """Encapsulates the broadcasting result. The object acts like an array when performing shape-related queries. + .. attribute:: name + + name of the broadcast, used to name the resulting arrays. + .. attribute:: base_arrays list of (lazy) arrays, whose api should support .shape and .ndim @@ -58,19 +62,39 @@ class BroadcastResult(object): size (number of elements) of the broadcast result + .. attribute:: inames + + all broadcast results share the same set of inames + .. attribute:: preconditions list of extra preconditions to make the broadcast feasible NOTE: only fixed (statically known) ndims are supported """ + _counter = 0 + _name_prefix = '__lappy_broadcast_res_' def __init__(self, array_list): self.base_arrays = array_list self.preconditions = [] + + self.name = self._make_broadcast_name() self._broadcase_base_arrays() + self.inames = self._make_broadcast_inames() + self._make_broadcast_arrays() + def _make_broadcast_name(self): + return make_default_name(self.__class__) + + def _make_broadcast_inames(self): + if self.ndim == 0: + return () + assert self.ndim > 0 + return tuple(var('__%s_inames_%d' % (self.name, d)) + for d in range(self.ndim)) + def _broadcase_base_arrays(self): """Compute the ndim, shape and size of the broadcast result. """ @@ -85,7 +109,6 @@ class BroadcastResult(object): for arr in self.base_arrays: for s in arr.shape: if s not in expr_map: - print(arr.namespace.keys()) expr_map[var(s)] = arr[s].expr else: # same name must have the same runtime values @@ -187,18 +210,17 @@ class BroadcastResult(object): bshape = tuple(bshape_pre[i] for i in range(self.ndim)) self._shape_exprs = bshape - self._size_expr = Product(self._shape_exprs) + # Unsigneds of broadcast shape + self.shape = to_lappy_shape(self._shape_exprs) + def _make_broadcast_arrays(self): """After knowing ndim, shape and size symbolically, construct the broadcast copies of the input arrays. """ self.broadcast_arrays = [] - # Unsigneds of broadcast shape - lappy_shape = to_lappy_shape(self._shape_exprs) - for base_arr in self.base_arrays: dim_offset = self.ndim - base_arr.ndim assert dim_offset >= 0 @@ -207,7 +229,7 @@ class BroadcastResult(object): in_shape = base_arr._get_shape_vals_or_strs() if dim_offset == 0: - if in_shape == self.shape: + if in_shape == self.shape_val_or_str: # unchanged self.broadcast_arrays.append(base_arr) continue @@ -223,10 +245,12 @@ class BroadcastResult(object): brdc_arr = base_arr.with_name(name) assert isinstance(self.ndim, int) and self.ndim >= 0 - brdc_arr.shape = lappy_shape + brdc_arr.shape = tuple(s.name for s in self.shape) + for s in self.shape: + brdc_arr._decl(s) # make new inames - brdc_arr.inames = brdc_arr._make_default_inames() + brdc_arr.inames = self.inames # make a new copy with broadcast expression # @@ -256,7 +280,7 @@ class BroadcastResult(object): assert len(self.base_arrays) == len(self.broadcast_arrays) @property - def shape(self): + def shape_val_or_str(self): return tuple(s if is_nonnegative_int(s) else str(s) for s in self._shape_exprs) diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index e891a91..1af5a10 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -123,21 +123,17 @@ class UnaryOperation(UFunc): else: new_dtype = self.dtype - new_interm = a.temporaries.copy() - if name in new_interm: + if name in a.namespace: raise ValueError("The name %s is ambiguous" % name) - new_interm[a.name] = a.as_stateless() obj = { 'name': name, 'inames': a.inames, 'expr': self.f(a.expr), 'value': None, 'domain_expr': a.domain_expr, - 'arguments': a.arguments.copy(), - 'bound_arguments': a.bound_arguments.copy(), 'env': a.env.copy(), + 'namespace': a.namespace.copy(), 'preconditions': list(a.preconditions), - 'temporaries': new_interm, 'ndim': a.ndim, 'shape': a.shape, 'dtype': new_dtype, 'is_integral': @@ -146,11 +142,10 @@ class UnaryOperation(UFunc): } arr = Array(**obj) - # remove self reference - if a.name in arr.arguments: - arr.arguments[a.name] = a.as_stateless() - elif a.name in arr.bound_arguments: - arr.bound_arguments[a.name] = a.as_stateless() + # update self reference + arr.namespace[a.name] = ( + a.as_stateless(), a.namespace[a.name][1].copy()) + arr.namespace[arr.name] = (None, dict()) return arr @@ -276,20 +271,6 @@ class BinaryOperation(UFunc): else: new_dtype = self.dtype - for ka, va in a.arguments.items(): - if va is None: - continue - if ka in b.arguments.keys(): - if not va.ndim == b.arguments[ka].ndim: - raise ValueError("argument %s has different ndims in %s and %s" - % (ka, a.name, b.name)) - if not va.shape == b.arguments[ka].shape: - raise ValueError("argument %s has different shapes in %s and %s" - % (ka, a.name, b.name)) - - assert ka == va.name - assert ka == b.arguments[ka].name - # TODO: more elegant handling of inames assert len(a.inames) == len(b.inames) b = b.with_inames(a.inames) diff --git a/test/test_broadcast.py b/test/test_broadcast.py index eef6121..cd73001 100644 --- a/test/test_broadcast.py +++ b/test/test_broadcast.py @@ -48,7 +48,7 @@ def test_broadcast_rules(shapes): lappy_res = broadcast(*laarrs) assert numpy_res.ndim == lappy_res.ndim - assert numpy_res.shape == lappy_res.shape + assert numpy_res.shape == lappy_res.shape_val_or_str assert numpy_res.size == lappy_res.size -- GitLab From 1de3083d2cd7ce1806830dd9ed364bf088b8379b Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Mar 2020 18:58:04 -0500 Subject: [PATCH 40/59] Finish unification of namespaces --- lappy/core/array.py | 245 +++++++++++++++------------------------- lappy/core/broadcast.py | 2 +- lappy/core/ufuncs.py | 23 ++-- pytest.ini | 3 + test/test_reshape.py | 2 - 5 files changed, 107 insertions(+), 168 deletions(-) create mode 100644 pytest.ini diff --git a/lappy/core/array.py b/lappy/core/array.py index e445732..80c63be 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -11,7 +11,7 @@ import loopy as lp import islpy as isl import pymbolic -from pymbolic import var, evaluate +from pymbolic import var, evaluate, substitute from pymbolic.primitives import Lookup, Subscript, Variable, Product from pymbolic.mapper.evaluator import UnknownVariableError @@ -364,7 +364,7 @@ class LazyObjectBase(object): return { key: val[0] for key, val in self.namespace.items() - if val[1].get('is_argument', False) and ( + if val[1].get('is_argument', True) and ( key not in self.env) } @@ -377,7 +377,7 @@ class LazyObjectBase(object): return { key: val[0] for key, val in self.namespace.items() - if val[1].get('is_argument', False) and ( + if val[1].get('is_argument', True) and ( key in self.env) } @@ -527,6 +527,11 @@ class Array(LazyObjectBase): else: if all(isinstance(s, str) for s in shape): self.shape = tuple(shape) + for s in self.shape: + # make new symbols if not given by the namespace arg + if s not in self.namespace: + sap = to_lappy_unsigned(s) + self._decl(sap) else: lapshape = to_lappy_shape(shape) self.shape = tuple(s.name for s in lapshape) @@ -599,7 +604,7 @@ class Array(LazyObjectBase): """ new_arr = self.copy() if name == self.name or name is None: - warnings.warn("name is unchanged, just making a copy") + # warnings.warn("name is unchanged, just making a copy") return new_arr if name in self.namespace: @@ -696,13 +701,13 @@ class Array(LazyObjectBase): return new_arr - def with_data(self, new_name=None, **kwargs): + def with_data(self, **kwargs): """Binds specific data to a slots in the arguments, resulting in a new (shallow) copy of self with the bound arguments catupred into its environment. """ - if new_name is None: - new_name = 'bound%d_' % self.__class__._counter + self.name + if 'new_name' in kwargs: + raise ValueError("cannot rename while binding data") for key in kwargs.keys(): if key not in self.namespace: @@ -715,22 +720,23 @@ class Array(LazyObjectBase): % key) if not self.namespace[key][1].get('is_argument', False): - print(self.namespace[key]) - print(key) - raise ValueError("trying to bind data to a non-argument") + raise ValueError("trying to bind data to a non-argument %s" + % key) + # make a copy new_arr = self.copy() - new_arr.name = new_name - new_arr.namespace[new_name] = new_arr.namespace.pop(self.name) for key, val in kwargs.items(): + if hasattr(val, 'dtype'): if key == self.name: new_arr.dtype = val.dtype else: new_arr.namespace[key][0].dtype = val.dtype + if hasattr(val, 'shape'): new_arr._set_shape_values(val.shape, key) + if hasattr(val, 'ndim'): if key == self.name: assert new_arr.ndim == val.ndim @@ -741,8 +747,6 @@ class Array(LazyObjectBase): new_arr.env[key] = val # actual data - # rename value of self - new_arr.env[new_name] = new_arr.env.pop(self.name, None) return new_arr # }}} End copy constructors, with_xxx(), as_xxx() @@ -837,9 +841,6 @@ class Array(LazyObjectBase): def __add__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -848,9 +849,6 @@ class Array(LazyObjectBase): def __radd__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -859,9 +857,6 @@ class Array(LazyObjectBase): def __sub__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -870,9 +865,6 @@ class Array(LazyObjectBase): def __rsub__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -881,9 +873,6 @@ class Array(LazyObjectBase): def __mul__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -892,9 +881,6 @@ class Array(LazyObjectBase): def __rmul__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -903,9 +889,6 @@ class Array(LazyObjectBase): def __div__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -916,9 +899,6 @@ class Array(LazyObjectBase): def __rdiv__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -929,9 +909,6 @@ class Array(LazyObjectBase): def __pow__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -940,9 +917,6 @@ class Array(LazyObjectBase): def __rpow__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -971,9 +945,6 @@ class Array(LazyObjectBase): otherwise. """ if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -989,9 +960,6 @@ class Array(LazyObjectBase): def __le__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -1000,9 +968,6 @@ class Array(LazyObjectBase): def __lt__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -1011,9 +976,6 @@ class Array(LazyObjectBase): def __ge__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -1022,9 +984,6 @@ class Array(LazyObjectBase): def __gt__(self, other): if not isinstance(other, Array): - warnings.warn( - "Implicit conversion of %s to Lappy array" - % str(type(other))) other_arr = to_lappy_array(other) else: other_arr = other @@ -1285,10 +1244,12 @@ class Array(LazyObjectBase): for s, sym_s_name in zip(shape, sym_shape): sym_s = self[sym_s_name] if sym_s.name in self.env: - warnings.warn("captured value for %s is overwritten " - "(%s --> %s)" % ( - sym_s.name, - str(self.env[sym_s.name]), str(s))) + if self.env[sym_s_name] != s: + warnings.warn( + "captured shape value for %s is overwritten " + "(%s --> %s)" % ( + sym_s.name, + str(self.env[sym_s.name]), str(s))) self.env[sym_s_name] = s def _get_data_mapping(self, knl=None): @@ -1411,38 +1372,15 @@ class Array(LazyObjectBase): logging.info("cache hit when evaluating %s" % self.name) return self.value - # Step 1: - # identify codegen statements (loopy assignments) - # and raise exception if ther are multiple statements - # FIXME - n_insns = 1 - if n_insns > 1: - raise TypeError("the array %s cannot be evaluated without temporaries " - "(use the compiler instead) " % self.name) - - # Step 2: - # make the initial loop domain by multiplying the index space of - # the array with hidden axes (e.g. in the case of inner products) - pass - - # Step 3: - # make the conditional expression that accounts for - # - # - sliced LHS A[1:n-1:2, :] = ... - # - # - conditional assignment A[A>0] = ... + # context: where the preconditions are checked + # data_map: passed to the loopy kernel + # scalar_map: inlined scalars # - # Note that this is done on the RHS, e.g. - # A[i, j] = if((i >= 1) and (i < n - 1) and ((i - 1) % 2 == 0), ..., ...) - # A[i, j] = if(A[i, j] > 0, ..., ...) - # FIXME - rhs_expr = self.expr + # context = data_map + scalar_map + context = dict() - # Step 4 - # Account for fancy indexing on the LHS A[B] = ... - # by adding a separate loop domain over the index space of B - # and making corresponding LHS expressions - # FIXME + # expression handling + rhs_expr = self.expr if self.ndim == 0: lhs_expr = var(self.name)[var(self.name + '_dummy_iname')] else: @@ -1450,75 +1388,74 @@ class Array(LazyObjectBase): loop_domain = self._generate_index_domains() - # Step 5 - # Make and run loopy kernel - # - # FIXME: collect all integer_domain and pass to loopy - # - kernel_args = [] + # collect kernel args + kernel_data = [] + if self.ndim == 0: + kernel_data.append( + lp.GlobalArg( + self.name, shape=(1, ), dtype=self.dtype)) + else: + kernel_data.append( + lp.GlobalArg( + self.name, shape=self._shape_str, dtype=self.dtype)) - # array args - # FIXME: instead of looping over env, better - # only loop over symbolic caputer lists. - for arr_name, varr in self.env.items(): - if arr_name in ['cl_ctx', 'cu_ctx']: - continue + # substitute baked-in constants + inline_scalars = True + if inline_scalars: + scalar_map = dict() + for vn, (vv, vtag) in self.namespace.items(): + if vv is None: + continue + if vv.ndim == 0: + scalar_map[vn] = self._get_value(vn) + scalar_map = self._purge_nones(scalar_map) + context.update(scalar_map) + rhs_expr = substitute(rhs_expr, scalar_map) + + # add more kernel args (reduces a log of guessing time) + # e.g., link array shape vars with the arrays, dtypes, etc. + # + # (the trick is to add no more than actually needed) + # FIXME: let loopy.ArgumentGuesser to find out what are needed. + for arr_name, (varr, tag) in self.namespace.items(): if arr_name == self.name: - continue # handle self later - elif arr_name in self.arguments.keys(): - if self.arguments[arr_name] is None: - pass # self + continue # self (output) handled above + if isinstance(varr, Array) and varr.ndim > 0: + # for now, we only specify inputs + if not tag.get('is_argument'): + continue + if varr.dtype is None: + dtype = np.int32 if varr.is_integral else np.float64 else: - assert isinstance(self.arguments[arr_name], Array) - kernel_args.append( - lp.GlobalArg( - arr_name, - dtype=self.arguments[arr_name].dtype, - shape=self.arguments[arr_name]._shape_str)) - elif arr_name in self.bound_arguments.keys(): - if self.bound_arguments[arr_name] is None: - pass # self + dtype = varr.dtype + kernel_data.append( + lp.GlobalArg( + arr_name, + dtype=dtype, + shape=varr._shape_str)) + elif isinstance(varr, Array) and varr.ndim == 0: + if varr.dtype is None: + dtype = np.int32 if varr.is_integral else np.float64 else: - assert isinstance(self.bound_arguments[arr_name], Array) - if self.bound_arguments[arr_name].ndim == 0: - kernel_args.append( - lp.ValueArg( - arr_name, - dtype=self.bound_arguments[ - arr_name].dtype)) - else: - kernel_args.append( - lp.GlobalArg( - arr_name, - dtype=self.bound_arguments[arr_name].dtype, - shape=self.bound_arguments[ - arr_name]._shape_str)) - elif arr_name in self.temporaries.keys(): - continue # do not add temps as kernel args + dtype = varr.dtype + kernel_data.append( + lp.ValueArg(arr_name, dtype=dtype)) else: pass # warnings.warn( # "cannot find shape information of %s" % arr_name) - # output array args - if self.ndim == 0: - kernel_args.append( - lp.GlobalArg( - self.name, shape=(1, ), dtype=self.dtype)) - else: - kernel_args.append( - lp.GlobalArg( - self.name, shape=self._shape_str, dtype=self.dtype)) + kernel_data.append('...') - # let loopy do the inference - kernel_args.append('...') + if '__lappy_array_3' in self.namespace: + print(self.namespace['__lappy_array_3']) knl = lp.make_kernel( loop_domain, [ lp.Assignment(lhs_expr, rhs_expr) ], - kernel_args, + kernel_data, lang_version=(2018, 2)) knl = lp.set_options(knl, return_dict=True) @@ -1544,12 +1481,12 @@ class Array(LazyObjectBase): knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) data_map = self._get_data_mapping(knl) + context.update(data_map) - # TODO: to test everything that the kernel sees, - # let loopy infer more context first (e.g. shape of input arrays) - self._check_preconditions(context=data_map) + # check preconditions + self._check_preconditions(context) - if 1: + if 0: print(knl) print(data_map) @@ -1741,7 +1678,8 @@ def to_lappy_unsigned(unsigned_like, name=None, base_env=None, dtype=np.int32): # a string of name, not value return Unsigned( name=unsigned_like, env=base_env, - namespace={name: (None, {'is_argument': True})}, + namespace={ + unsigned_like: (None, {'is_argument': True})}, dtype=dtype) if abs(int(unsigned_like)) - unsigned_like == 0: @@ -1820,11 +1758,6 @@ def to_lappy_shape(shape): for i, c in enumerate(shape): if isinstance(c, Array): components[i] = c - if not isinstance(c, Unsigned): - warnings.warn( - "Implicit conversion of %s to unsigned (non-negative) " - "integer" - % str(c)) else: components[i] = to_lappy_unsigned(c, name=None) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 4502950..701491b 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -272,7 +272,7 @@ class BroadcastResult(object): # capture the base array brdc_arr.namespace[base_arr.name] = ( base_arr.as_stateless(), - {'is_argument': False} + base_arr.namespace[base_arr.name][1].copy() ) self.broadcast_arrays.append(brdc_arr) diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 1af5a10..6f4ad85 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -23,7 +23,7 @@ THE SOFTWARE. """ import numpy as np -from pymbolic import var +from pymbolic import var, substitute from pymbolic.primitives import Min, Max from lappy.core.array import Array, to_lappy_array from lappy.core.primitives import FloatingPointRemainder @@ -271,17 +271,23 @@ class BinaryOperation(UFunc): else: new_dtype = self.dtype - # TODO: more elegant handling of inames - assert len(a.inames) == len(b.inames) - b = b.with_inames(a.inames) - # expression handling - new_expr = self.f(a.expr, b.expr) + a_expr = substitute( + a.expr, + {ainame: briname + for ainame, briname in zip(a.inames, bres.inames)} + ) + b_expr = substitute( + b.expr, + {biname: briname + for biname, briname in zip(b.inames, bres.inames)} + ) + new_expr = self.f(a_expr, b_expr) new_integer_domain = None obj = { 'name': name, - 'inames': a.inames, + 'inames': bres.inames, 'expr': new_expr, 'value': None, 'domain_expr': a.domain_expr, 'namespace': None, @@ -296,9 +302,8 @@ class BinaryOperation(UFunc): } arr = Array(**obj) + arr.namespace[arr.name][1]['is_argument'] = False arr._meld_namespace(a.namespace.copy(), a.as_stateless()) - print(arr.namespace.keys()) - print(b.namespace.keys()) arr._meld_namespace(b.namespace.copy(), b.as_stateless()) arr._meld_env(b.env.copy()) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c24fe5b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning diff --git a/test/test_reshape.py b/test/test_reshape.py index 9848ada..f35f5df 100644 --- a/test/test_reshape.py +++ b/test/test_reshape.py @@ -61,8 +61,6 @@ def test_reshape(ctx_factory, in_shape, out_shape, order, dtype=np.float64): C = B.with_data(A=data).with_name('C') # noqa: N806 - print(C.preconditions) - lappy_val = C.eval() numpy_val = data.reshape(out_shape, order=order) -- GitLab From a6422e195bbc9532ee0c2916f5343673e077e369 Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Mar 2020 23:33:20 -0500 Subject: [PATCH 41/59] Add configure() for setting evaluation means #16 --- .gitignore | 1 + lappy/__init__.py | 8 +++++--- lappy/core/array.py | 20 ++++++++++---------- test/test_reshape.py | 6 ++---- test/test_transpose.py | 6 ++---- test/test_ufunc.py | 19 ++++++++----------- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 6c1ff43..ed694da 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ *.egg-info !lappy/lib +!lappy/target diff --git a/lappy/__init__.py b/lappy/__init__.py index 8b72597..a699ca5 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -22,11 +22,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from lappy.core import ndarray, to_lappy_array +__all__ = [] -__all__ = ["ndarray", "to_lappy_array"] +from lappy.config import configure, CONFIGS +__all__ += ["CONFIGS", "configure"] +from lappy.core import ndarray, to_lappy_array +__all__ += ["ndarray", "to_lappy_array"] from lappy.core.ufuncs import sin, cos, exp - __all__ += ["sin", "cos", "exp"] diff --git a/lappy/core/array.py b/lappy/core/array.py index 80c63be..43915cf 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -5,6 +5,7 @@ import logging from traceback import format_list, extract_stack import pyopencl as cl +import pyopencl.array import numpy as np import loopy as lp @@ -16,6 +17,7 @@ from pymbolic.primitives import Lookup, Subscript, Variable, Product from pymbolic.mapper.evaluator import UnknownVariableError from pprint import pformat +from lappy import CONFIGS copyright__ = "Copyright (C) 2019 Sophia Lin, Andreas Kloeckner and Xiaoyu Wei" @@ -41,13 +43,9 @@ THE SOFTWARE. def default_env(): - """Default environment to be captured. Also used as the environment - for all stateless arrays. + """Default environment to be captured. """ - return { - "cl_ctx": None, - "cu_ctx": None - } + return dict() class PreconditionNotMetError(Exception): @@ -1263,8 +1261,6 @@ class Array(LazyObjectBase): # gather captured data for arr_name, varr in self.env.items(): - if arr_name in ['cl_ctx', 'cu_ctx']: - continue if arr_name == self.name: # only specify output shape, and let the backend to do the malloc @@ -1490,8 +1486,12 @@ class Array(LazyObjectBase): print(knl) print(data_map) - queue = cl.CommandQueue(self.env['cl_ctx']) - evt, lp_res = knl(queue, **data_map) + print(CONFIGS) + if CONFIGS['target'] == 'opencl': + queue = cl.CommandQueue(CONFIGS['cl_ctx']) + evt, lp_res = knl(queue, **data_map) + else: + raise NotImplementedError() if self.ndim == 0: self.value = lp_res[self.name][0] diff --git a/test/test_reshape.py b/test/test_reshape.py index f35f5df..66fcb5a 100644 --- a/test/test_reshape.py +++ b/test/test_reshape.py @@ -49,12 +49,10 @@ from pyopencl.tools import ( # noqa ]) @pytest.mark.parametrize('order', ['C', 'F']) def test_reshape(ctx_factory, in_shape, out_shape, order, dtype=np.float64): + la.configure('opencl', cl_ctx=ctx_factory()) ndim = len(in_shape) - env = la.core.array.default_env() - env['cl_ctx'] = ctx_factory() - - A = la.core.Array('A', ndim=ndim, env=env) # noqa: N806 + A = la.core.Array('A', ndim=ndim) # noqa: N806 B = A.reshape(out_shape, order=order, name='B') # noqa: N806 data = np.random.rand(*in_shape).astype(dtype) diff --git a/test/test_transpose.py b/test/test_transpose.py index 5d9ee06..e587aaa 100644 --- a/test/test_transpose.py +++ b/test/test_transpose.py @@ -37,12 +37,10 @@ from pyopencl.tools import ( # noqa ((2, 3, 4, 6), (0, 2, 1, 3)), ]) def test_transpose(ctx_factory, test_shape, axes, dtype=np.float64): + la.configure('opencl', cl_ctx=ctx_factory()) ndim = len(test_shape) - env = la.core.array.default_env() - env['cl_ctx'] = ctx_factory() - - A = la.core.Array('A', ndim=ndim, env=env) # noqa: N806 + A = la.core.Array('A', ndim=ndim) # noqa: N806 B = A.transpose(axes=axes, name='B') # noqa: N806 data = np.random.rand(*test_shape).astype(dtype) diff --git a/test/test_ufunc.py b/test/test_ufunc.py index d01d2bd..312ec38 100644 --- a/test/test_ufunc.py +++ b/test/test_ufunc.py @@ -38,12 +38,11 @@ from pyopencl.tools import ( # noqa ('exp', [(2, 4, 8), (1, 2, 3), ], np.complex128), ]) def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): - env = la.core.array.default_env() - env['cl_ctx'] = ctx_factory() + la.configure('opencl', cl_ctx=ctx_factory()) ndim = len(shapes[0]) # symbolic code is reused for different shapes (same ndim) - mat = la.core.Array('A', ndim=ndim, dtype=dtype, env=env) + mat = la.core.Array('A', ndim=ndim, dtype=dtype) fmat = getattr(math, ufunc)(mat).with_name('B') for shape in shapes: @@ -65,14 +64,13 @@ def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): ('multiply', [(2, 4, 8), (1, 2, 3), ], np.complex128), ]) def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): - env = la.core.array.default_env() - env['cl_ctx'] = ctx_factory() + la.configure('opencl', cl_ctx=ctx_factory()) ndim = len(shapes[0]) sym_shape = tuple('s%d' % i for i in range(ndim)) - mat_a = la.core.Array('A', shape=sym_shape, dtype=dtype, env=env) - mat_b = la.core.Array('B', shape=sym_shape, dtype=dtype, env=env) + mat_a = la.core.Array('A', shape=sym_shape, dtype=dtype) + mat_b = la.core.Array('B', shape=sym_shape, dtype=dtype) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') @@ -106,16 +104,15 @@ def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): @pytest.mark.parametrize('ufunc', ['add', 'subtract', 'multiply', 'divide']) @pytest.mark.parametrize('dtype', [np.float64, ]) def test_binary_ufunc_with_broadcast(ctx_factory, ufunc, shapes, dtype): - env = la.core.array.default_env() - env['cl_ctx'] = ctx_factory() + la.configure('opencl', cl_ctx=ctx_factory()) ndim_a = len(shapes[0]) ndim_b = len(shapes[1]) sym_shape_a = tuple('s_a_%d' % i for i in range(ndim_a)) sym_shape_b = tuple('s_b_%d' % i for i in range(ndim_b)) - mat_a = la.core.Array('A', shape=sym_shape_a, dtype=dtype, env=env) - mat_b = la.core.Array('B', shape=sym_shape_b, dtype=dtype, env=env) + mat_a = la.core.Array('A', shape=sym_shape_a, dtype=dtype) + mat_b = la.core.Array('B', shape=sym_shape_b, dtype=dtype) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') -- GitLab From 706d7069ebffb1a7cce158e2077dd4aa6e277cad Mon Sep 17 00:00:00 2001 From: xywei Date: Sat, 21 Mar 2020 23:35:49 -0500 Subject: [PATCH 42/59] Add configure() for setting evaluation means (add missing files) #16 --- lappy/config.py | 80 ++++++++++++++++++++++++++++++++++++++++ lappy/target/__init__.py | 30 +++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 lappy/config.py create mode 100644 lappy/target/__init__.py diff --git a/lappy/config.py b/lappy/config.py new file mode 100644 index 0000000..f11b421 --- /dev/null +++ b/lappy/config.py @@ -0,0 +1,80 @@ +import os +from lappy.target import SUPPORTED_TARGETS + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +CONFIGS = {'target': None} + + +def configure(target=None, **kwargs): + target = None + + if 'LAPPY_TARGET' in os.environ: + target = os.environ['LAPPY_TARGET'] + if target not in SUPPORTED_TARGETS: + raise ValueError('unsupported LAPPY_TARGET=%s' % target) + + if target is not None: + try: + target, target_config = setup_target(target, **kwargs) + except ValueError: + raise RuntimeError('failed to setup the target %s' % target) + + if target is None: + # try all targets one by one until something works + for t in SUPPORTED_TARGETS: + try: + target, target_config = setup_target(t, **kwargs) + break + except ValueError: + continue + + if target is None: + raise RuntimeError('failed to setup a usable target') + target_config['target'] = target + + CONFIGS.update(target_config) + + +def setup_target(target, **kwargs): + if target not in SUPPORTED_TARGETS: + raise ValueError('unsupported LAPPY_TARGET=%s' % target) + + if target == 'opencl': + try: + import pyopencl as cl + ctx = kwargs.get('cl_ctx', cl.create_some_context()) + return target, {'cl_ctx': ctx} + except: # noqa: E722 + raise ValueError('failed to setup target %s' % target) + + if target == 'cuda': + try: + import pycuda.autoinit + ctx = kwargs.get('cu_ctx', pycuda.autoinit.context) + return target, {'cu_ctx': ctx} + except: # noqa: E722 + raise ValueError('failed to setup target %s' % target) + + raise ValueError('unknown target %s' % target) diff --git a/lappy/target/__init__.py b/lappy/target/__init__.py new file mode 100644 index 0000000..377e9c1 --- /dev/null +++ b/lappy/target/__init__.py @@ -0,0 +1,30 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +__doc__ = """ +Code generation and evaluation. Currently only the lazy evaluation via +``loopy`` is supported. +""" + +SUPPORTED_TARGETS = ['opencl', 'cuda'] -- GitLab From bee2b4900675c12ae8303986074b914c4c71d462 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 23 Mar 2020 22:51:49 -0500 Subject: [PATCH 43/59] Fix arange, and make statelessness have consistent definition --- experiments/lazy_arange.py | 20 ++++------- lappy/core/array.py | 66 +++++++++++++++++++++++-------------- lappy/core/function_base.py | 27 +++++++-------- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/experiments/lazy_arange.py b/experiments/lazy_arange.py index 5bfa512..c6c058a 100644 --- a/experiments/lazy_arange.py +++ b/experiments/lazy_arange.py @@ -1,18 +1,12 @@ import pyopencl as cl -from lappy.core.function_base import arange +from lappy import configure +from lappy.core.function_base import arange, linspace ctx = cl.create_some_context() -a = arange(10) - -# pyopencl context -a.env['cl_ctx'] = ctx +configure('opencl', cl_ctx=ctx) -# eval shape first -s = a.shape[0] -s.env = a.env -s.eval() +a = arange(10) +print(a.eval()) -# eval arange -a.env[s.name] = s.value.get() -a.eval() -print(a.value) +b = linspace(0, 1, 10) +print(b.eval()) diff --git a/lappy/core/array.py b/lappy/core/array.py index 43915cf..7024aa2 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -145,14 +145,11 @@ class LazyObjectBase(object): self.namespace = kwargs.pop("namespace", None) self.env = kwargs.pop("env", None) - # if either namespace or env is set, the obj has state if self.namespace is None: - if self.env is not None: - self.namespace = {self.name: (None, {'is_argument': True})} + self.namespace = {self.name: (None, {'is_argument': True})} - if self.env is None: - if self.namespace is not None: - self.env = default_env() + if self.name not in self.namespace: + self.namespace[self.name] = (None, {'is_argument': True}) if 'value' in kwargs.keys(): if self.is_stateless: @@ -217,6 +214,8 @@ class LazyObjectBase(object): if handled_selref: raise ValueError("illegal namespace (contains more " "than one self-references)") + assert isinstance(stateless_self, LazyObjectBase) + assert stateless_self.is_stateless obj = stateless_self handled_selref = True else: @@ -225,11 +224,13 @@ class LazyObjectBase(object): if nm in self.namespace: # try to merge information sfobj = self.namespace[nm][0] sftags = self.namespace[nm][1] - if sfobj.ndim != lapject.ndim: + if sfobj is None: + sfobj = self + if sfobj.ndim != obj.ndim: raise ValueError("conflicting ndims") - if sfobj.shape != lapject.shape: + if sfobj.shape != obj.shape: raise ValueError("conflicting shapes") - if sfobj.inames != lapject.inames: + if sfobj.inames != obj.inames: raise ValueError("conflicting inames") # merge tags @@ -347,11 +348,11 @@ class LazyObjectBase(object): @property def is_stateless(self): - return (self.env is None and self.namespace is None) + return self.env is None @property def has_state(self): - return (self.env is not None and self.namespace is not None) + return self.env is not None @property def arguments(self): @@ -486,10 +487,11 @@ class Array(LazyObjectBase): if name is not None: kwargs['name'] = name - # Array shall always be constructed as stateful - # (may be converted to stateless later) - if 'env' not in kwargs or kwargs['env'] is None: - kwargs['env'] = default_env() + stateless = kwargs.pop('stateless', False) + if not stateless: + # name default env + if 'env' not in kwargs or kwargs['env'] is None: + kwargs['env'] = default_env() super(Array, self).__init__(**kwargs) @@ -560,17 +562,19 @@ class Array(LazyObjectBase): cp_integer_domain = self.integer_domain.copy() if stateless or self.is_stateless: + stateless = True env = None - nms = None else: env = self.env.copy() - nms = self.namespace.copy() + + nms = self.namespace.copy() return self.__class__( name=self.name, ndim=self.ndim, inames=self.inames, shape=self.shape, dtype=self.dtype, + stateless=stateless, expr=self.expr, is_integral=self.is_integral, domain_expr=self.domain_expr, env=env, @@ -1265,10 +1269,10 @@ class Array(LazyObjectBase): if arr_name == self.name: # only specify output shape, and let the backend to do the malloc for out_s in self.shape: - if out_s.name not in self.env: - expansion_list.append(out_s) + if out_s not in self.env: + expansion_list.append(self[out_s]) else: - data_map[out_s.name] = self.env[out_s.name] + data_map[out_s] = self.env[out_s] elif isinstance(varr, np.ndarray): data_map[arr_name] = varr @@ -1395,6 +1399,24 @@ class Array(LazyObjectBase): lp.GlobalArg( self.name, shape=self._shape_str, dtype=self.dtype)) + # evaluate scalar expressions + # e.g.: size of arange() + for aname, (a, atag) in self.namespace.items(): + if a is None: + continue + if a.ndim == 0 and self.env.get(aname, None) is None: + try: + # FIXME: move this to a better place + import math + env = self.env.copy() + env['floor'] = math.floor + + self.env[aname] = evaluate(a.expr, env) + + except UnknownVariableError as e: + print(e) + pass + # substitute baked-in constants inline_scalars = True if inline_scalars: @@ -1443,9 +1465,6 @@ class Array(LazyObjectBase): kernel_data.append('...') - if '__lappy_array_3' in self.namespace: - print(self.namespace['__lappy_array_3']) - knl = lp.make_kernel( loop_domain, [ @@ -1486,7 +1505,6 @@ class Array(LazyObjectBase): print(knl) print(data_map) - print(CONFIGS) if CONFIGS['target'] == 'opencl': queue = cl.CommandQueue(CONFIGS['cl_ctx']) evt, lp_res = knl(queue, **data_map) diff --git a/lappy/core/function_base.py b/lappy/core/function_base.py index 8514a41..16ba256 100644 --- a/lappy/core/function_base.py +++ b/lappy/core/function_base.py @@ -6,10 +6,7 @@ from lappy.core.array import ( LazyObjectBase, Array, Scalar, Unsigned, to_lappy_scalar, to_lappy_unsigned) from lappy.core.ufuncs import power, floor -from lappy.core.tools import ( - check_and_merge_envs, check_and_merge_args, - check_and_merge_bargs, check_and_merge_temps, - check_and_merge_precs) +from lappy.core.tools import (check_and_merge_envs, check_and_merge_precs) __copyright__ = "Copyright (C) 2020 Xiaoyu Wei" @@ -64,29 +61,32 @@ def arange(start, stop=None, step=1, dtype=None, stop = to_lappy_scalar(stop) step = to_lappy_scalar(step) + num = to_lappy_unsigned( + floor((stop - start) / step)) + obj = dict() obj['ndim'] = 1 + obj['shape'] = (num.name, ) obj['env'] = check_and_merge_envs(start, stop, step) - obj['arguments'] = check_and_merge_args(start, stop, step) - obj['bound_arguments'] = check_and_merge_bargs(start, stop, step) - obj['temporaries'] = check_and_merge_temps(start, stop, step) + obj['namespace'] = {num.name: (num, dict())} obj['preconditions'] = check_and_merge_precs(start, stop, step) if name is not None: obj['name'] = name - num = to_lappy_unsigned( - floor((stop - start) / step)) - obj['shape'] = (num, ) - if dtype is None: dtype = np.result_type(start, stop, step) obj['dtype'] = dtype arr = Array(**obj) + iname = arr.inames[0] arr.expr = start.expr + step.expr * iname + arr._meld_namespace(start.namespace, start.as_stateless()) + arr._meld_namespace(stop.namespace, stop.as_stateless()) + arr._meld_namespace(step.namespace, step.as_stateless()) + return arr @@ -113,7 +113,7 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, if not isinstance(stop, Scalar): stop = to_lappy_scalar(stop) if not isinstance(num, Unsigned): - num = to_lappy_unsigned(stop) + num = to_lappy_unsigned(num) if endpoint: if num == 1: @@ -121,7 +121,8 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, else: step = (stop - start) / (num - 1) y = arange(0, num) * step + start - y[-1] = stop + # FIXME: add this line when support indexing + # y[-1] = stop else: step = (stop - start) / num y = arange(0, num) * step + start -- GitLab From 423e6f24a66cdddd320f68beb78687a1ad773d6e Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 13:53:03 -0500 Subject: [PATCH 44/59] Restrict __getitem__ for public usage --- lappy/core/array.py | 18 +++++++++--------- lappy/core/basic_ops.py | 2 +- lappy/core/broadcast.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index 7024aa2..cc34504 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -265,7 +265,7 @@ class LazyObjectBase(object): # }}} End namespace/env utils - def __getitem__(self, member_name): + def _extract(self, member_name): """Given a name, returns the lapject from the namespace, with the state attached. """ @@ -688,7 +688,7 @@ class Array(LazyObjectBase): return expr.__class__.init_arg_names == ('name',) assert allow_complex_expr or all( - is_trivial(self[s].expr) for s in self.shape) + is_trivial(self._extract(s).expr) for s in self.shape) if new_name is None: new_name = 'bound%d_' % self.__class__._counter + self.name @@ -785,8 +785,8 @@ class Array(LazyObjectBase): :param idx: a tuple of indices or slices. """ if isinstance(indices, str): - # get a member in the namespace with specified name - return super(Array, self).__getitem__(indices) + # structured dtype support + raise NotImplementedError() if not isinstance(indices, tuple): # masking @@ -1179,7 +1179,7 @@ class Array(LazyObjectBase): """Values for the shape (if not None), otherwise expressions. """ return tuple( - self.env[s] if s in self.env else self[s].expr + self.env[s] if s in self.env else self._extract(s).expr for s in self.shape) # }}} End private api @@ -1239,12 +1239,12 @@ class Array(LazyObjectBase): if name is None or name == self.name: sym_shape = self.shape elif name in self.namespace: - sym_shape = self[name].shape + sym_shape = self._extract(name).shape else: raise ValueError() for s, sym_s_name in zip(shape, sym_shape): - sym_s = self[sym_s_name] + sym_s = self._extract(sym_s_name) if sym_s.name in self.env: if self.env[sym_s_name] != s: warnings.warn( @@ -1320,7 +1320,7 @@ class Array(LazyObjectBase): # Value unknown, but has expression. # Try to expand the expression on inputs before runtime # (applies to scalar expression, like shape vars) - expansion_list.append(self[argname]) + expansion_list.append(self._extract(argname)) # Make sure not to include None's prior to evaluating expressions data_map = self._purge_nones(data_map) @@ -1480,7 +1480,7 @@ class Array(LazyObjectBase): for argname, arg in knl.arg_dict.items(): if arg.dtype is None: # array args - sym_arg = self[argname] + sym_arg = self._extract(argname) if sym_arg is not None: extra_dtype_dict[argname] = sym_arg.dtype diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index a9be519..10404df 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -177,7 +177,7 @@ class SubArrayFactory(object): """ assert isinstance(slc, slice) dim_name = self.base_arr.shape[iaxis] - dim = self.base_arr[dim_name] + dim = self.base_arr._extract(dim_name) step_none = slc.step is None if step_none: diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 701491b..630e2c1 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -109,7 +109,7 @@ class BroadcastResult(object): for arr in self.base_arrays: for s in arr.shape: if s not in expr_map: - expr_map[var(s)] = arr[s].expr + expr_map[var(s)] = arr._extract(s).expr else: # same name must have the same runtime values # (may have different expressions) -- GitLab From 3a8a54ab5418864a325cdd523a82f6c7b39e06f8 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 14:14:30 -0500 Subject: [PATCH 45/59] Add numpy consistency tests --- test/test_numpy_consistency.py | 36 +++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/test/test_numpy_consistency.py b/test/test_numpy_consistency.py index 6f22788..b524127 100644 --- a/test/test_numpy_consistency.py +++ b/test/test_numpy_consistency.py @@ -25,13 +25,11 @@ THE SOFTWARE. import numpy as np import lappy as ly +import pytest from warnings import warn -def test_array_public_interface(): - npi = dir(np.ndarray) - lpi = dir(ly.ndarray) - +def warn_diff(npi, lpi, name): missing = [] extra = [] @@ -43,8 +41,28 @@ def test_array_public_interface(): if fn not in npi: extra.append(fn) - warn("\n>>>> Missing array API functions:\n %s" - % ', '.join(missing)) - warn("\n>>>> Extra array API functions:\n %s" - % ', '.join(extra)) - # assert success + warn("\n>>>> Missing API in %s (%d):\n %s" + % (name, len(missing), ', '.join(missing))) + warn("\n>>>> Extra API in %s (%d):\n %s" + % (name, len(extra), ', '.join(extra))) + + return len(missing) + len(extra) + + +@pytest.mark.parametrize('path_lappy, path_numpy, name, tol', [ + (None, None, 'lappy', 1000), + ('ndarray', 'ndarray', 'ndarray', 1000), + ]) +def test_public_interface(path_lappy, path_numpy, name, tol): + if path_lappy is None: + npi = dir(np) + else: + npi = dir(getattr(np, path_numpy)) + + if path_lappy is None: + lpi = dir(ly) + else: + lpi = dir(getattr(ly, path_lappy)) + + distance = warn_diff(npi, lpi, name) + assert distance < tol -- GitLab From ad8b607890b6a43be322486c52c3cd66446f2a55 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 18:31:01 -0500 Subject: [PATCH 46/59] Declare functions prototypes for numpy array interface --- lappy/__init__.py | 13 +- lappy/core/__init__.py | 6 +- lappy/core/array.py | 403 +++++++++++++++++++++++++++++++-- test/test_numpy_consistency.py | 74 ++++-- 4 files changed, 457 insertions(+), 39 deletions(-) diff --git a/lappy/__init__.py b/lappy/__init__.py index a699ca5..62beeea 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -27,8 +27,17 @@ __all__ = [] from lappy.config import configure, CONFIGS __all__ += ["CONFIGS", "configure"] -from lappy.core import ndarray, to_lappy_array -__all__ += ["ndarray", "to_lappy_array"] +from lappy.core import to_lappy_array +__all__ += ["to_lappy_array"] from lappy.core.ufuncs import sin, cos, exp __all__ += ["sin", "cos", "exp"] + +from lappy.core.array import Array + + +class ndarray(Array): # noqa: N801 + pass + + +__all__ += ["ndarray", ] diff --git a/lappy/core/__init__.py b/lappy/core/__init__.py index f808938..ff6cbde 100644 --- a/lappy/core/__init__.py +++ b/lappy/core/__init__.py @@ -23,9 +23,7 @@ THE SOFTWARE. """ from lappy.core.array import ( - Array, ndarray, to_lappy_array, to_lappy_shape) + Array, to_lappy_array, to_lappy_shape) -__all__ = [ - "Array", "ndarray", - "to_lappy_array", "to_lappy_shape"] +__all__ = ["Array", "to_lappy_array", "to_lappy_shape"] diff --git a/lappy/core/array.py b/lappy/core/array.py index cc34504..50eecde 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -550,6 +550,18 @@ class Array(LazyObjectBase): # {{{ copy constructors, with_xxx(), as_xxx() + def __copy__(self): + """Used if copy.copy is called on an array. Returns a copy of the array. + + Equivalent to ``a.copy(stateless=False)``. + """ + return self.copy(stateless=False) + + def __deepcopy__(self): + """Used if copy.deepcopy is called on an array. + """ + raise NotImplementedError() + def copy(self, stateless=False): """Returns a new copy of self, where the attributes undergo a shallow copy (expect for the value). @@ -763,6 +775,23 @@ class Array(LazyObjectBase): def size(self): return self._shadow_size() + def itemsize(self): + """TODO: dtype expression + """ + if self.dtype is None: + # use numpy defaults + return np.dtype(self.dtype).itemsize + else: + return np.dtype(self.dtype).itemsize + + def nbytes(self): + return self.size * self.itemsize + + def __len__(self): + raise NotImplementedError() + + # {{{ basic ops + def reshape(self, newshape, order='C', name=None, inames=None): """Reshape. """ @@ -779,6 +808,80 @@ class Array(LazyObjectBase): T = transpose + def swapaxes(self, axis1, axis2): + """Return a view of the array with axis1 and axis2 interchanged. + """ + raise NotImplementedError() + + def squeeze(self): + """Remove single-dimensional entries from the shape of a. + """ + raise NotImplementedError() + + def repeat(self): + """Repeat elements of an array. + """ + raise NotImplementedError() + + def fill(self): + raise NotImplementedError() + + def astype(self): + raise NotImplementedError() + + def resize(self, new_shape, order='C'): + """Change shape and size of array. + Shrinking: array is flattened, resized, and reshaped. + Enlarging: as above, with missing entries filled with zeros. + """ + raise NotImplementedError() + + def flatten(self): + raise NotImplementedError() + + ravel = flatten # unlike number, the two are equivalent in lappy + + # }}} End basic ops + + # {{{ data io + + def dump(self, with_env=True): + """Dump the full lazy array object into a file. + """ + raise NotImplementedError() + + def dumps(self, with_env=True): + """Dump the full lazy array object into a string. + """ + raise NotImplementedError() + + # }}} End data io + + # {{{ getter/setter + + def item(self, *args): + """Copy an element of an array to a standard Python scalar + and return it. + """ + raise NotImplementedError() + + def itemset(self, *args): + """Insert scalar into an array + (scalar is cast to array’s dtype, if possible). + """ + raise NotImplementedError() + + def take(self): + """Return an array formed from the elements of a at the + given indices. + """ + raise NotImplementedError() + + def put(self, indices, values, mode='raise'): + """Set a.flat[n] = values[n] for all n in indices. + """ + raise NotImplementedError() + def __getitem__(self, indices): """Returns a new array by sub-indexing the current array. @@ -804,18 +907,32 @@ class Array(LazyObjectBase): sub_arr = arr_fac() return sub_arr + def __setitem__(self, index, value): + """Set item described by index. + """ + raise NotImplementedError() + + def __delitem__(self, key): + """Delete self[key]. + """ + raise NotImplementedError() + def __iter__(self): """Iterator support if ndim and shape are all fixed. Its behavior mimics numpy.ndarray.__iter__() """ raise NotImplementedError() - def nditer(self): + def flat(self): """Flat iterator support if ndim and shape are all fixed. Its behavior mimics numpy.ndarray.nditer() """ raise NotImplementedError() + # }}} End getter/setter + + # {{{ numpy protocols + def __array__(self): """(For numpy’s dispatch). When converting to a numpy array with ``numpy.array`` or ``numpy.asarray``, the lazy array is evaluated @@ -827,17 +944,30 @@ class Array(LazyObjectBase): else: return res.get() - # {{{ unary operations + @property + def __array_interface__(self): + """The array interface (array protocol) for data buffer access. + """ + if self.is_stateless: + raise ValueError("array value is unknown") + else: + raise NotImplementedError() - def __abs__(self): - from lappy.core.ufuncs import absolute - return absolute(self) + @classmethod + def __array_function__(cls, func, types, args, kwargs): + """Array function protocol. + https://numpy.org/neps/nep-0018-array-function-protocol.html + """ + raise NotImplementedError() - def __neg__(self): - from lappy.core.ufuncs import negative - return negative(self) + @classmethod + def __array_ufunc__(cls, ufunc, method, *inputs, **kwargs): + """Array ufunc protocol. + https://docs.scipy.org/doc/numpy/reference/ufuncs.html + """ + raise NotImplementedError() - # }}} End unary operations + # }}} End numpy protocols # {{{ arithmetics @@ -899,7 +1029,7 @@ class Array(LazyObjectBase): __truediv__ = __div__ - def __rdiv__(self, other): + def __rtruediv__(self, other): if not isinstance(other, Array): other_arr = to_lappy_array(other) else: @@ -907,7 +1037,11 @@ class Array(LazyObjectBase): from lappy.core.ufuncs import divide return divide(other_arr, self) - __rtruediv__ = __rdiv__ + def __divmod__(self, value): + raise NotImplementedError() + + def __rdivmod__(self, value): + raise NotImplementedError() def __pow__(self, other): if not isinstance(other, Array): @@ -937,6 +1071,57 @@ class Array(LazyObjectBase): def __rmod__(self, other): raise NotImplementedError() + def __iadd__(self, value): + raise NotImplementedError() + + def __isub__(self, value): + raise NotImplementedError() + + def __imul__(self, value): + raise NotImplementedError() + + def __idiv__(self, value): + raise NotImplementedError() + + def __itruediv__(self, value): + raise NotImplementedError() + + def __ipow__(self, value): + raise NotImplementedError() + + def __ifloordiv__(self, value): + raise NotImplementedError() + + def __imod__(self, value): + raise NotImplementedError() + + def __matmul__(self, value): + raise NotImplementedError() + + def __rmatmul__(self, value): + raise NotImplementedError() + + def __imatmul__(self, value): + raise NotImplementedError() + + def __lshift__(self): + raise NotImplementedError() + + def __rlshift__(self): + raise NotImplementedError() + + def __ilshift__(self): + raise NotImplementedError() + + def __rshift__(self): + raise NotImplementedError() + + def __rrshift__(self): + raise NotImplementedError() + + def __irshift__(self): + raise NotImplementedError() + # }}} End arithmetics # {{{ comparisons @@ -994,6 +1179,199 @@ class Array(LazyObjectBase): # }}} End comparisons + # {{{ logical ops + + def __and__(self, value): + raise NotImplementedError() + + def __rand__(self, value): + raise NotImplementedError() + + def __or__(self, value): + raise NotImplementedError() + + def __ror__(self, value): + raise NotImplementedError() + + def __xor__(self, value): + raise NotImplementedError() + + def __rxor__(self, value): + raise NotImplementedError() + + def __iand__(self, value): + raise NotImplementedError() + + def __ior__(self, value): + raise NotImplementedError() + + def __ixor__(self, value): + raise NotImplementedError() + + def __contains__(self, key): + raise NotImplementedError() + + def __invert__(self): + raise NotImplementedError() + + def all(self, axis=None, out=None, keepdims=False): + raise NotImplementedError() + + def any(self, axis=None, out=None, keepdims=False): + raise NotImplementedError() + + # }}} End logical ops + + # {{{ other maths + + def __abs__(self): + """|self| + """ + from lappy.core.ufuncs import absolute + return absolute(self) + + def __pos__(self): + """+self + """ + raise NotImplementedError() + + def __neg__(self): + """-self + """ + from lappy.core.ufuncs import negative + return negative(self) + + def __index__(self): + """Called to implement operator.index(), and whenever Python needs to + losslessly convert the numeric object to an integer object (such as in + slicing, or in the built-in bin(), hex() and oct() functions). + """ + if self.is_integral: + raise NotImplementedError() + else: + raise ValueError() + + def __complex__(self): + """Convert size-1 arrays to Python scalars. + """ + raise NotImplementedError() + + def __float__(self): + """Convert size-1 arrays to Python scalars. + """ + raise NotImplementedError() + + def __int__(self): + """Convert size-1 arrays to Python scalars. + """ + raise NotImplementedError() + + def round(self): + """Return a with each element rounded to the given number of + decimals. + """ + raise NotImplementedError() + + def conjugate(self): + raise NotImplementedError() + + conj = conjugate + + def real(self): + raise NotImplementedError() + + def imag(self): + raise NotImplementedError() + + def max(self, axis=None, out=None, keepdims=False, initial=None, where=True): + raise NotImplementedError() + + def min(self): + raise NotImplementedError() + + def sort(self): + raise NotImplementedError() + + def searchsorted(self): + """Find indices where elements of v should be inserted in a + to maintain order. + """ + raise NotImplementedError() + + def mean(self): + raise NotImplementedError() + + def sum(self): + raise NotImplementedError() + + def var(self): + raise NotImplementedError() + + def std(self): + raise NotImplementedError() + + def nonzero(self): + raise NotImplementedError() + + def partition(self): + raise NotImplementedError() + + def prod(self): + """Return the product of the array elements over the given axis. + """ + raise NotImplementedError() + + product = prod + + def ptp(self): + """Peak to peak (maximum - minimum) value along a given axis. + """ + raise NotImplementedError() + + def anom(self): + """Compute the anomalies (deviations from the arithmetic mean) + along the given axis. + """ + raise NotImplementedError() + + def argmax(self): + raise NotImplementedError() + + def argmin(self): + raise NotImplementedError() + + def argpartition(self): + raise NotImplementedError() + + def argsort(self): + raise NotImplementedError() + + def clip(self): + """Clip (limit) the values in an array. + """ + raise NotImplementedError() + + def cumprod(self): + """Return the cumulative product of elements along a given axis. + """ + raise NotImplementedError() + + def cumsum(self): + """Return the cumulative sum of the elements along the given axis. + """ + raise NotImplementedError() + + def diagonal(self): + raise NotImplementedError() + + def trace(self): + raise NotImplementedError() + + def dot(self): + raise NotImplementedError() + + # }}} End other maths + # }}} End public (numpy-compliant) api # {{{ private api @@ -1520,11 +1898,8 @@ class Array(LazyObjectBase): # }}} End evaluation related - # }}} End array class -ndarray = Array - # {{{ special subtypes of array # for more readable names and handy constructors diff --git a/test/test_numpy_consistency.py b/test/test_numpy_consistency.py index b524127..b9241fd 100644 --- a/test/test_numpy_consistency.py +++ b/test/test_numpy_consistency.py @@ -22,14 +22,43 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import numpy as np -import lappy as ly +import numpy # noqa: F401 +import lappy # noqa: F401 import pytest from warnings import warn - -def warn_diff(npi, lpi, name): +# part of the numpy api is not meant to be included. +NOSUP = [ + '__array_finalize__', '__array_prepare__', + '__array_wrap__', + '__getstate__', '__setstate__', '__array_priority__', + '__array_struct__', '__long__', '__setmask__', + '_baseclass', '_comparison', '_data', '_defaulthardmask', + '_defaultmask', '_delegate_binop', '_get_data', '_get_flat', + '_get_mask', '_get_recordmask', '_insert_masked_print', '_print_width', + '_print_width_1d', '_set_flat', '_set_mask', '_set_recordmask', + '_update_from', + + # not well-defined for lappy arrays, since there is no direct + # correspondence with data in memory. + 'iscontiguous', 'byteswap', 'newbyteorder', 'getfield', 'setfield', + 'strides', 'tobytes', 'tofile', 'tolist', 'tostring', 'view', + + # (no masked array support for now) + 'compress', 'compressed', 'count', 'base', 'baseclass', + 'choose', 'ctypes', 'data', 'fill_value', 'filled', + 'get_fill_value', 'get_imag', 'get_real', + 'harden_mask', 'hardmask', 'ids', 'mini', 'mask', 'recordmask', + 'set_fill_value', 'sharedmask', 'shrink_mask', 'soften_mask', + 'toflex', 'torecords', 'unshare_mask', + + # attributes set during construction + 'dtype', 'shape', 'flags', 'setflags', + ] + + +def warn_diff(npi, lpi, name, assert_extra_hidden=False): missing = [] extra = [] @@ -37,32 +66,39 @@ def warn_diff(npi, lpi, name): if fn not in lpi: missing.append(fn) + missing = [m for m in missing if m not in NOSUP] + for fn in lpi: if fn not in npi: extra.append(fn) - warn("\n>>>> Missing API in %s (%d):\n %s" - % (name, len(missing), ', '.join(missing))) - warn("\n>>>> Extra API in %s (%d):\n %s" - % (name, len(extra), ', '.join(extra))) + if len(missing) > 0: + warn("\n>>>> Missing API in %s (%d):\n %s" + % (name, len(missing), ', '.join(missing))) + if len(extra) > 0: + warn("\n>>>> Extra API in %s (%d):\n %s" + % (name, len(extra), ', '.join(extra))) + + if assert_extra_hidden: + assert all(ext.startswith('_') for ext in extra) return len(missing) + len(extra) @pytest.mark.parametrize('path_lappy, path_numpy, name, tol', [ - (None, None, 'lappy', 1000), - ('ndarray', 'ndarray', 'ndarray', 1000), + ('lappy', 'numpy', 'lappy', 1000), + ('lappy.ndarray', 'numpy.ndarray, numpy.ma.MaskedArray', 'ndarray', 1000), ]) def test_public_interface(path_lappy, path_numpy, name, tol): - if path_lappy is None: - npi = dir(np) - else: - npi = dir(getattr(np, path_numpy)) - - if path_lappy is None: - lpi = dir(ly) - else: - lpi = dir(getattr(ly, path_lappy)) + lpi = [] + npi = [] + for pl in path_lappy.split(', '): + exec('lpi += dir(%s)' % pl) + for pn in path_numpy.split(', '): + exec('npi += dir(%s)' % pn) + + lpi = sorted(list(set(lpi))) + npi = sorted(list(set(npi))) distance = warn_diff(npi, lpi, name) assert distance < tol -- GitLab From ab5bd84d4916f746fd4ae35d1841597b83fac47d Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 20:49:03 -0500 Subject: [PATCH 47/59] Separate eval without global state (and pass all tests) --- lappy/__init__.py | 13 +- lappy/config.py | 80 ---- lappy/core/array.py | 375 ++---------------- lappy/core/ufuncs.py | 5 +- lappy/eval/__init__.py | 57 +++ lappy/eval/compiler.py | 349 ++++++++++++++++ .../{target/__init__.py => eval/execution.py} | 18 +- test/test_broadcast.py | 12 +- test/test_reshape.py | 8 +- test/test_transpose.py | 10 +- test/test_ufunc.py | 26 +- 11 files changed, 489 insertions(+), 464 deletions(-) delete mode 100644 lappy/config.py create mode 100644 lappy/eval/__init__.py create mode 100644 lappy/eval/compiler.py rename lappy/{target/__init__.py => eval/execution.py} (75%) diff --git a/lappy/__init__.py b/lappy/__init__.py index 62beeea..4e472db 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -24,9 +24,6 @@ THE SOFTWARE. __all__ = [] -from lappy.config import configure, CONFIGS -__all__ += ["CONFIGS", "configure"] - from lappy.core import to_lappy_array __all__ += ["to_lappy_array"] @@ -34,10 +31,18 @@ from lappy.core.ufuncs import sin, cos, exp __all__ += ["sin", "cos", "exp"] from lappy.core.array import Array +from lappy.eval import Compiler, Executor class ndarray(Array): # noqa: N801 - pass + def eval(self, queue, check_preconditions=True): + if self.value is None: + compiler = Compiler(check_preconditions) + closure = compiler(self) + evaluator = Executor(closure) + res = evaluator(queue) + self.value = res[self.name] + return self.value __all__ += ["ndarray", ] diff --git a/lappy/config.py b/lappy/config.py deleted file mode 100644 index f11b421..0000000 --- a/lappy/config.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from lappy.target import SUPPORTED_TARGETS - -__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - - -CONFIGS = {'target': None} - - -def configure(target=None, **kwargs): - target = None - - if 'LAPPY_TARGET' in os.environ: - target = os.environ['LAPPY_TARGET'] - if target not in SUPPORTED_TARGETS: - raise ValueError('unsupported LAPPY_TARGET=%s' % target) - - if target is not None: - try: - target, target_config = setup_target(target, **kwargs) - except ValueError: - raise RuntimeError('failed to setup the target %s' % target) - - if target is None: - # try all targets one by one until something works - for t in SUPPORTED_TARGETS: - try: - target, target_config = setup_target(t, **kwargs) - break - except ValueError: - continue - - if target is None: - raise RuntimeError('failed to setup a usable target') - target_config['target'] = target - - CONFIGS.update(target_config) - - -def setup_target(target, **kwargs): - if target not in SUPPORTED_TARGETS: - raise ValueError('unsupported LAPPY_TARGET=%s' % target) - - if target == 'opencl': - try: - import pyopencl as cl - ctx = kwargs.get('cl_ctx', cl.create_some_context()) - return target, {'cl_ctx': ctx} - except: # noqa: E722 - raise ValueError('failed to setup target %s' % target) - - if target == 'cuda': - try: - import pycuda.autoinit - ctx = kwargs.get('cu_ctx', pycuda.autoinit.context) - return target, {'cu_ctx': ctx} - except: # noqa: E722 - raise ValueError('failed to setup target %s' % target) - - raise ValueError('unknown target %s' % target) diff --git a/lappy/core/array.py b/lappy/core/array.py index 50eecde..cc873c0 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1,23 +1,15 @@ from __future__ import division, absolute_import, print_function import warnings -import logging -from traceback import format_list, extract_stack - -import pyopencl as cl -import pyopencl.array - import numpy as np -import loopy as lp import islpy as isl import pymbolic -from pymbolic import var, evaluate, substitute +from pymbolic import var, evaluate from pymbolic.primitives import Lookup, Subscript, Variable, Product from pymbolic.mapper.evaluator import UnknownVariableError from pprint import pformat -from lappy import CONFIGS copyright__ = "Copyright (C) 2019 Sophia Lin, Andreas Kloeckner and Xiaoyu Wei" @@ -48,10 +40,6 @@ def default_env(): return dict() -class PreconditionNotMetError(Exception): - pass - - class IndexError(Exception): pass @@ -1382,6 +1370,31 @@ class Array(LazyObjectBase): return tuple(self.env[s.name] if s.name in self.env else None for s in self.shape) + def _set_shape_values(self, shape, name=None): + """Instantiate shape of a namespace member with concrete values. + """ + for s in tuple(shape): + assert int(s) == s + assert s >= 0 + + if name is None or name == self.name: + sym_shape = self.shape + elif name in self.namespace: + sym_shape = self._extract(name).shape + else: + raise ValueError() + + for s, sym_s_name in zip(shape, sym_shape): + sym_s = self._extract(sym_s_name) + if sym_s.name in self.env: + if self.env[sym_s_name] != s: + warnings.warn( + "captured shape value for %s is overwritten " + "(%s --> %s)" % ( + sym_s.name, + str(self.env[sym_s.name]), str(s))) + self.env[sym_s_name] = s + def _get_value(self, name=None): """Try to get the value of a captured variable. Returns None if the process fails. @@ -1562,342 +1575,6 @@ class Array(LazyObjectBase): # }}} End private api - # {{{ evaluation related - - def _check_preconditions(self, context): - """Call checkers in the list of preconditions. - - :param context: a dict which is passed to all checkers. - """ - failed_checks = [] - for checker in self.preconditions: - try: - res = checker(context) - if res is not None: - if isinstance(res, bool): - if res: - pass - else: - raise PreconditionNotMetError( - "precondition checker failed: %s" - % str(checker)) - else: - raise ValueError( - "cannot understand the return value " - "of the precondition checker %s" - % str(checker)) - except Exception as e: # noqa: W0703 - print(e) - failed_checks.append(checker) - - err_msgs = [] - for fc in failed_checks: - msg = ("Precondition %s not met, which was imposed at\n\n" - % str(checker)) - if hasattr(fc, "frame"): - msg = msg + '\n'.join(format_list(extract_stack( - fc.frame))) - err_msgs.append(msg) - if len(failed_checks) > 0: - precon_err_msg = ( - "%d out of %d preconditions are not met" - % (len(failed_checks), len(self.preconditions))) - for im, msg in enumerate(err_msgs): - precon_err_msg = (precon_err_msg + '\n\n' - + '[%d / %d]: ' % (im + 1, len(failed_checks)) + msg) - raise PreconditionNotMetError(precon_err_msg) - - def _set_shape_values(self, shape, name=None): - """Instantiate shape of a namespace member with concrete values. - """ - for s in tuple(shape): - assert int(s) == s - assert s >= 0 - - if name is None or name == self.name: - sym_shape = self.shape - elif name in self.namespace: - sym_shape = self._extract(name).shape - else: - raise ValueError() - - for s, sym_s_name in zip(shape, sym_shape): - sym_s = self._extract(sym_s_name) - if sym_s.name in self.env: - if self.env[sym_s_name] != s: - warnings.warn( - "captured shape value for %s is overwritten " - "(%s --> %s)" % ( - sym_s.name, - str(self.env[sym_s.name]), str(s))) - self.env[sym_s_name] = s - - def _get_data_mapping(self, knl=None): - """Make a data mapping using known data, tailored for giving inputs to - the loopy kernel. Returns all known information if knl is None. - - :param knl: the loopy kernel - """ - data_map = {} - expansion_list = [] - - # gather captured data - for arr_name, varr in self.env.items(): - - if arr_name == self.name: - # only specify output shape, and let the backend to do the malloc - for out_s in self.shape: - if out_s not in self.env: - expansion_list.append(self[out_s]) - else: - data_map[out_s] = self.env[out_s] - - elif isinstance(varr, np.ndarray): - data_map[arr_name] = varr - - elif varr is None: - pass # self - - elif np.isscalar(varr): - data_map[arr_name] = varr - - elif isinstance(varr, cl.array.Array): - data_map[arr_name] = varr - - else: - raise RuntimeError("unrecogonized captured variable %s" % arr_name) - - if knl is None: - # gather all known shapes - - for s in self.shape: - if s in data_map: - continue - if self._get_value(s) is not None: - data_map[s] = self._get_value(s) - - for arg, _ in self.namespace.values(): - if arg is None: - continue # skip self - assert isinstance(arg, Array) - for s in arg.shape: - if s not in self.env: - expansion_list.append(self[s]) - elif arg._get_value(s) is not None: - data_map[s] = self.env[s] - - else: - # try to get as much extra data that loopy wants as possible - # also evaluates the shape expressions - for argname, arg in knl.arg_dict.items(): - if argname in data_map: - continue - if argname in self.env: - data_map[argname] = self.env[argname] - elif argname in self.namespace: - # Value unknown, but has expression. - # Try to expand the expression on inputs before runtime - # (applies to scalar expression, like shape vars) - expansion_list.append(self._extract(argname)) - - # Make sure not to include None's prior to evaluating expressions - data_map = self._purge_nones(data_map) - - for se in expansion_list: - try: - seval = evaluate(se.expr, data_map) - except UnknownVariableError: - if 0: - warnings.warn( - "cannot get value for %s prior to calling " - "the loopy kernel" % se.name) - continue - - if se.name in data_map: - assert (seval is None) or (seval == data_map[se.name]) - else: - data_map[se.name] = seval - - # Remove keyward arguments not needed by the kernel - rm_args = [] - if knl is not None: - for argname in data_map.keys(): - if argname not in knl.arg_dict: - rm_args.append(argname) - for rmarg in rm_args: - del data_map[rmarg] - - return data_map - - def _purge_nones(self, data_map): - """Purge None-valued entries from a data map. - """ - entries_to_purge = [] - for key, val in data_map.items(): - if val is None: - entries_to_purge.append(key) - for key in entries_to_purge: - data_map.pop(key) - return data_map - - def eval(self, shape_dtype=np.int32): - """Evaluates the array expression after binding all the arguments. - Note that this function has side effects: it sets self.value to the - evaluation result. If the results is available, it is returned without - re-computing. - """ - if self.value is not None: - logging.info("cache hit when evaluating %s" % self.name) - return self.value - - # context: where the preconditions are checked - # data_map: passed to the loopy kernel - # scalar_map: inlined scalars - # - # context = data_map + scalar_map - context = dict() - - # expression handling - rhs_expr = self.expr - if self.ndim == 0: - lhs_expr = var(self.name)[var(self.name + '_dummy_iname')] - else: - lhs_expr = var(self.name)[self.inames] - - loop_domain = self._generate_index_domains() - - # collect kernel args - kernel_data = [] - if self.ndim == 0: - kernel_data.append( - lp.GlobalArg( - self.name, shape=(1, ), dtype=self.dtype)) - else: - kernel_data.append( - lp.GlobalArg( - self.name, shape=self._shape_str, dtype=self.dtype)) - - # evaluate scalar expressions - # e.g.: size of arange() - for aname, (a, atag) in self.namespace.items(): - if a is None: - continue - if a.ndim == 0 and self.env.get(aname, None) is None: - try: - # FIXME: move this to a better place - import math - env = self.env.copy() - env['floor'] = math.floor - - self.env[aname] = evaluate(a.expr, env) - - except UnknownVariableError as e: - print(e) - pass - - # substitute baked-in constants - inline_scalars = True - if inline_scalars: - scalar_map = dict() - for vn, (vv, vtag) in self.namespace.items(): - if vv is None: - continue - if vv.ndim == 0: - scalar_map[vn] = self._get_value(vn) - scalar_map = self._purge_nones(scalar_map) - context.update(scalar_map) - rhs_expr = substitute(rhs_expr, scalar_map) - - # add more kernel args (reduces a log of guessing time) - # e.g., link array shape vars with the arrays, dtypes, etc. - # - # (the trick is to add no more than actually needed) - # FIXME: let loopy.ArgumentGuesser to find out what are needed. - for arr_name, (varr, tag) in self.namespace.items(): - if arr_name == self.name: - continue # self (output) handled above - if isinstance(varr, Array) and varr.ndim > 0: - # for now, we only specify inputs - if not tag.get('is_argument'): - continue - if varr.dtype is None: - dtype = np.int32 if varr.is_integral else np.float64 - else: - dtype = varr.dtype - kernel_data.append( - lp.GlobalArg( - arr_name, - dtype=dtype, - shape=varr._shape_str)) - elif isinstance(varr, Array) and varr.ndim == 0: - if varr.dtype is None: - dtype = np.int32 if varr.is_integral else np.float64 - else: - dtype = varr.dtype - kernel_data.append( - lp.ValueArg(arr_name, dtype=dtype)) - else: - pass - # warnings.warn( - # "cannot find shape information of %s" % arr_name) - - kernel_data.append('...') - - knl = lp.make_kernel( - loop_domain, - [ - lp.Assignment(lhs_expr, rhs_expr) - ], - kernel_data, - lang_version=(2018, 2)) - - knl = lp.set_options(knl, return_dict=True) - - # help with dtype inference - extra_dtype_dict = {} - for argname, arg in knl.arg_dict.items(): - if arg.dtype is None: - # array args - sym_arg = self._extract(argname) - if sym_arg is not None: - extra_dtype_dict[argname] = sym_arg.dtype - - # shape args - if argname in self.shape: - extra_dtype_dict[argname] = shape_dtype - for arr_arg in self.bound_arguments.values(): - if arr_arg is None: - continue - if argname in arr_arg.shape: - extra_dtype_dict[argname] = shape_dtype - - knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) - - data_map = self._get_data_mapping(knl) - context.update(data_map) - - # check preconditions - self._check_preconditions(context) - - if 0: - print(knl) - print(data_map) - - if CONFIGS['target'] == 'opencl': - queue = cl.CommandQueue(CONFIGS['cl_ctx']) - evt, lp_res = knl(queue, **data_map) - else: - raise NotImplementedError() - - if self.ndim == 0: - self.value = lp_res[self.name][0] - else: - self.value = lp_res[self.name] - - return self.value - - # }}} End evaluation related - # }}} End array class # {{{ special subtypes of array diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 6f4ad85..0ec69be 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -25,6 +25,7 @@ import numpy as np from pymbolic import var, substitute from pymbolic.primitives import Min, Max +import lappy from lappy.core.array import Array, to_lappy_array from lappy.core.primitives import FloatingPointRemainder from lappy.core.broadcast import broadcast @@ -140,7 +141,7 @@ class UnaryOperation(UFunc): (a.is_integral and self.preserve_integral) or self.is_integral, 'integer_domain': new_integer_domain, } - arr = Array(**obj) + arr = lappy.ndarray(**obj) # update self reference arr.namespace[a.name] = ( @@ -300,7 +301,7 @@ class BinaryOperation(UFunc): or self.is_integral, 'integer_domain': new_integer_domain, } - arr = Array(**obj) + arr = lappy.ndarray(**obj) arr.namespace[arr.name][1]['is_argument'] = False arr._meld_namespace(a.namespace.copy(), a.as_stateless()) diff --git a/lappy/eval/__init__.py b/lappy/eval/__init__.py new file mode 100644 index 0000000..536f4ff --- /dev/null +++ b/lappy/eval/__init__.py @@ -0,0 +1,57 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +__doc__ = """ +Code generation and evaluation. +""" + + +class Closure(object): + """Code and data container. + """ + def __init__(self, out_names, lpknl, data_map): + self.out_names = out_names + self.kernel = lpknl + self.data_map = data_map + assert self._check_context() + + def _check_context(self): + """Check if data_map has consistent context. + """ + ctx = None + for key, val in self.data_map.items(): + if hasattr(val, 'context'): + if ctx is None: + ctx = val.context + elif ctx == val.context: + continue + else: + return False + self.context = ctx + return True + + +from lappy.eval.execution import Executor +from lappy.eval.compiler import Compiler +__all__ = ['Compiler', 'Closure', 'Executor'] diff --git a/lappy/eval/compiler.py b/lappy/eval/compiler.py new file mode 100644 index 0000000..e98baab --- /dev/null +++ b/lappy/eval/compiler.py @@ -0,0 +1,349 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import warnings +from traceback import format_list, extract_stack + +import numpy as np +import loopy as lp +import pyopencl as cl + +from pymbolic import var, substitute, evaluate +from pymbolic.mapper.evaluator import UnknownVariableError + +from lappy.core.array import Array +from lappy.eval import Closure + + +class PreconditionNotMetError(Exception): + pass + + +class Compiler(object): + def __init__(self, check_preconditions=True): + self.check_preconditions = check_preconditions + + def __call__(self, *args): + """When given a few of :class:`Array` objects, make one loopy + kernel that evaluates all of them. + """ + if len(args) > 1: + raise NotImplementedError() + + out_names = [] + for arr in args: + assert isinstance(arr, Array) + out_names.append(arr.name) + + knl, context = self.compile(*args) + data_map = self._get_data_mapping(arr, knl) + context.update(data_map) + + if self.check_preconditions: + self._check_preconditions(arr.preconditions, context) + + return Closure(out_names, knl, data_map) + + def _check_preconditions(self, preconditions, context): + """Call checkers in the list of preconditions. + + :param context: a dict which is passed to all checkers. + """ + failed_checks = [] + for checker in preconditions: + try: + res = checker(context) + if res is not None: + if isinstance(res, bool): + if res: + pass + else: + raise PreconditionNotMetError( + "precondition checker failed: %s" + % str(checker)) + else: + raise ValueError( + "cannot understand the return value " + "of the precondition checker %s" + % str(checker)) + except Exception as e: # noqa: W0703 + print(e) + failed_checks.append(checker) + + err_msgs = [] + for fc in failed_checks: + msg = ("Precondition %s not met, which was imposed at\n\n" + % str(checker)) + if hasattr(fc, "frame"): + msg = msg + '\n'.join(format_list(extract_stack( + fc.frame))) + err_msgs.append(msg) + if len(failed_checks) > 0: + precon_err_msg = ( + "%d out of %d preconditions are not met" + % (len(failed_checks), len(preconditions))) + for im, msg in enumerate(err_msgs): + precon_err_msg = (precon_err_msg + '\n\n' + + '[%d / %d]: ' % (im + 1, len(failed_checks)) + msg) + raise PreconditionNotMetError(precon_err_msg) + + def _get_data_mapping(self, arr, knl=None): + """Make a data mapping using known data, tailored for giving inputs to + the loopy kernel. Returns all known information if knl is None. + + :param knl: the loopy kernel + """ + data_map = {} + expansion_list = [] + + # gather captured data + for arr_name, varr in arr.env.items(): + + if arr_name == arr.name: + # only specify output shape, and let the backend to do the malloc + for out_s in arr.shape: + if out_s not in arr.env: + expansion_list.append(arr._extract(out_s)) + else: + data_map[out_s] = arr.env[out_s] + + elif isinstance(varr, np.ndarray): + data_map[arr_name] = varr + + elif varr is None: + pass # self + + elif np.isscalar(varr): + data_map[arr_name] = varr + + elif isinstance(varr, cl.array.Array): + data_map[arr_name] = varr + + else: + raise RuntimeError("unrecogonized captured variable %s" % arr_name) + + if knl is None: + # gather all known shapes + + for s in arr.shape: + if s in data_map: + continue + if arr._get_value(s) is not None: + data_map[s] = arr._get_value(s) + + for arg, _ in arr.namespace.values(): + if arg is None: + continue # skip self + assert isinstance(arg, Array) + for s in arg.shape: + if s not in arr.env: + expansion_list.append(arr._extract(s)) + elif arg._get_value(s) is not None: + data_map[s] = arr.env[s] + + else: + # try to get as much extra data that loopy wants as possible + # also evaluates the shape expressions + for argname, arg in knl.arg_dict.items(): + if argname in data_map: + continue + if argname in arr.env: + data_map[argname] = arr.env[argname] + elif argname in arr.namespace: + # Value unknown, but has expression. + # Try to expand the expression on inputs before runtime + # (applies to scalar expression, like shape vars) + expansion_list.append(arr._extract(argname)) + + # Make sure not to include None's prior to evaluating expressions + data_map = self._purge_nones(data_map) + + for se in expansion_list: + try: + seval = evaluate(se.expr, data_map) + except UnknownVariableError: + if 0: + warnings.warn( + "cannot get value for %s prior to calling " + "the loopy kernel" % se.name) + continue + + if se.name in data_map: + assert (seval is None) or (seval == data_map[se.name]) + else: + data_map[se.name] = seval + + # Remove keyward arguments not needed by the kernel + rm_args = [] + if knl is not None: + for argname in data_map.keys(): + if argname not in knl.arg_dict: + rm_args.append(argname) + for rmarg in rm_args: + del data_map[rmarg] + + return data_map + + def _purge_nones(self, data_map): + """Purge None-valued entries from a data map. + """ + entries_to_purge = [] + for key, val in data_map.items(): + if val is None: + entries_to_purge.append(key) + for key in entries_to_purge: + data_map.pop(key) + return data_map + + def compile(self, *args, **kwargs): + # context: where the preconditions are checked + # data_map: passed to the loopy kernel + # scalar_map: inlined scalars + # + # context = data_map + scalar_map + context = dict() + + if len(args) > 1: + raise NotImplementedError() + arr = args[0] + + shape_dtype = kwargs.pop('shape_dtype', np.int32) + + # expression handling + rhs_expr = arr.expr + if arr.ndim == 0: + lhs_expr = var(arr.name)[var(arr.name + '_dummy_iname')] + else: + lhs_expr = var(arr.name)[arr.inames] + + loop_domain = arr._generate_index_domains() + + # collect kernel args + kernel_data = [] + if arr.ndim == 0: + kernel_data.append( + lp.GlobalArg( + arr.name, shape=(1, ), dtype=arr.dtype)) + else: + kernel_data.append( + lp.GlobalArg( + arr.name, shape=arr._shape_str, dtype=arr.dtype)) + + # evaluate scalar expressions + # e.g.: size of arange() + for aname, (a, atag) in arr.namespace.items(): + if a is None: + continue + if a.ndim == 0 and arr.env.get(aname, None) is None: + try: + # FIXME: move this to a better place + import math + env = arr.env.copy() + env['floor'] = math.floor + + arr.env[aname] = evaluate(a.expr, env) + + except UnknownVariableError as e: + print(e) + pass + + # substitute baked-in constants + inline_scalars = True + if inline_scalars: + scalar_map = dict() + for vn, (vv, vtag) in arr.namespace.items(): + if vv is None: + continue + if vv.ndim == 0: + scalar_map[vn] = arr._get_value(vn) + scalar_map = self._purge_nones(scalar_map) + context.update(scalar_map) + rhs_expr = substitute(rhs_expr, scalar_map) + + # add more kernel args (reduces a log of guessing time) + # e.g., link array shape vars with the arrays, dtypes, etc. + # + # (the trick is to add no more than actually needed) + # FIXME: let loopy.ArgumentGuesser to find out what are needed. + for arr_name, (varr, tag) in arr.namespace.items(): + if arr_name == arr.name: + continue # arr (output) handled above + if isinstance(varr, Array) and varr.ndim > 0: + # for now, we only specify inputs + if not tag.get('is_argument'): + continue + if varr.dtype is None: + dtype = np.int32 if varr.is_integral else np.float64 + else: + dtype = varr.dtype + kernel_data.append( + lp.GlobalArg( + arr_name, + dtype=dtype, + shape=varr._shape_str)) + elif isinstance(varr, Array) and varr.ndim == 0: + if varr.dtype is None: + dtype = np.int32 if varr.is_integral else np.float64 + else: + dtype = varr.dtype + kernel_data.append( + lp.ValueArg(arr_name, dtype=dtype)) + else: + pass + # warnings.warn( + # "cannot find shape information of %s" % arr_name) + + kernel_data.append('...') + + knl = lp.make_kernel( + loop_domain, + [ + lp.Assignment(lhs_expr, rhs_expr) + ], + kernel_data, + lang_version=(2018, 2)) + + knl = lp.set_options(knl, return_dict=True) + + # help with dtype inference + extra_dtype_dict = {} + for argname, arg in knl.arg_dict.items(): + if arg.dtype is None: + # array args + sym_arg = arr._extract(argname) + if sym_arg is not None: + extra_dtype_dict[argname] = sym_arg.dtype + + # shape args + if argname in arr.shape: + extra_dtype_dict[argname] = shape_dtype + for arr_arg in arr.bound_arguments.values(): + if arr_arg is None: + continue + if argname in arr_arg.shape: + extra_dtype_dict[argname] = shape_dtype + + knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) + + return knl, context diff --git a/lappy/target/__init__.py b/lappy/eval/execution.py similarity index 75% rename from lappy/target/__init__.py rename to lappy/eval/execution.py index 377e9c1..66f444f 100644 --- a/lappy/target/__init__.py +++ b/lappy/eval/execution.py @@ -1,6 +1,6 @@ from __future__ import division, absolute_import, print_function -__copyright__ = "Copyright (C) 2019 Xiaoyu Wei" +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,9 +22,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -__doc__ = """ -Code generation and evaluation. Currently only the lazy evaluation via -``loopy`` is supported. -""" +from lappy.eval import Closure + + +class Executor(object): + def __init__(self, closure): + assert isinstance(closure, Closure) + self.closure = closure -SUPPORTED_TARGETS = ['opencl', 'cuda'] + def __call__(self, queue): + evt, lp_res = self.closure.kernel( + queue, **self.closure.data_map) + return lp_res diff --git a/test/test_broadcast.py b/test/test_broadcast.py index cd73001..7996cfa 100644 --- a/test/test_broadcast.py +++ b/test/test_broadcast.py @@ -25,8 +25,9 @@ THE SOFTWARE. import pytest import numpy as np from pymbolic import evaluate -from lappy.core.array import Array +from lappy import ndarray from lappy.core.broadcast import broadcast +from lappy.eval import Compiler @pytest.mark.parametrize('shapes', [ @@ -44,7 +45,7 @@ def test_broadcast_rules(shapes): nparrs = [np.zeros(s) for s in shapes] numpy_res = np.broadcast(*nparrs) - laarrs = [Array(shape=s) for s in shapes] + laarrs = [ndarray(shape=s) for s in shapes] lappy_res = broadcast(*laarrs) assert numpy_res.ndim == lappy_res.ndim @@ -70,7 +71,7 @@ def test_symbolic_broadcast_rules(shapes): nparrs = [np.zeros(s) for s in shapes] numpy_res = np.broadcast(*nparrs) - laarrs = [Array(ndim=nd) for nd in ndims] + laarrs = [ndarray(ndim=nd) for nd in ndims] lappy_res = broadcast(*laarrs) assert numpy_res.ndim == lappy_res.ndim @@ -78,7 +79,10 @@ def test_symbolic_broadcast_rules(shapes): # inform lappy with actual shapes to evaluate exprs for i in range(len(laarrs)): laarrs[i] = laarrs[i].with_shape_data(shapes[i]) - arr_contexts = [arr._get_data_mapping() for arr in laarrs] + + compiler = Compiler() + closures = [compiler(arr) for arr in laarrs] + arr_contexts = [clos.data_map for clos in closures] broadcast_ctx = {} for ctx in arr_contexts: broadcast_ctx.update(ctx) diff --git a/test/test_reshape.py b/test/test_reshape.py index 66fcb5a..d117e82 100644 --- a/test/test_reshape.py +++ b/test/test_reshape.py @@ -23,6 +23,7 @@ THE SOFTWARE. """ import pytest +import pyopencl as cl import lappy as la import numpy as np @@ -49,17 +50,18 @@ from pyopencl.tools import ( # noqa ]) @pytest.mark.parametrize('order', ['C', 'F']) def test_reshape(ctx_factory, in_shape, out_shape, order, dtype=np.float64): - la.configure('opencl', cl_ctx=ctx_factory()) + cl_ctx = ctx_factory() + queue = cl.CommandQueue(cl_ctx) ndim = len(in_shape) - A = la.core.Array('A', ndim=ndim) # noqa: N806 + A = la.ndarray('A', ndim=ndim) # noqa: N806 B = A.reshape(out_shape, order=order, name='B') # noqa: N806 data = np.random.rand(*in_shape).astype(dtype) C = B.with_data(A=data).with_name('C') # noqa: N806 - lappy_val = C.eval() + lappy_val = C.eval(queue) numpy_val = data.reshape(out_shape, order=order) assert np.allclose(lappy_val, numpy_val) diff --git a/test/test_transpose.py b/test/test_transpose.py index e587aaa..8ba0b4c 100644 --- a/test/test_transpose.py +++ b/test/test_transpose.py @@ -23,6 +23,7 @@ THE SOFTWARE. """ import pytest +import pyopencl as cl import lappy as la import numpy as np @@ -37,19 +38,18 @@ from pyopencl.tools import ( # noqa ((2, 3, 4, 6), (0, 2, 1, 3)), ]) def test_transpose(ctx_factory, test_shape, axes, dtype=np.float64): - la.configure('opencl', cl_ctx=ctx_factory()) + cl_ctx = ctx_factory() + queue = cl.CommandQueue(cl_ctx) ndim = len(test_shape) - A = la.core.Array('A', ndim=ndim) # noqa: N806 + A = la.ndarray('A', ndim=ndim) # noqa: N806 B = A.transpose(axes=axes, name='B') # noqa: N806 data = np.random.rand(*test_shape).astype(dtype) C = B.with_data(A=data).with_name('C') # noqa: N806 - print(repr(C)) - print(C.inames) - lappy_val = C.eval() + lappy_val = C.eval(queue) numpy_val = data.transpose(axes) assert np.allclose(lappy_val, numpy_val) diff --git a/test/test_ufunc.py b/test/test_ufunc.py index 312ec38..8edb082 100644 --- a/test/test_ufunc.py +++ b/test/test_ufunc.py @@ -24,6 +24,7 @@ THE SOFTWARE. import numpy as np import lappy as la +import pyopencl as cl import lappy.core.ufuncs as math import pytest @@ -38,11 +39,12 @@ from pyopencl.tools import ( # noqa ('exp', [(2, 4, 8), (1, 2, 3), ], np.complex128), ]) def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): - la.configure('opencl', cl_ctx=ctx_factory()) + cl_ctx = ctx_factory() + queue = cl.CommandQueue(cl_ctx) ndim = len(shapes[0]) # symbolic code is reused for different shapes (same ndim) - mat = la.core.Array('A', ndim=ndim, dtype=dtype) + mat = la.ndarray('A', ndim=ndim, dtype=dtype) fmat = getattr(math, ufunc)(mat).with_name('B') for shape in shapes: @@ -51,7 +53,7 @@ def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): bound_mat = fmat.with_data(A=mat_data).with_name('C') # eval - lappy_res = bound_mat.eval() + lappy_res = bound_mat.eval(queue) numpy_res = getattr(np, ufunc)(mat_data) # compare with numpy @@ -64,13 +66,14 @@ def test_unary_ufunc(ctx_factory, ufunc, shapes, dtype): ('multiply', [(2, 4, 8), (1, 2, 3), ], np.complex128), ]) def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): - la.configure('opencl', cl_ctx=ctx_factory()) + cl_ctx = ctx_factory() + queue = cl.CommandQueue(cl_ctx) ndim = len(shapes[0]) sym_shape = tuple('s%d' % i for i in range(ndim)) - mat_a = la.core.Array('A', shape=sym_shape, dtype=dtype) - mat_b = la.core.Array('B', shape=sym_shape, dtype=dtype) + mat_a = la.ndarray('A', shape=sym_shape, dtype=dtype) + mat_b = la.ndarray('B', shape=sym_shape, dtype=dtype) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') @@ -85,7 +88,7 @@ def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): ).with_name('C') # eval - lappy_res = bound_mat.eval() + lappy_res = bound_mat.eval(queue) numpy_res = getattr(np, ufunc)(mat_a_data, mat_b_data) # compare with numpy @@ -104,15 +107,16 @@ def test_binary_ufunc(ctx_factory, ufunc, shapes, dtype): @pytest.mark.parametrize('ufunc', ['add', 'subtract', 'multiply', 'divide']) @pytest.mark.parametrize('dtype', [np.float64, ]) def test_binary_ufunc_with_broadcast(ctx_factory, ufunc, shapes, dtype): - la.configure('opencl', cl_ctx=ctx_factory()) + cl_ctx = ctx_factory() + queue = cl.CommandQueue(cl_ctx) ndim_a = len(shapes[0]) ndim_b = len(shapes[1]) sym_shape_a = tuple('s_a_%d' % i for i in range(ndim_a)) sym_shape_b = tuple('s_b_%d' % i for i in range(ndim_b)) - mat_a = la.core.Array('A', shape=sym_shape_a, dtype=dtype) - mat_b = la.core.Array('B', shape=sym_shape_b, dtype=dtype) + mat_a = la.ndarray('A', shape=sym_shape_a, dtype=dtype) + mat_b = la.ndarray('B', shape=sym_shape_b, dtype=dtype) fmat = getattr(math, ufunc)(mat_a, mat_b).with_name('C') @@ -126,7 +130,7 @@ def test_binary_ufunc_with_broadcast(ctx_factory, ufunc, shapes, dtype): ).with_name('C') # eval - lappy_res = bound_mat.eval() + lappy_res = bound_mat.eval(queue) numpy_res = getattr(np, ufunc)(mat_a_data, mat_b_data) # compare with numpy -- GitLab From 0f824b627da71c84dcd60df0a263ecfa7bf4d4bf Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 21:19:15 -0500 Subject: [PATCH 48/59] Try to fix py2 tests --- lappy/core/array.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lappy/core/array.py b/lappy/core/array.py index cc873c0..c422672 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -775,8 +775,14 @@ class Array(LazyObjectBase): def nbytes(self): return self.size * self.itemsize - def __len__(self): - raise NotImplementedError() + def __len__(self, order='C'): + if order == 'C': + return self.shape[0] + elif order == 'F': + return self.shape[-1] + else: + raise ValueError( + "order must be either 'C' or 'F' (got %s)" % str(order)) # {{{ basic ops -- GitLab From 4582d303b03981f0f3070508f630991cdfce3441 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 31 Mar 2020 21:31:25 -0500 Subject: [PATCH 49/59] Fix __len__ when ndim=0 --- lappy/core/array.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lappy/core/array.py b/lappy/core/array.py index c422672..cba3026 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -776,6 +776,9 @@ class Array(LazyObjectBase): return self.size * self.itemsize def __len__(self, order='C'): + if self.ndim == 0: + return 0 + if order == 'C': return self.shape[0] elif order == 'F': -- GitLab From 8dae95eb29b15154faffb03060601f7f3787cb4b Mon Sep 17 00:00:00 2001 From: xywei Date: Thu, 2 Apr 2020 11:10:01 -0500 Subject: [PATCH 50/59] Interface improvements --- .gitignore | 2 ++ lappy/__init__.py | 12 ++++++-- lappy/core/array.py | 4 --- lappy/core/basic_ops.py | 2 +- lappy/core/tools.py | 7 +++-- lappy/exceptions.py | 46 ++++++++++++++++++++++++++++ lappy/version.py | 3 -- setup.py | 55 +++++++++++++++++++++++++++++----- test/test_numpy_consistency.py | 19 ++++++++++++ 9 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 lappy/exceptions.py delete mode 100644 lappy/version.py diff --git a/.gitignore b/.gitignore index ed694da..bdba81c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__ !lappy/lib !lappy/target + +lappy/version.py diff --git a/lappy/__init__.py b/lappy/__init__.py index 4e472db..9ad06c1 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -24,11 +24,19 @@ THE SOFTWARE. __all__ = [] +from lappy.version import GIT_REVISION as __git_revision__ # noqa: N811 +from lappy.version import VERSION_TEXT as __version__ # noqa: N811 +__all__ += ['__git_revision__', '__version__'] + +from lappy.exceptions import AxisError, ComplexWarning, RankWarning +__all__ += ['AxisError', 'ComplexWarning', 'RankWarning'] + from lappy.core import to_lappy_array -__all__ += ["to_lappy_array"] +from lappy.core.tools import ScalarType +__all__ += ['to_lappy_array', 'ScalarType'] from lappy.core.ufuncs import sin, cos, exp -__all__ += ["sin", "cos", "exp"] +__all__ += ['sin', 'cos', 'exp'] from lappy.core.array import Array from lappy.eval import Compiler, Executor diff --git a/lappy/core/array.py b/lappy/core/array.py index cba3026..fb81258 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -40,10 +40,6 @@ def default_env(): return dict() -class IndexError(Exception): - pass - - # {{{ base lazy obj class diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 10404df..e99b485 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -22,8 +22,8 @@ THE SOFTWARE. import numpy as np from pymbolic import var, evaluate, substitute from pymbolic.primitives import Variable, Product -from numpy import AxisError +from lappy.exceptions import AxisError from lappy.core.ufuncs import minimum, maximum from lappy.core.conditional import conditional from lappy.core.array import ( diff --git a/lappy/core/tools.py b/lappy/core/tools.py index c62323a..cdb4c41 100644 --- a/lappy/core/tools.py +++ b/lappy/core/tools.py @@ -22,10 +22,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import numpy as np -from lappy.core.array import LazyObjectBase +from lappy.core.array import LazyObjectBase, Scalar __all__ = ["check_and_merge_envs", "check_and_merge_precs", - "is_nonnegative_int", "is_constexpr"] + "is_nonnegative_int", "is_constexpr", 'ScalarType'] + + +ScalarType = np.ScalarType + (Scalar, ) def check_and_merge_envs(*arr_list): diff --git a/lappy/exceptions.py b/lappy/exceptions.py new file mode 100644 index 0000000..9829d06 --- /dev/null +++ b/lappy/exceptions.py @@ -0,0 +1,46 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = "Copyright (C) 2020 Xiaoyu Wei" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +# {{{ errors + + +class AxisError(IndexError, ValueError): + # invalid axis passed + pass + +# }}} End errors + +# {{{ warnings + + +class ComplexWarning(RuntimeWarning): + # casting complex dtype to real + pass + + +class RankWarning(UserWarning): + # rank deficient matrix encountered when expecting otherwise + pass + +# }}} End warnings diff --git a/lappy/version.py b/lappy/version.py deleted file mode 100644 index 6ba2863..0000000 --- a/lappy/version.py +++ /dev/null @@ -1,3 +0,0 @@ -VERSION = (2019, 1) -VERSION_STATUS = "" -VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS diff --git a/setup.py b/setup.py index 3fa8245..e4b351c 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,58 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- +import os +import subprocess from setuptools import setup, find_packages -ver_dic = {} -version_file = open("lappy/version.py") -try: - version_file_contents = version_file.read() -finally: - version_file.close() +YEAR = 2020 +MONTH = 1 +STATUS = 'dev' -exec(compile(version_file_contents, "lappy/version.py", 'exec'), ver_dic) + +def get_git_version(): + if not os.path.exists('.git'): + return None + + git_rev = None + try: + git_rev = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT + ).strip().decode('ascii') + except (subprocess.SubprocessError, OSError): + pass + + if git_rev is None: + git_rev = "Unknown" + + return git_rev + + +VERSION = (YEAR, MONTH) +VERSION_TEXT = '%d.%d%s' % (YEAR, MONTH, STATUS) +GIT_REVISION = get_git_version() + +VERSION_FILE = "lappy/version.py" +VER_DICT = { + 'version': VERSION, + 'version_status': STATUS, + 'version_text': VERSION_TEXT, + 'git_revision': GIT_REVISION} + +# rewrite version file every time +with open(VERSION_FILE, 'w') as a: + a.write("""################################################## +# THIS FILE IS GENERATED BY LAPPY SETUP.PY +################################################## + +VERSION = '%(version)s' +VERSION_STATUS = '%(version_status)s' +VERSION_TEXT = '%(version_text)s' +GIT_REVISION = '%(git_revision)s' +""" % VER_DICT) setup(name="lappy", - version="2017.1", + version=VER_DICT['version_text'], description="Lazy array programming in Python", long_description=open("README.rst").read(), classifiers=[ diff --git a/test/test_numpy_consistency.py b/test/test_numpy_consistency.py index b9241fd..ddec2f5 100644 --- a/test/test_numpy_consistency.py +++ b/test/test_numpy_consistency.py @@ -40,10 +40,26 @@ NOSUP = [ '_print_width_1d', '_set_flat', '_set_mask', '_set_recordmask', '_update_from', + # build system + '__config__', '__NUMPY_SETUP__', '_NoValue', 'MachAr', 'WRAP', + 'Tester', 'ModuleDeprecationWarning', '_add_newdoc_ufunc', '_arg', + '_distributor_init', '_pytesttester' + # not well-defined for lappy arrays, since there is no direct # correspondence with data in memory. 'iscontiguous', 'byteswap', 'newbyteorder', 'getfield', 'setfield', 'strides', 'tobytes', 'tofile', 'tolist', 'tostring', 'view', + 'ALLOW_THREADS', 'BUFSIZE', 'CLIP', 'ERR_CALL', 'ERR_DEFAULT', + 'ERR_IGNORE', 'ERR_LOG', 'ERR_PRINT', 'ERR_RAISE', 'ERR_WARN', + 'FLOATING_POINT_SUPPORT', 'FPE_DIVIDEBYZERO', 'FPE_INVALID', + 'FPE_OVERFLOW', 'FPE_UNDERFLOW', 'False_', 'True_', 'RAISE', + 'MAXDIMS', 'MAY_SHARE_BOUNDS', 'MAY_SHARE_EXACT', + 'Inf', 'Infinity', 'NAN', 'NINF', 'NZERO', 'NaN', 'PINF', 'PZERO', + 'SHIFT_DIVIDEBYZERO', 'SHIFT_INVALID', 'SHIFT_OVERFLOW', 'SHIFT_UNDERFLOW', + 'UFUNC_BUFSIZE_DEFAULT', 'UFUNC_PYVALS_NAME', '_UFUNC_API', + + # not planned to support + 'DataSource', '_globals', # (no masked array support for now) 'compress', 'compressed', 'count', 'base', 'baseclass', @@ -53,6 +69,9 @@ NOSUP = [ 'set_fill_value', 'sharedmask', 'shrink_mask', 'soften_mask', 'toflex', 'torecords', 'unshare_mask', + # (no matrixlib support for now) + '_mat', + # attributes set during construction 'dtype', 'shape', 'flags', 'setflags', ] -- GitLab From 6c7564b363e00866084e078b5ff4869642a46545 Mon Sep 17 00:00:00 2001 From: xywei Date: Mon, 6 Apr 2020 23:31:22 -0500 Subject: [PATCH 51/59] Preliminary multi-insn support --- experiments/lazy_arange.py | 7 +- experiments/multiinsn.py | 15 +++++ lappy/__init__.py | 15 +++-- lappy/core/array.py | 37 +++++++--- lappy/core/basic_ops.py | 6 +- lappy/core/broadcast.py | 2 +- lappy/core/function_base.py | 12 ++-- lappy/core/ufuncs.py | 38 +++++++---- lappy/eval/compiler.py | 131 ++++++++++++++++++++++++++++-------- 9 files changed, 197 insertions(+), 66 deletions(-) create mode 100644 experiments/multiinsn.py diff --git a/experiments/lazy_arange.py b/experiments/lazy_arange.py index c6c058a..c7b6a7f 100644 --- a/experiments/lazy_arange.py +++ b/experiments/lazy_arange.py @@ -1,12 +1,11 @@ import pyopencl as cl -from lappy import configure from lappy.core.function_base import arange, linspace ctx = cl.create_some_context() -configure('opencl', cl_ctx=ctx) +queue = cl.CommandQueue(ctx) a = arange(10) -print(a.eval()) +print(a.eval(queue)) b = linspace(0, 1, 10) -print(b.eval()) +print(b.eval(queue)) diff --git a/experiments/multiinsn.py b/experiments/multiinsn.py new file mode 100644 index 0000000..35197a6 --- /dev/null +++ b/experiments/multiinsn.py @@ -0,0 +1,15 @@ +import pyopencl as cl +from lappy.core.function_base import arange, linspace + +ctx = cl.create_some_context() +queue = cl.CommandQueue(ctx) + +a = arange(10).with_name('A') +a.pin() + +b = a + 1 +# print(a.eval(queue)) +print(b.eval(queue)) + + +print(a.namespace[a.name][1]) diff --git a/lappy/__init__.py b/lappy/__init__.py index 9ad06c1..18f6aa6 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -43,11 +43,18 @@ from lappy.eval import Compiler, Executor class ndarray(Array): # noqa: N801 - def eval(self, queue, check_preconditions=True): + def eval(self, queue, check_preconditions=False): if self.value is None: - compiler = Compiler(check_preconditions) - closure = compiler(self) - evaluator = Executor(closure) + if hasattr(self, '_closure'): + pass + else: + compiler = Compiler(check_preconditions) + self._closure = compiler(self) + + print(self._closure.kernel) + # print(self._closure.data_map) + + evaluator = Executor(self._closure) res = evaluator(queue) self.value = res[self.name] return self.value diff --git a/lappy/core/array.py b/lappy/core/array.py index fb81258..8d69104 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -82,7 +82,9 @@ class LazyObjectBase(object): as values, where ``object_desc`` is a dictionary of tags. Sample tags are: ``is_argument``: whether the object is an input argument. - ``is_bound``: whether the object's value is known. + ``is_temporary``: whether the object is a temporary. + + /All tags default to False./ The namespace associated with a lazy object contains all lazy objects involved during the construction of itself. Stateless @@ -592,7 +594,7 @@ class Array(LazyObjectBase): """ return self.copy(stateless=True) - def with_name(self, name, rename_arguments=False): + def with_name(self, name, rename_arguments=True): """Rename the array object. Returns a new (shallow) copy of self with the new name. @@ -689,7 +691,7 @@ class Array(LazyObjectBase): if new_name is None: new_name = 'bound%d_' % self.__class__._counter + self.name - new_arr = self.with_name(new_name) + new_arr = self.with_name(new_name, False) for s, sv in zip(self.shape, shape_data): if s in new_arr.env and new_arr.env[s] != sv: @@ -1053,10 +1055,20 @@ class Array(LazyObjectBase): return power(other_arr, self) def __floordiv__(self, other): - raise NotImplementedError() + if not isinstance(other, Array): + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import floordiv + return floordiv(self, other_arr) def __rfloordiv__(self, other): - raise NotImplementedError() + if not isinstance(other, Array): + other_arr = to_lappy_array(other) + else: + other_arr = other + from lappy.core.ufuncs import floordiv + return floordiv(other_arr, self) def __mod__(self, other): raise NotImplementedError() @@ -1367,6 +1379,15 @@ class Array(LazyObjectBase): # }}} End public (numpy-compliant) api + # {{{ extended public api + + def pin(self): + """Declare that the array shall be treated as a temporary. + """ + self.namespace[self.name][1]['is_temporary'] = True + + # }}} End extended public api + # {{{ private api def _get_shape_vals(self, axis=None): @@ -1656,7 +1677,7 @@ def to_lappy_array(arr_like, name=None, base_env=None): if np.isscalar(arr_like): try: - dtype = np.dtype(type(arr_like)) + dtype = np.dtype(type(arr_like)).type except TypeError: dtype = None @@ -1714,13 +1735,13 @@ def to_lappy_scalar(scalar_like, name=None, base_env=None, dtype=np.float64): if isinstance(scalar_like, Array): assert scalar_like.ndim == 0 assert scalar_like.shape == () - assert scalar_like.value is not None if not isinstance(scalar_like, Scalar): scalar_like = Scalar( name=scalar_like.name, value=scalar_like.value, dtype=dtype, - env=scalar_like.env.copy()) + env=scalar_like.env.copy(), + namespace=scalar_like.namespace.copy()) return scalar_like diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index e99b485..235270e 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -358,7 +358,7 @@ class SubArrayFactory(object): name_prefix = get_internal_name_prefix(self.base_arr.name) name = (name_prefix + self.base_arr.name + '_subarray%d' % self.base_arr.__class__._counter) - sub_arr = self.base_arr.with_name(name) + sub_arr = self.base_arr.with_name(name, False) new_inames = sub_arr._make_default_inames() new_shape = tuple(Unsigned() for iax in range(self.base_arr.ndim)) @@ -521,7 +521,7 @@ def reshape(array, newshape, order='C', name=None, inames=None): iname_subs[array.inames[iaxis]] = flat_id // array._shadow_size( range(array.ndim + iaxis)) - new_arr = array.with_name(name) + new_arr = array.with_name(name, False) new_arr.inames = inames new_arr.preconditions = new_precond @@ -586,7 +586,7 @@ def transpose(array, axes=None, name=None): name = (name_prefix + array.name + '_T%d' % array.__class__._counter) # noqa: W0212 - new_arr = array.with_name(name) + new_arr = array.with_name(name, False) new_arr.inames = tuple(array.inames[axes[i]] for i in range(ndim)) new_arr.shape = tuple(array.shape[axes[i]] for i in range(ndim)) diff --git a/lappy/core/broadcast.py b/lappy/core/broadcast.py index 630e2c1..fda79d6 100644 --- a/lappy/core/broadcast.py +++ b/lappy/core/broadcast.py @@ -242,7 +242,7 @@ class BroadcastResult(object): name = (name_prefix + name + '_broadcast%d' % base_arr.__class__._counter) # noqa: W0212 - brdc_arr = base_arr.with_name(name) + brdc_arr = base_arr.with_name(name, False) assert isinstance(self.ndim, int) and self.ndim >= 0 brdc_arr.shape = tuple(s.name for s in self.shape) diff --git a/lappy/core/function_base.py b/lappy/core/function_base.py index 16ba256..c81687d 100644 --- a/lappy/core/function_base.py +++ b/lappy/core/function_base.py @@ -1,11 +1,12 @@ from __future__ import division, absolute_import, print_function import numpy as np +from lappy import ndarray from lappy.core.array import ( default_env, - LazyObjectBase, Array, Scalar, Unsigned, + LazyObjectBase, Scalar, Unsigned, to_lappy_scalar, to_lappy_unsigned) -from lappy.core.ufuncs import power, floor +from lappy.core.ufuncs import power from lappy.core.tools import (check_and_merge_envs, check_and_merge_precs) __copyright__ = "Copyright (C) 2020 Xiaoyu Wei" @@ -61,8 +62,7 @@ def arange(start, stop=None, step=1, dtype=None, stop = to_lappy_scalar(stop) step = to_lappy_scalar(step) - num = to_lappy_unsigned( - floor((stop - start) / step)) + num = to_lappy_unsigned((stop - start) // step) obj = dict() obj['ndim'] = 1 @@ -75,10 +75,10 @@ def arange(start, stop=None, step=1, dtype=None, obj['name'] = name if dtype is None: - dtype = np.result_type(start, stop, step) + dtype = np.result_type(start, stop, step).type obj['dtype'] = dtype - arr = Array(**obj) + arr = ndarray(**obj) iname = arr.inames[0] arr.expr = start.expr + step.expr * iname diff --git a/lappy/core/ufuncs.py b/lappy/core/ufuncs.py index 0ec69be..2abca4c 100644 --- a/lappy/core/ufuncs.py +++ b/lappy/core/ufuncs.py @@ -127,10 +127,15 @@ class UnaryOperation(UFunc): if name in a.namespace: raise ValueError("The name %s is ambiguous" % name) + if a.namespace[a.name][1].get('is_temporary', False): + base_expr = var(a.name) + else: + base_expr = a.expr + obj = { 'name': name, 'inames': a.inames, - 'expr': self.f(a.expr), 'value': None, + 'expr': self.f(base_expr), 'value': None, 'domain_expr': a.domain_expr, 'env': a.env.copy(), 'namespace': a.namespace.copy(), @@ -268,21 +273,27 @@ class BinaryOperation(UFunc): elif b.dtype is None: new_dtype = a.dtype else: - new_dtype = np.result_type(a.dtype, b.dtype) + new_dtype = np.result_type(a.dtype, b.dtype).type else: new_dtype = self.dtype # expression handling - a_expr = substitute( - a.expr, - {ainame: briname - for ainame, briname in zip(a.inames, bres.inames)} - ) - b_expr = substitute( - b.expr, - {biname: briname - for biname, briname in zip(b.inames, bres.inames)} - ) + if a.namespace[a.name][1].get('is_temporary', False): + a_expr = var(a.name) + else: + a_expr = substitute( + a.expr, + {ainame: briname + for ainame, briname in zip(a.inames, bres.inames)} + ) + if b.namespace[b.name][1].get('is_temporary', False): + b_expr = var(b.name) + else: + b_expr = substitute( + b.expr, + {biname: briname + for biname, briname in zip(b.inames, bres.inames)} + ) new_expr = self.f(a_expr, b_expr) new_integer_domain = None @@ -357,7 +368,8 @@ add = BinaryOperation(expr_func=lambda x, y: x + y, preserve_integral=True) subtract = BinaryOperation(expr_func=lambda x, y: x - y, preserve_integral=True) multiply = BinaryOperation(expr_func=lambda x, y: x * y, preserve_integral=True) divide = BinaryOperation(expr_func=lambda x, y: x / y) -power = BinaryOperation(expr_func=lambda x, y: x**y) +floordiv = BinaryOperation(expr_func=lambda x, y: x // y, is_integral=True) +power = BinaryOperation(expr_func=lambda x, y: x**y, preserve_integral=True) equal = BinaryOperation(lambda x, y: x.eq(y)) not_equal = BinaryOperation(lambda x, y: x.ne(y)) diff --git a/lappy/eval/compiler.py b/lappy/eval/compiler.py index e98baab..3418e62 100644 --- a/lappy/eval/compiler.py +++ b/lappy/eval/compiler.py @@ -23,6 +23,7 @@ THE SOFTWARE. """ import warnings +import functools from traceback import format_list, extract_stack import numpy as np @@ -30,6 +31,7 @@ import loopy as lp import pyopencl as cl from pymbolic import var, substitute, evaluate +from pymbolic.mapper import CombineMapper from pymbolic.mapper.evaluator import UnknownVariableError from lappy.core.array import Array @@ -39,9 +41,45 @@ from lappy.eval import Closure class PreconditionNotMetError(Exception): pass +# {{{ expression mappers + + +class TemporaryCollector(CombineMapper): + """Find temporaries in the expression. + Note that it is possible that a temporary appears multiple + times within different inames. + + .. attribute:: arr + + reference to the lazy array object. + """ + def __init__(self, arr): + self.arr = arr + + def combine(self, values): + import operator + return functools.reduce(operator.add, values, list()) + + def map_constant(self, expr): + if hasattr(expr, 'name'): + if expr.name in self.arr.namespace: + tags = self.arr.namespace[expr.name][1] + is_temp = tags.get('is_temporary', False) + if is_temp: + return [expr.name, ] + + return list() + + map_algebraic_leaf = map_constant + +# }}} End expression mappers + + +# {{{ compiler + class Compiler(object): - def __init__(self, check_preconditions=True): + def __init__(self, check_preconditions=False): self.check_preconditions = check_preconditions def __call__(self, *args): @@ -60,6 +98,8 @@ class Compiler(object): data_map = self._get_data_mapping(arr, knl) context.update(data_map) + # FIXME: using with_name breaks precondition checking. + # Possible Solution: keep track of name changes if self.check_preconditions: self._check_preconditions(arr.preconditions, context) @@ -216,6 +256,42 @@ class Compiler(object): data_map.pop(key) return data_map + def _make_assignments(self, arr, **kwargs): + """Make the list of loopy instructions for a lazy array. + + :param substitutions: a dictionary of inlined information. + :param is_temporary: whether the lhs is a temporary. + """ + assignments = [] + + rhs_expr = arr.expr + if arr.ndim == 0: + lhs_expr = var(arr.name)[var(arr.name + '_dummy_iname')] + else: + lhs_expr = var(arr.name)[arr.inames] + + if 'substitutions' in kwargs: + rhs_expr = substitute(rhs_expr, kwargs['substitutions']) + + is_temporary = kwargs.get('is_temporary', False) + + if is_temporary: + assignments.append( + lp.Assignment(lhs_expr, rhs_expr), + temp_var_type=arr.dtype) + else: + assignments.append(lp.Assignment(lhs_expr, rhs_expr)) + + # recursively add assignments for all temps + tc = TemporaryCollector(arr) + temps = tc(rhs_expr) + if len(temps) > 0: + for tmp_name in temps: + tmp = arr._extract(tmp_name) + assignments.extend(self._make_assignments(tmp, **kwargs)) + + return assignments + def compile(self, *args, **kwargs): # context: where the preconditions are checked # data_map: passed to the loopy kernel @@ -226,17 +302,11 @@ class Compiler(object): if len(args) > 1: raise NotImplementedError() - arr = args[0] + arrs = list(args) + arr = arrs[0] shape_dtype = kwargs.pop('shape_dtype', np.int32) - # expression handling - rhs_expr = arr.expr - if arr.ndim == 0: - lhs_expr = var(arr.name)[var(arr.name + '_dummy_iname')] - else: - lhs_expr = var(arr.name)[arr.inames] - loop_domain = arr._generate_index_domains() # collect kernel args @@ -257,18 +327,16 @@ class Compiler(object): continue if a.ndim == 0 and arr.env.get(aname, None) is None: try: - # FIXME: move this to a better place - import math env = arr.env.copy() - env['floor'] = math.floor - - arr.env[aname] = evaluate(a.expr, env) + val = evaluate(a.expr, env) + if a.dtype is not None: + val = a.dtype(val) + arr.env[aname] = val except UnknownVariableError as e: print(e) pass - # substitute baked-in constants inline_scalars = True if inline_scalars: scalar_map = dict() @@ -279,7 +347,9 @@ class Compiler(object): scalar_map[vn] = arr._get_value(vn) scalar_map = self._purge_nones(scalar_map) context.update(scalar_map) - rhs_expr = substitute(rhs_expr, scalar_map) + assignments = self._make_assignments(arr, substitutions=scalar_map) + else: + assignments = self._make_assignments(arr) # add more kernel args (reduces a log of guessing time) # e.g., link array shape vars with the arrays, dtypes, etc. @@ -291,17 +361,25 @@ class Compiler(object): continue # arr (output) handled above if isinstance(varr, Array) and varr.ndim > 0: # for now, we only specify inputs - if not tag.get('is_argument'): - continue + if varr.dtype is None: dtype = np.int32 if varr.is_integral else np.float64 else: dtype = varr.dtype - kernel_data.append( - lp.GlobalArg( - arr_name, - dtype=dtype, - shape=varr._shape_str)) + + if tag.get('is_argument'): + kernel_data.append( + lp.GlobalArg( + arr_name, + dtype=dtype, + shape=varr._shape_str)) + elif tag.get('is_temporary'): + kernel_data.append( + lp.TemporaryVariable( + arr_name, + dtype=dtype, + shape=varr._shape_str)) + elif isinstance(varr, Array) and varr.ndim == 0: if varr.dtype is None: dtype = np.int32 if varr.is_integral else np.float64 @@ -317,10 +395,7 @@ class Compiler(object): kernel_data.append('...') knl = lp.make_kernel( - loop_domain, - [ - lp.Assignment(lhs_expr, rhs_expr) - ], + loop_domain, assignments, kernel_data, lang_version=(2018, 2)) @@ -347,3 +422,5 @@ class Compiler(object): knl = lp.add_and_infer_dtypes(knl, extra_dtype_dict) return knl, context + +# }}} End compiler -- GitLab From cd9e263107033958f8638831200251dc9044a6c9 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 12:26:12 -0500 Subject: [PATCH 52/59] Add docs for loop domain --- .gitignore | 7 + doc/Makefile | 20 +++ doc/make.bat | 35 +++++ doc/source/_static/.gitkeep | 0 doc/source/_templates/.gitkeep | 0 doc/source/conf.py | 72 +++++++++++ doc/source/index.rst | 23 ++++ doc/source/loop_domain.rst | 214 +++++++++++++++++++++++++++++++ doc/source/ndarray.rst | 4 + experiments/finite_difference.py | 2 + experiments/multiinsn.py | 4 +- 11 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 doc/Makefile create mode 100644 doc/make.bat create mode 100644 doc/source/_static/.gitkeep create mode 100644 doc/source/_templates/.gitkeep create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst create mode 100644 doc/source/loop_domain.rst create mode 100644 doc/source/ndarray.rst diff --git a/.gitignore b/.gitignore index bdba81c..19586d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,13 @@ __pycache__ *.egg-info + +!doc/ +!doc/source/*.rst +!doc/source/_static +!doc/source/_templates +!doc/Makefile + !lappy/lib !lappy/target diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..6247f7e --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/doc/source/_static/.gitkeep b/doc/source/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/doc/source/_templates/.gitkeep b/doc/source/_templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..ce73d9e --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,72 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +import lappy + +# -- Project information ----------------------------------------------------- + +project = 'Lappy' +copyright = lappy.__copyright__ +author = 'Lappy Developers' + +# The full version, including alpha/beta/rc tags +release = lappy.__version__ +version = lappy.version.GIT_REVISION + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.autosummary', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.githubpages'] + +templates_path = ['_templates'] +master_doc = 'index' +source_suffix = '.rst' + +mathjax_path = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" # noqa: E501 + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..4c7378c --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,23 @@ +.. Lappy documentation master file, created by + sphinx-quickstart on Tue Apr 7 09:42:33 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Lappy's documentation! +================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ndarray + loop_domain + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/loop_domain.rst b/doc/source/loop_domain.rst new file mode 100644 index 0000000..7943308 --- /dev/null +++ b/doc/source/loop_domain.rst @@ -0,0 +1,214 @@ +Loop Domain +=========== + +.. note:: + + (Vaporware Warning). + This section mostly contain pseudo-codes that are not implemented yet. + The plans may change at anytime. + +The loop domain of a lazy array is an integer set of all work items. +In :mod:`lappy`, it is represented as a set expression. + +Expression Tree +--------------- + +The expression tree of an array ``domain_expr`` starts from leaf nodes +of type :class:`SetVar`, :class:`SetParam` or :class:`SetZero`. +It has two levels: + +1. (Affine Expressions). + The nodes close to the leaf level that are of type :class:`PwAff`. + They consist the expression trees of the (irreducible) + low-dimensional polygons of type :class:`PwAffComparison`. + To avoid branches, the polygons' subtrees should have disjoint + variables (inames), but may share parameters (loop bounds). + +2. (Integer Set Expression). + The remaining nodes of type :class:`SetIntersection` and :class:`SetUnion` + represent the expression from the basic polygons + to the loop domain. + +The integer set expression exposes structures within the loop domain +that could benefit array manipulations, e.g., + +1. The :class:`PwAff` objects are labeled consistently. ISL treat inames + as purely positional placeholders, which may not be as handy. For example, + an FEM code may want to tag an iname as ``'i_q_point'``. Position of + the iname may change as the array is manipulated (e.g. transpose). + +2. The affine expressions are nonlinearity-tolerant w.r.t paramters. + ISL only allow affine operations among paramters. + When assembling the array expression, some loop bounds do not become + affine until more shaped information are fixed (e.g. reshape). + + +Temporaries and CSEs +-------------------- + +Here is a sample integer set expression, where +``OR`` represents set union, ``AND`` represents set intersection. + +Consider the following pseudo-code: + +.. code-block:: python + + n = Unsigned('n') + k = arange(n) + c = outer(sin(k), cos(k)) + +The expression for ``c`` is flattened by default, yielding a single instruction like +(names are replaced for readability). + +.. code-block:: + + for i, j + c[i, j] = sin(i) * cos(j) + end + +If the user choose to make ``k`` a temporary, + +.. code-block:: python + + n = Unsigned('n') + k = arange(n) + k.pin() + c = outer(sin(k), cos(k)) + +The instructions for ``c`` shall be like + +.. code-block:: + + for i, j + <> k[i] = i {id=k} + c[i, j] = sin(k[i]) * cos(k[j]) {dep=k} + end + +The loop domain is unchanged, +but depending on the loop nesting order, ``k`` may be computed multiple times. + +.. code-block:: + + AND + /-------/ \-------\ + | | + { 0 <= i < n } { 0 <= j < n } + +If the user choose to make ``k`` a CSE, + +.. code-block:: python + + n = Unsigned('n') + k = arange(n) + k.pin_cse() + c = outer(sin(k), cos(k)) + +The inames and temporaries of ``k`` are then duplicated. +It should produce the same results as evaluating ``k`` separately +by calling ``k.eval()`` and pass in its value as an argument. + +.. code-block:: + + for i_dup + <> k[i_dup] = i_dup {id=k} + end + + for i, j + c[i, j] = sin(k[i]) * cos(k[j]) {dep=k} + end + + +And the loop domain acquires a new branch, + +.. code-block:: + + OR + /------------------/ \-------------\ + | | + AND { 0 <= i_dup < n } + /-------/ \-------\ + | | + { 0 <= i < n } { 0 <= j < n } + + + +Diamond Problem +--------------- + +The following code produces a diamond structure in the computation graph: + +.. code-block:: python + + A = ndarray('A', shape='n, n') + x = ndarray('x', shape='n') + k = A @ x + sk = sin(k) + ck = cos(k) + C = outer(sk, ck) + +The data flow looks like (top to bottom): + +.. code-block:: + + A @ x + | + k + /\ + / \ + sk ck + \ / + \/ + c + +By default, the lazy evaluation expands all expressions, yielding +redundant computations. + +.. code-block:: + + c[i, j] = sin(sum([k], A[i, k] * x[k])) * cos(sum([k], A[j, k] * x[k])) + +Note that the ``matvec`` is computed twice above. + +Besides Loopy's ability to recognize optimization opportunities, user can +also make ``k`` a temporary or a CSE. + +.. code-block:: python + + A = ndarray('A', shape='n, n') + x = ndarray('x', shape='n') + k = A @ x + k.pin() + sk = sin(k) + ck = cos(k) + C = outer(sk, ck) + +Making ``k`` a temporary yields: + +.. code-block:: + + <> k[i] = sum([k], A[i, k] * x[k]) {id=k} + c[i, j] = sin(k[i]) * cos(k[j]) {dep=k} + +Similarly, + +.. code-block:: python + + A = ndarray('A', shape='n, n') + x = ndarray('x', shape='n') + k = A @ x + k.pin_cse() + sk = sin(k) + ck = cos(k) + C = outer(sk, ck) + +Making ``k`` a CSE yields: + +.. code-block:: + + for i_dup + <> k[i_dup] = sum([k], A[i_dup, k] * x[k]) {id=k} + end + + for i, j + c[i, j] = sin(k[i]) * cos(k[j]) {dep=k} + end diff --git a/doc/source/ndarray.rst b/doc/source/ndarray.rst new file mode 100644 index 0000000..daed0d4 --- /dev/null +++ b/doc/source/ndarray.rst @@ -0,0 +1,4 @@ +NDArray +======= + +.. automodule:: lappy.ndarray diff --git a/experiments/finite_difference.py b/experiments/finite_difference.py index d2c484a..4406f68 100644 --- a/experiments/finite_difference.py +++ b/experiments/finite_difference.py @@ -91,3 +91,5 @@ if __name__ == '__main__': fe = solve_analytic() fn = solve_numpy() fl = solve_lappy() + +# flake8: noqa diff --git a/experiments/multiinsn.py b/experiments/multiinsn.py index 35197a6..a917c26 100644 --- a/experiments/multiinsn.py +++ b/experiments/multiinsn.py @@ -1,5 +1,5 @@ import pyopencl as cl -from lappy.core.function_base import arange, linspace +from lappy.core.function_base import arange ctx = cl.create_some_context() queue = cl.CommandQueue(ctx) @@ -10,6 +10,4 @@ a.pin() b = a + 1 # print(a.eval(queue)) print(b.eval(queue)) - - print(a.namespace[a.name][1]) -- GitLab From 0143fd5010707eb33d1690db6fa8be67af681ed9 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 12:30:38 -0500 Subject: [PATCH 53/59] Add html built doc --- .gitignore | 7 +- doc/build/doctrees/environment.pickle | Bin 0 -> 15172 bytes doc/build/doctrees/index.doctree | Bin 0 -> 4852 bytes doc/build/doctrees/loop_domain.doctree | Bin 0 -> 24186 bytes doc/build/doctrees/ndarray.doctree | Bin 0 -> 14264 bytes doc/build/html/.buildinfo | 4 + doc/build/html/.nojekyll | 0 doc/build/html/genindex.html | 254 ++++++++++++++++ doc/build/html/index.html | 225 +++++++++++++++ doc/build/html/loop_domain.html | 383 +++++++++++++++++++++++++ doc/build/html/ndarray.html | 268 +++++++++++++++++ doc/build/html/objects.inv | Bin 0 -> 403 bytes doc/build/html/py-modindex.html | 216 ++++++++++++++ doc/build/html/search.html | 212 ++++++++++++++ doc/build/html/searchindex.js | 1 + 15 files changed, 1568 insertions(+), 2 deletions(-) create mode 100644 doc/build/doctrees/environment.pickle create mode 100644 doc/build/doctrees/index.doctree create mode 100644 doc/build/doctrees/loop_domain.doctree create mode 100644 doc/build/doctrees/ndarray.doctree create mode 100644 doc/build/html/.buildinfo create mode 100644 doc/build/html/.nojekyll create mode 100644 doc/build/html/genindex.html create mode 100644 doc/build/html/index.html create mode 100644 doc/build/html/loop_domain.html create mode 100644 doc/build/html/ndarray.html create mode 100644 doc/build/html/objects.inv create mode 100644 doc/build/html/py-modindex.html create mode 100644 doc/build/html/search.html create mode 100644 doc/build/html/searchindex.js diff --git a/.gitignore b/.gitignore index 19586d3..41e385f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,15 @@ __pycache__ *.egg-info - !doc/ +!doc/Makefile + +!doc/build +!doc/build/html/*.html + !doc/source/*.rst !doc/source/_static !doc/source/_templates -!doc/Makefile !lappy/lib !lappy/target diff --git a/doc/build/doctrees/environment.pickle b/doc/build/doctrees/environment.pickle new file mode 100644 index 0000000000000000000000000000000000000000..acff3cc31f54b8ac431e2c787cf06da53a9b29bf GIT binary patch literal 15172 zcmcIreT*H~Rgc%}{r>j)t4ZpYx@p&>`_^A+?6e_qY$qveJ0W(ISj0@{y*F=X#`9*L z=fk_3$f!kX>XojPNSUevAt3>kiYoCR{DoRdD)k?rfKWlG5s*+26eI+HAcRo)opbJ; znfG?rUxX_4+jq~s=bn4+`9Al3Y4B^OKKB6sPt7G=*9+I`PI$wMqp;(IX|86T&Agy> zt}^s)e)!Az&3r-aw7cE>q8e*O%`|qLmKW!0amkH3&eGcYs^cxa6i2?(Op~PmGuM|e zZ>d4p=r3B4Gw6+n}= zv{yi86>G;%UEf~oo$9p>;Z+1TJZH5TwSb_ypBJW1EZg(~R=m{B8eQy|sjqp#{*+=DmTIv&hX;eS?7im zL|ts0tKrxQ96JG7qsNXMJ5oQ|a&BN0hb2DdYT?A=Cy%v`A35GQa{ScEqwOQ@<42x2 z^7xUH?av-Ra_ZF4W~<$Fj-NPjG*?rp)9D6w>LgYde8Cl{F{>EMZ8g@h6CBs#d)VcL zEeD*NlXmzJ8X&`;{kAj(?pz>b}IGwLkaxTdARYa~m0)zY#06ZOZJE`ZXXv)Af=^0eIA zQ@Z37tVui&RR3bS`KXl`dPZvRCj%a^eC~CbUh&PHP<*tVr6-u ztf4PSeoKwHX(zB!*XdyHjLfy-v{ei-JW1`;YZm+)267qz^QN3`*07pMVzs>hLILhh z@sggQNgnbmlP1Hg1Ff>!QH+zbb`K}QU);|n`K$IQDJ-UCF{m4U&RePT(5}>O(Ah>V z((6^h>V+!=$<)vdJIFv9EQtGY_n_JdbCX~EYVLl*J>>p8{ZVrt zQG-Mx4}Z%2vOov zuzXK)TNdn>Q(0ymvoEyi7WTdwM^SpS{-%N5d=nPrrtIxL=6;4TxQp&lK{c&K)d|4^ zoEkclrR~L&iwUfjOqtSCx^|q9ei>wsyPuT_6D_B0XF+Pk5g4n_u-|UAtacW%)Z_AF z$d&6PVC5}YHA53RQ7gkiiS`2Ua2k6Z+-;PGQR-N+(}uQ$O~*>QHf-vK-ZXKeRZFZu zq6gh4+>`EQT#A}=VMYQnB?Xfp1DZ~KuGw-<)D_({M_X_Y3!Z7C>sAA{nQw^kPhNAJ zuC;2%A$VrCSF=gh?Zyt|xqw{No9)F(**aLz+)fl)m`An&a7H)=17JB_FNjtQ2YCoa zvNANstRa8oU^*-T_SWp2>Vj)9@vuZ4+k=%GfJxP?3gPac)9f$vix-JOGcs23E;j)I z02U}@6K!QNL|QP6gkIY9n%CCz4W2Zmzgyl)7(u!Lax#Sf4wY@L-Y+VFNykwb(rm;)X`HN$8siya zfZg}p_h@Xc9J4BN%`iRROQEOkL76|!^R+vLQR9h1kU0y4$q93Th|LB8ftR>WoSPNe zmK(2~yN}5HX-1E@CQQAyhXral3$KOIszE6GCr$Pu?hj?v2(QY#R*n%ev-+qUvgC)W zLo1l&m?2GF>=YhF3mN0cL6t)?!*fU89H`1u7MR=zm70PKCp_*31 zVrqh5226glx<3@&f|64FqpjFH)77!{xV7rFNJk!!RdO0ObWlceXI>b1A<*(A2^bO$ z$4qg@Eg7C+oCqYFvB!3ec*qL`f|vkiq=Bdqx2GYqyk<4_$n%j^hASCEeC54?$#x(c zx=*^F(`J!fGRvLu>&EUD5kXb>#Kg%W7C!Z`gl$E9R)bN?DE2^TfEZ1%&F~7GKTfUm z9@a@*A~DQGK{d*#U`iwCJ(DxRBW?SJ2ZrcD_q6*gL)|m(S^4Ah?o%>1M(+_J4O%Yqq+$;w_~kD!@O!C^}*3CJq%M2<>sgnYN* zv*2T&i8zWtdZu#vU z4?IAy2?NW>du$Zrb7q{^uTkk zoIhW?c>2P*+UL(*IQ@n5&zwGc_T_VzF8yp?Y`&*lEs;KQNV}7(`SYhQKL5(;=dtws z`DX&J9>pt_WfPQnu0cFk$mpw)E+u~$k!WjLpvG`=bm4x6A`oQLb(&UW)>|aWLkL4M zSoSoxS+vFkn^;I5nUN7-gVeAA(qZ0%0wA@Pzd8v1VMT+i6Y2m_(?(O)V7^S;ctGZ- zQ8%D`-O@=L;!^iY!oE3LC5WLU`!QA>UA6# zBF%vcU^R-bQPjA+tAB|dHsKUujTo@M+(iRQ>eRq5KB8}U6gRHj3=|fUvWq1J<)c1d zOhSLiUfy9uW0Dq@Y!|-nPH=01BVNFGH~%4Mq3HiC1kelH5miRmu^ai+yCVENIx)b5S%@@H zjLdF^k!LSFOOcY$zKmLDN^G;Kyk?dsv@8^XI3xr` z#^TKri1YTXpnJe`+`bcZVN1i7&f$?%sL=w#wt9E*)l7t0zUcp0o9p=!j4BNS z0g}k8T_G}p8zzpd!Zt->vlPv+i-Lk9!lfR_E^%36Qs!v_&SZ5QF0ep^5b3EgJLtN0 z!=XB?Ox$B8nqr>0T^;}QgDX)hTapKh2~jhFF+mzhvSg=Je$~lwNen8jh+n%%>`H^f zXCYG4^{#`~4nS(O#36L-HQVS4n}>Q%j4({^@G%4SLKG*E=>VTlV+^T#N{8@M#1{^q zttbo-p@`p-O1IO&+M_Lc^X|K+~|h(!{blunQET;)HyO`>H?`7+u7{ z9s~w~n$Xa*x(M$WmSqh!DcKJ9LEz+;cc|SxBV@~B8|+j=h>}SUE>^l_Q&PvR7tzu1 zOS+L(^b$>*(jBEjzZwAprNaI;5;ZgzqHa+oHZ7~+uw}Jl)a z2}K0&*F+S9E?O9i=EnNDM|5KUPO&-X21_d$RIeE{FSVM}3)pr#pV zuu)!`FAu0aeXF|bXcmUFqJMwtyKOO?q%91)dzS~?2l4q3{(o}$WTxhI*g@tEcP7=F zd_&Fkj3a4<(c3`7fot(3`f3Esq2d~S0aM8uBg~^V8_KBQ+lJbM_MUysa!|exEChkn zNPu45QWIVD8pxS&n4xN>DY&}2T9=bi-nax`w}hF=61r%#qG+6u{7l5{W(CJX1_yh~ zy62>A#zAh8*3;~s^;2HhQz>yhH3hNa$iiBqMjUa0^0_2y^p1|XHhM#x@Igb=UUsJ* zszEWO?#OOQOYEYBIYliqjSn`+* zzB-D-l6punA7TccLEUJ1EHN6}tJ>be42?8hyn{gT2%f?JHlx1`8%3ubzIgUD+l{Z} zywZPd>oBfU=TPOQRac!ykO;6S07GO(O2`r^QZ?Sl@J>QXt%WT1 zT_TI$C>3Kltk`B+@gxRzREzkmDda*T?UmT>y4$Q3T1E@=x7{=rr$W-JO!xnY4$Cre zU}uj^_lux+L33;yJ`klour{QcVj?$2!aAL2IU&@TXU z#V+_imh1jU`S>UK_=$Y{D}QV)17zJ`Ke_gI2N^hb&s*L}2A*Y%{&p<)X90&zmZx&t zsf@Oacf*0gaqy{!5e9Sl^lH9=h)I9RikGldrqE2MQ$aCkTIws*K5JgOYC(vzs!VXMY3x{_Y6z10Lc+ppd;pYBo})$T*M9O{%T zf~rMLl`z?V4ZF(zU%%)5H43Xhk>4+hcMBA9FZX`KzT&;bm2YbeK63lp6UOxT+W&-A z6vlqPk1PKrCiTDN-aYT7FLACw@m)r-twr75 ziQTWxOrKlh*J;SQYM1e#xmRH}L4~RRaL)(5OQx!sLry`@B0aFqHH%6ve|XQEZ{qUb z-Y<#?EEE>J&nP}}6E=StZ;PYYCcnmJ?98QeeMW5SDMHoQV6VCV$fRBEIYh4>HDzqd znpB9`bUVZMOu?qx8N6Q<|L?ameb%$_PP0`*bq)22PM@;$L*-8D_k5hYSC*F0vxvGM z)3fP19=qqwH(kfV{i5i|#PL(i`(p)v8Lt_+t_z^t~GA2Z{l>jwS zGaR>~jfDc7JX4{qDTGD!h*!MBv25`zQg1RE%v~vkCFn<*P)E+;|M4_wFNJ% z$<$iURG^sa8NeQ1rw75o#I?K5?J(f+Ooh5S&+kD|^SA_eI~;hoQ^g?*dXPLSvWMPA zH@+PvJQ1p5s;ufkcd$iW!rLM0I~LFHLGh@UKp9g9wV>d3i25k1tm;9BI|44WyoKc3 zVSK3fjJ*p5siSQJTN#u)Gd19o0xkFn!~*tYSmc4th|$PF?tfG!O}chsIoC7R1zAqg zxxvUc823h2C^Y?Ipj^B_<@Cx`=o-|J?3=%p`@aTCq%bvF6m@C**S8t}jcvxivCa55 zw;BJ|Hsimu^?2t0E&$Nq@8Cn!6HgA$|^rs(Zs9Ec>emDrzgt%gD9fgtw|9#9>(|Akw#(MFx z&;g|U`4wJ2Qnxukts8kSUiPIZQ2T#}b^ZtR&xiES-{YUJg8vP4kVkM!LJKHgUo5_% z82&eyw$Qg$OlA+8rJv)gaosWG z*C}e|IlM|HqxVYaT|X<09TXbh&e0of1yWe@hj!uGCePxR4CvdnXhkA*FExjU^LVm} z$0&>*Z95NCs8MoMmiE4C94=8FZSb>XHOr}}kU>9NaUEK-r#k9|)DDjDJ5E3ztU`!Z Uya=wsXRV^yEheM+^=lsss zynpqVf8AYje|9TWL1tv64`LN^o!b@>Mm#q8tNhLH=ikfsZ98SBT4ia#bGrnNK*%Ue zd7MAX_r;QJ25Pi$Ks4+}Uu8)$@hVx&NELe{rVUTMfsFWDUXXHTc3}SjNM*ynbdpX*?}d7KI)&Uc_;iwLry&$rv)Ymnvnv=SBr4<@~u9 z%9IC2p74WQG@s>o=GCWsOh);?dfNxNbFpZMRlAxnIMw80J?k^O#*9fN3ZPcv8xrzx?Q10ygQzpa@qY_s*d0b-HixSU0|wP@O5Pz^6^2cO>VD7GUoY{M%E`p zYVw_mrx2$1ksArF4I@p&=eF$a@9+rV;RG9p+k1Kuxwi`p?ATA&Isfwhhv#@_Hhmi} z>(a}_ou0VYgIUE5*iyU!Kz#zAoA`VRAMaV7iO+}**zXtY;;y*&r0E{hB3!F14(G01 zyvA-9k#oSpu#Dbj3E*?Q|5dTrvkhlQxE9_|rE}b&@7?D4@0;nKB$qlys7?B3)`VgKPMyj8m*;OIK z5J}|BZkDp9gjSr5`aI=f{$$CnIe%#tM-%X_As5ZA>6Voi@g5oRvAE-mN2vNj-Qsq; zv`^g>kgFb z{VYPR^&^Sks|v;JnoG7#JH9l0G`DTH50MrQpAUKLAosk=t!<UPq0{(YM9j^oA zS2&&5PtP;iRh$DVa?wNSq^~Y&#BXgAD>F%u30Fw8UB`buP9hmd18mdwDNE!0r8|d8 zIOIrevnLR~e^J6?57+qlMb{wapUyF#y^i_Ekod!yr-6L&atgQ7b0Y;uAnBH#`&qRw z{@{G|s3mvdogpTGc4a7O~|Gj4ZVm&i|vED(2b?0lw|5yEHmo@%B>jy7k{G;~r5=!)C zLjm@$ErHRJ-$;{KCqB6nxRu$|g$OB@-;$tsqOOuk%ZK&u&x{mo$5}rMWT4 zuju}-Q8jbpz#w%c8>w*h+bqgZU8dQ!gS>{c%2Q@kisqmlqLWACK1!%lDAb@b#K%~! zmCG!Ru-ZiV^{G3gU8rkNDWBwacYy9YR))`QK3TS#)W@M0@W%s{j&e}8G&+>TFDDKS zk)JFmJmNHiX#Z3i;a5E>G?oX-rWKD*d7@HOE`Guz>QdGOi>Awly~f89wld6Qx?*bB zM#%fw5bCxlK~pV)T+T+Iv`vcUmL7$?y(Qx$Gd_<4N|Zx%KA9PG-tB0K?>84WeV(Q& z^#wo?p%Vej@wNa7e_Lf{QA)!}$?dJ$PEEwocPX^C3c0g8G`{up%bjLf_}|j$7H0^&_|z#x(qecGWP5$rl|p*35&?kN2foK z;~Znf7R!wCXC!+?0#ut}HcI>{@L*U+kZ2Rrm_;OT+fiWMSd;r_q(t*9R6Yg^aP6Rg zD$V`*lMW?;@lT|d1w^;~P-<$D@R0JB%_<=`)@~&{@u^%R zln}|_L06Ihu24FMb~GBHBM2Pz$QLw?c)r|#x`~r~|iTqYyw|N1r`iBJg(f1hewk7jh^feB8*T2;m!ggpu8Uks*9F zPF{EGV(CXJV8wu7Zv_HPUI70VVFiy-{_%18I7`s}&`%Mr*Du2zg8?Wq9>CRA{O?jm z&{=OCH)nw_Cfv)!~%u_L>GQx(8xfA!f7^GAg@o!&Y5c=C8#o%siDpK2mb~IutXsM literal 0 HcmV?d00001 diff --git a/doc/build/doctrees/loop_domain.doctree b/doc/build/doctrees/loop_domain.doctree new file mode 100644 index 0000000000000000000000000000000000000000..fb7155ddefd7fa44f61adc48f6801ec6e99c9535 GIT binary patch literal 24186 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 literal 0 HcmV?d00001 diff --git a/doc/build/doctrees/ndarray.doctree b/doc/build/doctrees/ndarray.doctree new file mode 100644 index 0000000000000000000000000000000000000000..94cc76932df5264f58de763d5ef1e49175de0696 GIT binary patch literal 14264 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 literal 0 HcmV?d00001 diff --git a/doc/build/html/.buildinfo b/doc/build/html/.buildinfo new file mode 100644 index 0000000..1c2ade6 --- /dev/null +++ b/doc/build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: d3a2d3a5a718e5136ae3668f3ebe7514 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/doc/build/html/.nojekyll b/doc/build/html/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html new file mode 100644 index 0000000..9747374 --- /dev/null +++ b/doc/build/html/genindex.html @@ -0,0 +1,254 @@ + + + + + + + + + + + + Index — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ +
    + +
  • Docs »
  • + +
  • Index
  • + + +
  • + + + +
  • + +
+ + +
+
+ +
+ + +
+ +
+

+ © Copyright Copyright (C) 2019 Xiaoyu Wei + +

+
+ Built with Sphinx using a theme provided by Read the Docs. + +
+ +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/index.html b/doc/build/html/index.html new file mode 100644 index 0000000..9009496 --- /dev/null +++ b/doc/build/html/index.html @@ -0,0 +1,225 @@ + + + + + + + + + + + Welcome to Lappy’s documentation! — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

Welcome to Lappy’s documentation!

+ +
+
+

Indices and tables

+ +
+ + +
+ +
+
+ + + + +
+ +
+

+ © Copyright Copyright (C) 2019 Xiaoyu Wei + +

+
+ Built with Sphinx using a theme provided by Read the Docs. + +
+ +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/loop_domain.html b/doc/build/html/loop_domain.html new file mode 100644 index 0000000..cf0b4a1 --- /dev/null +++ b/doc/build/html/loop_domain.html @@ -0,0 +1,383 @@ + + + + + + + + + + + Loop Domain — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

Loop Domain

+
+

Note

+

(Vaporware Warning). +This section mostly contain pseudo-codes that are not implemented yet. +The plans may change at anytime.

+
+

The loop domain of a lazy array is an integer set of all work items. +In lappy, it is represented as a set expression.

+
+

Expression Tree

+

The expression tree of an array domain_expr starts from leaf nodes +of type SetVar, SetParam or SetZero. +It has two levels:

+
    +
  1. (Affine Expressions). +The nodes close to the leaf level that are of type PwAff. +They consist the expression trees of the (irreducible) +low-dimensional polygons of type PwAffComparison. +To avoid branches, the polygons’ subtrees should have disjoint +variables (inames), but may share parameters (loop bounds).

  2. +
  3. (Integer Set Expression). +The remaining nodes of type SetIntersection and SetUnion +represent the expression from the basic polygons +to the loop domain.

  4. +
+

The integer set expression exposes structures within the loop domain +that could benefit array manipulations, e.g.,

+
    +
  1. The PwAff objects are labeled consistently. ISL treat inames +as purely positional placeholders, which may not be as handy. For example, +an FEM code may want to tag an iname as 'i_q_point'. Position of +the iname may change as the array is manipulated (e.g. transpose).

  2. +
  3. The affine expressions are nonlinearity-tolerant w.r.t paramters. +ISL only allow affine operations among paramters. +When assembling the array expression, some loop bounds do not become +affine until more shaped information are fixed (e.g. reshape).

  4. +
+
+
+

Temporaries and CSEs

+

Here is a sample integer set expression, where +OR represents set union, AND represents set intersection.

+

Consider the following pseudo-code:

+
n = Unsigned('n')
+k = arange(n)
+c = outer(sin(k), cos(k))
+
+
+

The expression for c is flattened by default, yielding a single instruction like +(names are replaced for readability).

+
for i, j
+  c[i, j] = sin(i) * cos(j)
+end
+
+
+

If the user choose to make k a temporary,

+
n = Unsigned('n')
+k = arange(n)
+k.pin()
+c = outer(sin(k), cos(k))
+
+
+

The instructions for c shall be like

+
for i, j
+  <> k[i] = i  {id=k}
+  c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
+end
+
+
+

The loop domain is unchanged, +but depending on the loop nesting order, k may be computed multiple times.

+
                AND
+       /-------/  \-------\
+       |                  |
+{ 0 <= i < n }     { 0 <= j < n }
+
+
+

If the user choose to make k a CSE,

+
n = Unsigned('n')
+k = arange(n)
+k.pin_cse()
+c = outer(sin(k), cos(k))
+
+
+

The inames and temporaries of k are then duplicated. +It should produce the same results as evaluating k separately +by calling k.eval() and pass in its value as an argument.

+
for i_dup
+  <> k[i_dup] = i_dup {id=k}
+end
+
+for i, j
+    c[i, j] = sin(k[i]) * cos(k[j]) {dep=k}
+end
+
+
+

And the loop domain acquires a new branch,

+
                                     OR
+                 /------------------/  \-------------\
+                 |                                   |
+                AND                         { 0 <= i_dup < n }
+       /-------/  \-------\
+       |                  |
+{ 0 <= i < n }     { 0 <= j < n }
+
+
+
+
+

Diamond Problem

+

The following code produces a diamond structure in the computation graph:

+
A = ndarray('A', shape='n, n')
+x = ndarray('x', shape='n')
+k = A @ x
+sk = sin(k)
+ck = cos(k)
+C = outer(sk, ck)
+
+
+

The data flow looks like (top to bottom):

+
 A @ x
+   |
+   k
+  /\
+ /  \
+sk  ck
+ \  /
+  \/
+  c
+
+
+

By default, the lazy evaluation expands all expressions, yielding +redundant computations.

+
c[i, j] = sin(sum([k], A[i, k] * x[k])) * cos(sum([k], A[j, k] * x[k]))
+
+
+

Note that the matvec is computed twice above.

+

Besides Loopy’s ability to recognize optimization opportunities, user can +also make k a temporary or a CSE.

+
A = ndarray('A', shape='n, n')
+x = ndarray('x', shape='n')
+k = A @ x
+k.pin()
+sk = sin(k)
+ck = cos(k)
+C = outer(sk, ck)
+
+
+

Making k a temporary yields:

+
<> k[i] = sum([k], A[i, k] * x[k])  {id=k}
+c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
+
+
+

Similarly,

+
A = ndarray('A', shape='n, n')
+x = ndarray('x', shape='n')
+k = A @ x
+k.pin_cse()
+sk = sin(k)
+ck = cos(k)
+C = outer(sk, ck)
+
+
+

Making k a CSE yields:

+
for i_dup
+  <> k[i_dup] = sum([k], A[i_dup, k] * x[k])  {id=k}
+end
+
+for i, j
+  c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
+end
+
+
+
+
+ + +
+ +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/ndarray.html b/doc/build/html/ndarray.html new file mode 100644 index 0000000..4b3042d --- /dev/null +++ b/doc/build/html/ndarray.html @@ -0,0 +1,268 @@ + + + + + + + + + + + NDArray — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

NDArray

+

Lazily evaluated array. An array is a stateful (partial) +closure consisting of three basic components:

+
    +
  1. a formal argument list

  2. +
  3. an expression

  4. +
  5. an environment

  6. +
+

1) and 3) are stored in a single capture list, where the value +None represents items in the argument list. Arguments can be +instantiated and moved into its environment. This process +is monotonic, that is, the environment cannot be modified by +operations other than instantiation of the arguments.

+
+
+lappy.ndarray.ndim
+

number of dimensions specified as an +Unsigned. At the moment, only concrete ndim is +supported, i.e., ndim.value must be known.

+
+ +
+
+lappy.ndarray.shape
+

overall shape of array expression. +Tuple of Unsigned with length equal to ndim.

+
+ +
+
+lappy.ndarray.inames
+

index names for iterating over the array. +Tuple of pymbolic.Variable’s.

+
+ +
+
+lappy.ndarray.domain_expr
+

set expression for constructing the loop domain. A set expression +has leafs of lappy.core.primitives.PwAff variables and +integers. If domain_expr is None, the index domain of the array is +used.

+
+ +
+
+lappy.ndarray.dtype
+

data type, either None or convertible to loopy type. Lappy +does not allow implicit type casting.

+
+ +
+
+lappy.ndarray.is_integral
+

a bool indicating whether array elements are integers

+
+ +
+
+lappy.ndarray.integer_domain
+

an ISL set if is_integral is True, otherwise None. The set represents +the domain for the full array. It is used to express conditions like +non-negative integers, multiples of two, etc.

+
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/objects.inv b/doc/build/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..62cb04caa3122700220e5c199e5f003558ce7e61 GIT binary patch literal 403 zcmV;E0c`#wAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkRVQ_GH z3L_v^WpZ8b#rNMXCQiP zX<{x4c-obcziPuU5XN^s#g%lmY1V9oEQPcbI%G5#>#zm-hjfN$>udD&`XsfjnktZ? z+NAHk-|5qF8of`gl^6mh@abTjoHW4`{A%sUM(?uPJVGOMNwm@#RJNn54-1XVL0u*$ z*xxp|Lvr#|$ta74Vl~7xQ0Q{hS~zwqA5d44Rw`48#1a27l7XzUlCBV09yDG^L$ilX zU48Jf9L1kax_&Vgjx*`r83J~2T0(q(enpsVlOfB<%Gn2@o%7q+iY|qmd!1$hj;kQ% xO}UfKr)Dc; + + + + + + + + Python Module Index — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ +
    + +
  • Docs »
  • + +
  • Python Module Index
  • + + +
  • + +
  • + +
+ + +
+
+
+
+ + +

Python Module Index

+ +
+ l +
+ + + + + + + + + + +
 
+ l
+ lappy +
    + lappy.ndarray +
+ + +
+ +
+
+ + +
+ +
+

+ © Copyright Copyright (C) 2019 Xiaoyu Wei + +

+
+ Built with Sphinx using a theme provided by Read the Docs. + +
+ +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/search.html b/doc/build/html/search.html new file mode 100644 index 0000000..dbdd562 --- /dev/null +++ b/doc/build/html/search.html @@ -0,0 +1,212 @@ + + + + + + + + + + + Search — Lappy 2020.1dev documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ +
    + +
  • Docs »
  • + +
  • Search
  • + + +
  • + + + +
  • + +
+ + +
+
+
+
+ + + + +
+ +
+ +
+ +
+
+ + +
+ +
+

+ © Copyright Copyright (C) 2019 Xiaoyu Wei + +

+
+ Built with Sphinx using a theme provided by Read the Docs. + +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/searchindex.js b/doc/build/html/searchindex.js new file mode 100644 index 0000000..d1789f4 --- /dev/null +++ b/doc/build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["index","loop_domain","ndarray"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.index":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["index.rst","loop_domain.rst","ndarray.rst"],objects:{"lappy.ndarray":{domain_expr:[2,1,1,""],dtype:[2,1,1,""],inames:[2,1,1,""],integer_domain:[2,1,1,""],is_integral:[2,1,1,""],ndim:[2,1,1,""],shape:[2,1,1,""]},lappy:{ndarray:[2,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:attribute"},terms:{"default":1,"new":1,"true":2,AND:1,And:1,For:1,The:[1,2],abil:1,abov:1,acquir:1,affin:1,all:1,allow:[1,2],also:1,among:1,anytim:1,arang:1,argument:[1,2],arrai:[1,2],assembl:1,avoid:1,basic:[1,2],becom:1,benefit:1,besid:1,bool:2,bottom:1,bound:1,branch:1,call:1,can:[1,2],cannot:2,captur:2,cast:2,chang:1,choos:1,close:1,closur:2,code:1,compon:2,comput:1,concret:2,condit:2,consid:1,consist:[1,2],construct:2,contain:1,content:0,convert:2,core:2,cos:1,could:1,cse:0,data:[1,2],defin:[],dep:1,depend:1,detail:[],diamond:0,dimens:2,dimension:1,disjoint:1,doe:2,domain:[0,2],domain_expr:[1,2],dtype:2,duplic:1,either:2,element:2,end:1,environ:2,equal:2,etc:2,eval:1,evalu:[1,2],exampl:1,expand:1,expos:1,express:[0,2],fem:1,fix:1,flatten:1,flow:1,follow:1,formal:2,from:1,full:2,graph:1,handi:1,has:[1,2],have:1,here:1,i_dup:1,i_q_point:1,implement:1,implicit:2,inam:[1,2],index:[0,2],indic:2,inform:1,instanti:2,instead:[],instruct:1,integ:[1,2],integer_domain:2,intersect:1,irreduc:1,is_integr:2,isl:[1,2],item:[1,2],iter:2,its:[1,2],known:2,label:1,lappi:[1,2],lazi:1,lazili:2,leaf:[1,2],length:2,level:1,like:[1,2],list:2,look:1,loop:[0,2],loopi:[1,2],low:1,mai:1,make:1,manipul:1,matvec:1,modifi:2,modul:0,moment:2,monoton:2,more:1,mostli:1,move:2,multipl:[1,2],must:2,name:[1,2],ndarrai:[0,1],ndim:2,neg:2,nest:1,node:1,non:2,none:2,nonlinear:1,note:1,number:2,object:1,onli:[1,2],oper:[1,2],opportun:1,optim:1,order:1,other:2,otherwis:2,outer:1,over:2,overal:2,page:0,paramet:1,paramt:1,partial:2,pass:1,pin:1,pin_cs:1,placehold:1,plan:1,polygon:1,posit:1,primit:2,problem:0,process:2,produc:1,pseudo:1,pure:1,pwaff:[1,2],pwaffcomparison:1,pymbol:2,readabl:1,recogn:1,redund:1,remain:1,replac:1,repres:[1,2],reshap:1,result:1,same:1,sampl:1,search:0,section:1,separ:1,set:[1,2],setintersect:1,setparam:1,setunion:1,setvar:1,setzero:1,shall:1,shape:[1,2],share:1,should:1,similarli:1,sin:1,singl:[1,2],some:1,specifi:2,start:1,state:2,store:2,structur:1,subtre:1,sum:1,support:2,tag:1,temporari:0,than:2,thei:1,thi:[1,2],three:2,time:1,toler:1,top:1,transpos:1,treat:1,tree:0,tupl:2,twice:1,two:[1,2],type:[1,2],unchang:1,union:1,unsign:[1,2],until:1,used:2,user:1,using:[],valu:[1,2],vaporwar:1,variabl:[1,2],want:1,warn:1,when:1,where:[1,2],whether:2,which:1,within:1,work:1,yet:1,yield:1},titles:["Welcome to Lappy\u2019s documentation!","Loop Domain","NDArray"],titleterms:{branch:[],cse:1,diamond:1,document:0,domain:1,express:1,indic:0,lappi:0,loop:1,ndarrai:2,problem:1,tabl:0,temporari:1,tree:1,welcom:0}}) \ No newline at end of file -- GitLab From 7cff32157c6e61e7c7b7f9bf606d810f54da14b0 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 12:39:51 -0500 Subject: [PATCH 54/59] Remove built docs --- .gitignore | 3 - doc/build/doctrees/environment.pickle | Bin 15172 -> 0 bytes doc/build/doctrees/index.doctree | Bin 4852 -> 0 bytes doc/build/doctrees/loop_domain.doctree | Bin 24186 -> 0 bytes doc/build/doctrees/ndarray.doctree | Bin 14264 -> 0 bytes doc/build/html/.buildinfo | 4 - doc/build/html/.nojekyll | 0 doc/build/html/genindex.html | 254 ---------------- doc/build/html/index.html | 225 --------------- doc/build/html/loop_domain.html | 383 ------------------------- doc/build/html/ndarray.html | 268 ----------------- doc/build/html/objects.inv | Bin 403 -> 0 bytes doc/build/html/py-modindex.html | 216 -------------- doc/build/html/search.html | 212 -------------- doc/build/html/searchindex.js | 1 - 15 files changed, 1566 deletions(-) delete mode 100644 doc/build/doctrees/environment.pickle delete mode 100644 doc/build/doctrees/index.doctree delete mode 100644 doc/build/doctrees/loop_domain.doctree delete mode 100644 doc/build/doctrees/ndarray.doctree delete mode 100644 doc/build/html/.buildinfo delete mode 100644 doc/build/html/.nojekyll delete mode 100644 doc/build/html/genindex.html delete mode 100644 doc/build/html/index.html delete mode 100644 doc/build/html/loop_domain.html delete mode 100644 doc/build/html/ndarray.html delete mode 100644 doc/build/html/objects.inv delete mode 100644 doc/build/html/py-modindex.html delete mode 100644 doc/build/html/search.html delete mode 100644 doc/build/html/searchindex.js diff --git a/.gitignore b/.gitignore index 41e385f..3453fe6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ __pycache__ !doc/ !doc/Makefile -!doc/build -!doc/build/html/*.html - !doc/source/*.rst !doc/source/_static !doc/source/_templates diff --git a/doc/build/doctrees/environment.pickle b/doc/build/doctrees/environment.pickle deleted file mode 100644 index acff3cc31f54b8ac431e2c787cf06da53a9b29bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15172 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 Uya=wsXRV^yEheM+^=lsss zynpqVf8AYje|9TWL1tv64`LN^o!b@>Mm#q8tNhLH=ikfsZ98SBT4ia#bGrnNK*%Ue zd7MAX_r;QJ25Pi$Ks4+}Uu8)$@hVx&NELe{rVUTMfsFWDUXXHTc3}SjNM*ynbdpX*?}d7KI)&Uc_;iwLry&$rv)Ymnvnv=SBr4<@~u9 z%9IC2p74WQG@s>o=GCWsOh);?dfNxNbFpZMRlAxnIMw80J?k^O#*9fN3ZPcv8xrzx?Q10ygQzpa@qY_s*d0b-HixSU0|wP@O5Pz^6^2cO>VD7GUoY{M%E`p zYVw_mrx2$1ksArF4I@p&=eF$a@9+rV;RG9p+k1Kuxwi`p?ATA&Isfwhhv#@_Hhmi} z>(a}_ou0VYgIUE5*iyU!Kz#zAoA`VRAMaV7iO+}**zXtY;;y*&r0E{hB3!F14(G01 zyvA-9k#oSpu#Dbj3E*?Q|5dTrvkhlQxE9_|rE}b&@7?D4@0;nKB$qlys7?B3)`VgKPMyj8m*;OIK z5J}|BZkDp9gjSr5`aI=f{$$CnIe%#tM-%X_As5ZA>6Voi@g5oRvAE-mN2vNj-Qsq; zv`^g>kgFb z{VYPR^&^Sks|v;JnoG7#JH9l0G`DTH50MrQpAUKLAosk=t!<UPq0{(YM9j^oA zS2&&5PtP;iRh$DVa?wNSq^~Y&#BXgAD>F%u30Fw8UB`buP9hmd18mdwDNE!0r8|d8 zIOIrevnLR~e^J6?57+qlMb{wapUyF#y^i_Ekod!yr-6L&atgQ7b0Y;uAnBH#`&qRw z{@{G|s3mvdogpTGc4a7O~|Gj4ZVm&i|vED(2b?0lw|5yEHmo@%B>jy7k{G;~r5=!)C zLjm@$ErHRJ-$;{KCqB6nxRu$|g$OB@-;$tsqOOuk%ZK&u&x{mo$5}rMWT4 zuju}-Q8jbpz#w%c8>w*h+bqgZU8dQ!gS>{c%2Q@kisqmlqLWACK1!%lDAb@b#K%~! zmCG!Ru-ZiV^{G3gU8rkNDWBwacYy9YR))`QK3TS#)W@M0@W%s{j&e}8G&+>TFDDKS zk)JFmJmNHiX#Z3i;a5E>G?oX-rWKD*d7@HOE`Guz>QdGOi>Awly~f89wld6Qx?*bB zM#%fw5bCxlK~pV)T+T+Iv`vcUmL7$?y(Qx$Gd_<4N|Zx%KA9PG-tB0K?>84WeV(Q& z^#wo?p%Vej@wNa7e_Lf{QA)!}$?dJ$PEEwocPX^C3c0g8G`{up%bjLf_}|j$7H0^&_|z#x(qecGWP5$rl|p*35&?kN2foK z;~Znf7R!wCXC!+?0#ut}HcI>{@L*U+kZ2Rrm_;OT+fiWMSd;r_q(t*9R6Yg^aP6Rg zD$V`*lMW?;@lT|d1w^;~P-<$D@R0JB%_<=`)@~&{@u^%R zln}|_L06Ihu24FMb~GBHBM2Pz$QLw?c)r|#x`~r~|iTqYyw|N1r`iBJg(f1hewk7jh^feB8*T2;m!ggpu8Uks*9F zPF{EGV(CXJV8wu7Zv_HPUI70VVFiy-{_%18I7`s}&`%Mr*Du2zg8?Wq9>CRA{O?jm z&{=OCH)nw_Cfv)!~%u_L>GQx(8xfA!f7^GAg@o!&Y5c=C8#o%siDpK2mb~IutXsM diff --git a/doc/build/doctrees/loop_domain.doctree b/doc/build/doctrees/loop_domain.doctree deleted file mode 100644 index fb7155ddefd7fa44f61adc48f6801ec6e99c9535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24186 zcmeHPTZ|;vS>D^s?DX!=tnHm7UVHHgCQOerm)ImxSZu<2y>?c%$BTEpc6_0yx~gWX zx~r?&Rn;@o-Ze_3Sa_rJ07^R{F9{HFNfZQ%EEL3x0U=(1h!<{w7bIS|iOT~b34!lF zRdvp(nwsvKaj%UPue3egr_S{||No!=R_9YkUJHKafcVE}tibHWt`{x)fn`Snev}G2 zwjU2ZH(2=e;N?M`Plv`<6!b#V9`FOGVLGm7g|R8ff-ZX~=oqd)plYafo0%Z^q&qucGT0JD{3N-G|g z*RaI$mcs}$zVp#Ty(TTv58@mW*Xvh}ZV+x^W$bAq^j&{rVYzn2aU+&O$~r+5dwpgG zehj!+H?n(Huw>FsvDh(UMs3kAb30wnrd_iw*0*Eo#%5j5@FUhS(1Bz48#bc`em{0Q z_HqXOG=McWLZj>GoAm!+YIFSUorI#kkAMh%3=aUGY8 z4W>Nc78;&b(RoTpr#U_$AXMvsbUW>Qvbe`}engDK4|V$k$gBPm;2+pUI(ZsxFBjYD zmG~s)3wZ)zsELvRgfJB+Qq$s7_!alN5XgsV7=8@Dv5lT5pLKq#A32UBIKV=3AsiXgD~fKy)yBrt*+jtxRrJ#Hlx`I!HgN! zGSFj@Wt_beLxw>k#gc^)-k9+HIC>-r2Rwc6`+c!PQ#9 zB?$7)8Vf?b@L4+y)*uaH=78PftpL4l+Fo>4AVG}IMom!>eucU~uwm@R?l>sp1n}>f~_SBdcRLvp5d`>;Pp2GSO=x!oDVuM3*0F1X~cjTn}KVw zW@z}PV@Hdk|E!037WJBRuEApdpo6AzqS{$cJhVJwSAltJ5p_^?f&K_DVCt@E zEulo31YfIPOr0@M$Fw6qJ@G@Etai8}(zCyK!bUdvPP==?E#eVnjjkOFp{FGxQj(m z^-DxUsrR8!S4~Zbs$UUf?C*g3Qc2q?qU!5tx32?gwzw8uRNbKvgBwlIwOVUdpz5dt zK(`wc@-M>qM+#0dzeFX~x4`j_OR%Ua^8c`;Z515Owmsknm&s?}n#U|M zqc8P(p31rJLReclN4sWh;VVoy7(H>=Gvgi{=xoc4;e;0AS@Yl5M4-kLrcu-OF&PTm zkR!ii_-?o785BK(Rb?-4EH5T>KF8mVp`~5jI(SH&d;d!SiE}S;v*%@!&JRSiSqOw> znD&~jI(o>?NiZekUp;qrK65)N5Xw1?4_?fs2(O9wmS;3=4{ zBX=r7T8UX%gMYJ&Z9upT8{-nDWaoN@X*+>u;dotSTaIfw!t$h;SrY-V2-d1^p}U9S zqOrFP3ZpI75MFus!s8gwvPJDJ!-ugAW8K(DLNBmasqNa@yxVxO(IrE7er;`;T}nrW zS&eNc+M$|y1dif%XqyFcBne=Bbs_+)2|+s|trGk9eeZ%L_@xXisUI96a?+$+@*6_u z6o?w^txlN{hU8tu1j&hAYDm6c^q1#}s^`m@lD0ZO$tY0Xqucr*+U@I{E-@tUR+!B3 zUw? zcR%3uQ--Mbsm zvItoxNj^a07e!itNcm{mCZ5#NN%wpt@gQFW^MzNn3km7^>p|ENV~b@#^O15B+6q;| z5mWTB^8fk)MIvLsAK3LP?Qes4<#fv)&0SLGtE0X#y8f=*xI53cz5}JF^b z%>^j|OMpB$=PzG~GNLMfs1WA~;-95ImGy0;9<+W=wh(h%$(b+RPuLI#lrF>~q1onzgmHC!RzOf^;-d9$L(L#WXJ7p98aDKuE_=z+(tkLuWv#mvX`H%bT0rnJDvEkcR zecqp6sI^f7*8{RG>;6K`#IFDesbM{G{dyarPcw+{cOl#2H_?M!t{{xR8$lR+db{JD zjk++0k-;mH0K-juoq&>nq8t1ul0`Rq#sP_sycSr0&CQ4`oAxfHW0UDA__**er34YbY#Qix+YW*!BlEhRe>TwrY ztHzl59DR8Kd_^4PF0gw9OSKkiNcYTkrg{U3j@ZCchlig9t$)I_TK`NR|ALPjt$)QQ zpGrx1(E2z0+`%Is&#g>L^j*4`WNP#x$UxHx(u|Of(YC=WZSac0kXj;?ihkl4q>%j` zLz*dq99|7m`Vp*E7mj%{$LJZD2ivMwfzr|llrl1>5d@i^$S*`<&I=DpW>t-NF61E2 z6VJI(GP=Fo1tnFG)nn*ME?ofm@CYEw$t{!4<<#~_4hoIh>Ld*{i3%|+WipIGq>sZN zN>WzrI}zGkX$wURyH&spu3}}o6BmvFNnAx4+8BXJMqD(4Ag&p*ceU9|h{Nt=az}fI75>5G0ndnkD@)106~>-VzdoNm zyIyF9pVw>G*oWBt#C-QNpS>(ZL{Z4} z3pgppu33C@-xU7)BcRDBoJJ57KE(|3vn*=7DvJP1aVFL1)yM&wr`HRWw26ip*;O=( zG4+k1{8vACVBOuC|IK47`m7HM6b4tk!UZ|LS-FW2^Zzxet zwaDvE^M$)1UQaSb74lQr@K{8U?dULK_NL+W;KRmYkn|PVeaG;#8$&z7lb zeSx%QIN!Q(t3%ttTW=AgE=0H%VO^N9;^kI#T>Q}R6=;+-swOb{M_|7{;r^c^b}b`) z8bOf$Bf<=R^RQ|Q99x%f3W>=hw8DH?RS%8sK;pe{Q(7(43^J?C?06 z6{-G(T)%y<^ZwC_lq%VqS8Y+Us%oOIg_gS=)t1n78{lCa!ilO&KZLqVUowps`dqE1 zX~jjGeWwOoq5!`&gGpq?2HrPf14{+x5WU#XfvE}2d(J^Zfpc@Zv0=X0b193+V7|ST zzHA}Wf%eiwuBvq&E5ans(Vsp0LHoJs=Z?W=D0gX3A5Fze zN~NNwnw6-0>S6lr`i{|{PwuFd6=eFNpkARfX3euHYV(paDw#)h)$?8CGVqhXRgjbP zEV%TEF>~p)9GNRI$xmsz%o22pB`9@~7CqFT3rU_n9}+^O@Z69P;_O2%?oIrwAu-WyL;kr3LLJ>79w!?DrTm<_ZLYgQV|)E2bXGTB%iEwnlkl~G%b>6 ziEW;w9Edw5WB_N3J*vg`lf`ezrR*h35$}0R5Qby2#EmqtBUm@&o&F?Az|IZtDKZB zrHO{rR|gOCO&mdD)_Ogmfp6@K2ELL5Cr<;96S`HG8UhZAR1xGjskdjT*vbKigvrRjV`AWBKyye2y zH=4m_dI=GQ8fNlMm1VW#2?qaGVJEf76;fETK{He*lLFPH0OhYnfRY)RG?p;8Zztz< z`649Td`P8|c`IFGSpLM3(5Cc>h&_fEy!!KC!z2Pu!ZSo};D+zMgv&;{aF4q$ zC1-NbMKCc&LQf3;%pywykA`0}yeNSGw=C{RR~HB$oAQ`k%?*tA3&%6G+97@=H^P{f zM(+sz`|hNJ*#0o*`*$i542>WN<~@Kd(;)jAii%oxt5Gt~YNYb@9I51m)#g}Knk-hj zXVvEI8eXC;xc;_@@vAt$l>=Q9hwO2qt^AfcF^J<)yMkvbt*LQQ$%o@>P2!^t*b;vr(0L(&z_w@ zZ{&v0@6=1<6D?`rWY6N|?VN%{SnPXqKl?tyiu!{b_}bo|E{GU}!V=xPx^M{# zTpO{{Ot{bpLb%K%iiDh@1P+-RLdB3zu-@XlstJ@o$sv^|(`&o?uXO=K&Olu~mqMoK z*RCpb^t_I@bq*45L8yZf+qYK%^=CQg)pqgO)Lpa$50da|X$kn5CrLdT;yr_pO6ykK zj<}TGh^V?0#p%tNJM;MuCz4ONyl^Y)?m;Gb!>n@;(eXh(WA<>Lk{>tZ&9ld|lH>+j zeiX^_6sW`?(1YInF7o!{7 z;DF8mB{T5cx!nfL8M-nyLYH?WC3w$Plr-p7=0k^_eRv*G3zuK^;`ODEEg{i*!0*gIY}jEKgbjL)7jI?(XPT;sJ1uA7W6Sq4A$l3` zGleIGE?R#9*5ETqziJK>PhEL<>0=l_|De#ApQJTJ0Ln>Ilm|#4PgARbxP|=cL%2Gb z-$piCXf=`yC=3IW3^+{UE$4MC6NtE0L%q8`WAx&np^Eq+7esZ!>UFveNq7KO6o?#u zy#7ob&WaN8RxxeADoXO~8%PUBt&{NIApf+Xw-i)j#*IxEuTu*Woo(P+FS^VN3rS}= zl`({K4?m8@P~w&_Hqq&cv|3}`#r8hQ1tx8!MK*xq_O}%7D*N3+jTB1I*TpbS-0?8PbZj)i0#@t{9zDv>fS@`J$y;*zjU=1?27iy34RR~( zklyqI-4aF?2>BRp&XzyHcqgMiZgSpk$h`UiF0jU1fdhU#>fq+j#*00$Z-kb&iQmc3 z2Hra`QUuQ+eGZ(0R|*og!j6&rX`pqx*F_#bUFav428bju>19C0|F@DvpljRc za&B4gB;G^BZFf=J$5@VydtqSATjn}~Cx(HBG6y#NY*!DN++HHO`kpy5p zP?3u(cR|;2Be_^s?D;Ji2viZ{mT_Q%TxoibfZv)c6?y$sjNaDe54s7L?(jzW6cr}l z>#g4gf3|*;K0bql>(-~~!-U=4TBDEOpa;K-j~<_QCyd07YD5XiR_?Rpf#89(Hz@@T)&fQ*f`Tqd^Jx=Zb diff --git a/doc/build/doctrees/ndarray.doctree b/doc/build/doctrees/ndarray.doctree deleted file mode 100644 index 94cc76932df5264f58de763d5ef1e49175de0696..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14264 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 diff --git a/doc/build/html/.buildinfo b/doc/build/html/.buildinfo deleted file mode 100644 index 1c2ade6..0000000 --- a/doc/build/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: d3a2d3a5a718e5136ae3668f3ebe7514 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/doc/build/html/.nojekyll b/doc/build/html/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html deleted file mode 100644 index 9747374..0000000 --- a/doc/build/html/genindex.html +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - - - - - Index — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Index
  • - - -
  • - - - -
  • - -
- - -
-
- -
- - -
- -
-

- © Copyright Copyright (C) 2019 Xiaoyu Wei - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/index.html b/doc/build/html/index.html deleted file mode 100644 index 9009496..0000000 --- a/doc/build/html/index.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - Welcome to Lappy’s documentation! — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

Welcome to Lappy’s documentation!

- -
-
-

Indices and tables

- -
- - -
- -
-
- - - - -
- -
-

- © Copyright Copyright (C) 2019 Xiaoyu Wei - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/loop_domain.html b/doc/build/html/loop_domain.html deleted file mode 100644 index cf0b4a1..0000000 --- a/doc/build/html/loop_domain.html +++ /dev/null @@ -1,383 +0,0 @@ - - - - - - - - - - - Loop Domain — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

Loop Domain

-
-

Note

-

(Vaporware Warning). -This section mostly contain pseudo-codes that are not implemented yet. -The plans may change at anytime.

-
-

The loop domain of a lazy array is an integer set of all work items. -In lappy, it is represented as a set expression.

-
-

Expression Tree

-

The expression tree of an array domain_expr starts from leaf nodes -of type SetVar, SetParam or SetZero. -It has two levels:

-
    -
  1. (Affine Expressions). -The nodes close to the leaf level that are of type PwAff. -They consist the expression trees of the (irreducible) -low-dimensional polygons of type PwAffComparison. -To avoid branches, the polygons’ subtrees should have disjoint -variables (inames), but may share parameters (loop bounds).

  2. -
  3. (Integer Set Expression). -The remaining nodes of type SetIntersection and SetUnion -represent the expression from the basic polygons -to the loop domain.

  4. -
-

The integer set expression exposes structures within the loop domain -that could benefit array manipulations, e.g.,

-
    -
  1. The PwAff objects are labeled consistently. ISL treat inames -as purely positional placeholders, which may not be as handy. For example, -an FEM code may want to tag an iname as 'i_q_point'. Position of -the iname may change as the array is manipulated (e.g. transpose).

  2. -
  3. The affine expressions are nonlinearity-tolerant w.r.t paramters. -ISL only allow affine operations among paramters. -When assembling the array expression, some loop bounds do not become -affine until more shaped information are fixed (e.g. reshape).

  4. -
-
-
-

Temporaries and CSEs

-

Here is a sample integer set expression, where -OR represents set union, AND represents set intersection.

-

Consider the following pseudo-code:

-
n = Unsigned('n')
-k = arange(n)
-c = outer(sin(k), cos(k))
-
-
-

The expression for c is flattened by default, yielding a single instruction like -(names are replaced for readability).

-
for i, j
-  c[i, j] = sin(i) * cos(j)
-end
-
-
-

If the user choose to make k a temporary,

-
n = Unsigned('n')
-k = arange(n)
-k.pin()
-c = outer(sin(k), cos(k))
-
-
-

The instructions for c shall be like

-
for i, j
-  <> k[i] = i  {id=k}
-  c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
-end
-
-
-

The loop domain is unchanged, -but depending on the loop nesting order, k may be computed multiple times.

-
                AND
-       /-------/  \-------\
-       |                  |
-{ 0 <= i < n }     { 0 <= j < n }
-
-
-

If the user choose to make k a CSE,

-
n = Unsigned('n')
-k = arange(n)
-k.pin_cse()
-c = outer(sin(k), cos(k))
-
-
-

The inames and temporaries of k are then duplicated. -It should produce the same results as evaluating k separately -by calling k.eval() and pass in its value as an argument.

-
for i_dup
-  <> k[i_dup] = i_dup {id=k}
-end
-
-for i, j
-    c[i, j] = sin(k[i]) * cos(k[j]) {dep=k}
-end
-
-
-

And the loop domain acquires a new branch,

-
                                     OR
-                 /------------------/  \-------------\
-                 |                                   |
-                AND                         { 0 <= i_dup < n }
-       /-------/  \-------\
-       |                  |
-{ 0 <= i < n }     { 0 <= j < n }
-
-
-
-
-

Diamond Problem

-

The following code produces a diamond structure in the computation graph:

-
A = ndarray('A', shape='n, n')
-x = ndarray('x', shape='n')
-k = A @ x
-sk = sin(k)
-ck = cos(k)
-C = outer(sk, ck)
-
-
-

The data flow looks like (top to bottom):

-
 A @ x
-   |
-   k
-  /\
- /  \
-sk  ck
- \  /
-  \/
-  c
-
-
-

By default, the lazy evaluation expands all expressions, yielding -redundant computations.

-
c[i, j] = sin(sum([k], A[i, k] * x[k])) * cos(sum([k], A[j, k] * x[k]))
-
-
-

Note that the matvec is computed twice above.

-

Besides Loopy’s ability to recognize optimization opportunities, user can -also make k a temporary or a CSE.

-
A = ndarray('A', shape='n, n')
-x = ndarray('x', shape='n')
-k = A @ x
-k.pin()
-sk = sin(k)
-ck = cos(k)
-C = outer(sk, ck)
-
-
-

Making k a temporary yields:

-
<> k[i] = sum([k], A[i, k] * x[k])  {id=k}
-c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
-
-
-

Similarly,

-
A = ndarray('A', shape='n, n')
-x = ndarray('x', shape='n')
-k = A @ x
-k.pin_cse()
-sk = sin(k)
-ck = cos(k)
-C = outer(sk, ck)
-
-
-

Making k a CSE yields:

-
for i_dup
-  <> k[i_dup] = sum([k], A[i_dup, k] * x[k])  {id=k}
-end
-
-for i, j
-  c[i, j] = sin(k[i]) * cos(k[j])  {dep=k}
-end
-
-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/ndarray.html b/doc/build/html/ndarray.html deleted file mode 100644 index 4b3042d..0000000 --- a/doc/build/html/ndarray.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - - - NDArray — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

NDArray

-

Lazily evaluated array. An array is a stateful (partial) -closure consisting of three basic components:

-
    -
  1. a formal argument list

  2. -
  3. an expression

  4. -
  5. an environment

  6. -
-

1) and 3) are stored in a single capture list, where the value -None represents items in the argument list. Arguments can be -instantiated and moved into its environment. This process -is monotonic, that is, the environment cannot be modified by -operations other than instantiation of the arguments.

-
-
-lappy.ndarray.ndim
-

number of dimensions specified as an -Unsigned. At the moment, only concrete ndim is -supported, i.e., ndim.value must be known.

-
- -
-
-lappy.ndarray.shape
-

overall shape of array expression. -Tuple of Unsigned with length equal to ndim.

-
- -
-
-lappy.ndarray.inames
-

index names for iterating over the array. -Tuple of pymbolic.Variable’s.

-
- -
-
-lappy.ndarray.domain_expr
-

set expression for constructing the loop domain. A set expression -has leafs of lappy.core.primitives.PwAff variables and -integers. If domain_expr is None, the index domain of the array is -used.

-
- -
-
-lappy.ndarray.dtype
-

data type, either None or convertible to loopy type. Lappy -does not allow implicit type casting.

-
- -
-
-lappy.ndarray.is_integral
-

a bool indicating whether array elements are integers

-
- -
-
-lappy.ndarray.integer_domain
-

an ISL set if is_integral is True, otherwise None. The set represents -the domain for the full array. It is used to express conditions like -non-negative integers, multiples of two, etc.

-
- -
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/objects.inv b/doc/build/html/objects.inv deleted file mode 100644 index 62cb04caa3122700220e5c199e5f003558ce7e61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 403 zcmV;E0c`#wAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkRVQ_GH z3L_v^WpZ8b#rNMXCQiP zX<{x4c-obcziPuU5XN^s#g%lmY1V9oEQPcbI%G5#>#zm-hjfN$>udD&`XsfjnktZ? z+NAHk-|5qF8of`gl^6mh@abTjoHW4`{A%sUM(?uPJVGOMNwm@#RJNn54-1XVL0u*$ z*xxp|Lvr#|$ta74Vl~7xQ0Q{hS~zwqA5d44Rw`48#1a27l7XzUlCBV09yDG^L$ilX zU48Jf9L1kax_&Vgjx*`r83J~2T0(q(enpsVlOfB<%Gn2@o%7q+iY|qmd!1$hj;kQ% xO}UfKr)Dc; - - - - - - - - Python Module Index — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Python Module Index
  • - - -
  • - -
  • - -
- - -
-
-
-
- - -

Python Module Index

- -
- l -
- - - - - - - - - - -
 
- l
- lappy -
    - lappy.ndarray -
- - -
- -
-
- - -
- -
-

- © Copyright Copyright (C) 2019 Xiaoyu Wei - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/search.html b/doc/build/html/search.html deleted file mode 100644 index dbdd562..0000000 --- a/doc/build/html/search.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - - - - - Search — Lappy 2020.1dev documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Search
  • - - -
  • - - - -
  • - -
- - -
-
-
-
- - - - -
- -
- -
- -
-
- - -
- -
-

- © Copyright Copyright (C) 2019 Xiaoyu Wei - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/build/html/searchindex.js b/doc/build/html/searchindex.js deleted file mode 100644 index d1789f4..0000000 --- a/doc/build/html/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({docnames:["index","loop_domain","ndarray"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.index":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["index.rst","loop_domain.rst","ndarray.rst"],objects:{"lappy.ndarray":{domain_expr:[2,1,1,""],dtype:[2,1,1,""],inames:[2,1,1,""],integer_domain:[2,1,1,""],is_integral:[2,1,1,""],ndim:[2,1,1,""],shape:[2,1,1,""]},lappy:{ndarray:[2,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:attribute"},terms:{"default":1,"new":1,"true":2,AND:1,And:1,For:1,The:[1,2],abil:1,abov:1,acquir:1,affin:1,all:1,allow:[1,2],also:1,among:1,anytim:1,arang:1,argument:[1,2],arrai:[1,2],assembl:1,avoid:1,basic:[1,2],becom:1,benefit:1,besid:1,bool:2,bottom:1,bound:1,branch:1,call:1,can:[1,2],cannot:2,captur:2,cast:2,chang:1,choos:1,close:1,closur:2,code:1,compon:2,comput:1,concret:2,condit:2,consid:1,consist:[1,2],construct:2,contain:1,content:0,convert:2,core:2,cos:1,could:1,cse:0,data:[1,2],defin:[],dep:1,depend:1,detail:[],diamond:0,dimens:2,dimension:1,disjoint:1,doe:2,domain:[0,2],domain_expr:[1,2],dtype:2,duplic:1,either:2,element:2,end:1,environ:2,equal:2,etc:2,eval:1,evalu:[1,2],exampl:1,expand:1,expos:1,express:[0,2],fem:1,fix:1,flatten:1,flow:1,follow:1,formal:2,from:1,full:2,graph:1,handi:1,has:[1,2],have:1,here:1,i_dup:1,i_q_point:1,implement:1,implicit:2,inam:[1,2],index:[0,2],indic:2,inform:1,instanti:2,instead:[],instruct:1,integ:[1,2],integer_domain:2,intersect:1,irreduc:1,is_integr:2,isl:[1,2],item:[1,2],iter:2,its:[1,2],known:2,label:1,lappi:[1,2],lazi:1,lazili:2,leaf:[1,2],length:2,level:1,like:[1,2],list:2,look:1,loop:[0,2],loopi:[1,2],low:1,mai:1,make:1,manipul:1,matvec:1,modifi:2,modul:0,moment:2,monoton:2,more:1,mostli:1,move:2,multipl:[1,2],must:2,name:[1,2],ndarrai:[0,1],ndim:2,neg:2,nest:1,node:1,non:2,none:2,nonlinear:1,note:1,number:2,object:1,onli:[1,2],oper:[1,2],opportun:1,optim:1,order:1,other:2,otherwis:2,outer:1,over:2,overal:2,page:0,paramet:1,paramt:1,partial:2,pass:1,pin:1,pin_cs:1,placehold:1,plan:1,polygon:1,posit:1,primit:2,problem:0,process:2,produc:1,pseudo:1,pure:1,pwaff:[1,2],pwaffcomparison:1,pymbol:2,readabl:1,recogn:1,redund:1,remain:1,replac:1,repres:[1,2],reshap:1,result:1,same:1,sampl:1,search:0,section:1,separ:1,set:[1,2],setintersect:1,setparam:1,setunion:1,setvar:1,setzero:1,shall:1,shape:[1,2],share:1,should:1,similarli:1,sin:1,singl:[1,2],some:1,specifi:2,start:1,state:2,store:2,structur:1,subtre:1,sum:1,support:2,tag:1,temporari:0,than:2,thei:1,thi:[1,2],three:2,time:1,toler:1,top:1,transpos:1,treat:1,tree:0,tupl:2,twice:1,two:[1,2],type:[1,2],unchang:1,union:1,unsign:[1,2],until:1,used:2,user:1,using:[],valu:[1,2],vaporwar:1,variabl:[1,2],want:1,warn:1,when:1,where:[1,2],whether:2,which:1,within:1,work:1,yet:1,yield:1},titles:["Welcome to Lappy\u2019s documentation!","Loop Domain","NDArray"],titleterms:{branch:[],cse:1,diamond:1,document:0,domain:1,express:1,indic:0,lappi:0,loop:1,ndarrai:2,problem:1,tabl:0,temporari:1,tree:1,welcom:0}}) \ No newline at end of file -- GitLab From 36f40f625115800eb3885e38d267dc8f044fc160 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 14:15:31 -0500 Subject: [PATCH 55/59] CI build doc --- .gitlab-ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ee5f898..cd06f82 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,3 +32,29 @@ Flake8: - python3 except: - tags + +Documentation Sphinx: + script: + - export PY_EXE=python3 + - export PYOPENCL_TEST=portable + - export EXTRA_INSTALL="pybind11 numpy" + - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-py-project.sh + - ". ./build-py-project.sh" + - cd doc && make html + - git config --global user.name "${COMMIT_USER}" + - git config --global user.email "${COMMIT_EMAIL}" + - git clone https://${GH_USER}:${GH_ACCESS_TOKEN}@${HOMEPAGE_URL} homepage + - cd homepage + - git checkout src + - cd static && mkdir -p docs + - rm -rf docs/lappy + - cp -r ../../build/html docs/lappy + - git add -f ./docs/lappy + - (git commit -m "Auto-updated lappy docs") || (echo "Docs are up to date") + - git push + tags: + - python3 + - pocl + except: + - tags + allow_failure: true -- GitLab From ecf97b96540cd57b350a508d3e34da76e04b7130 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 14:22:27 -0500 Subject: [PATCH 56/59] Add dependencies for building doc --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index e4b351c..f9171c0 100644 --- a/setup.py +++ b/setup.py @@ -83,6 +83,11 @@ setup(name="lappy", "numpy", ], + extras_require={ + "test": ["pytest", ], + "doc": ["sphinx", "sphinx_rtd_theme"], + }, + author="Sophia Lin, Andreas Kloeckner, Xiaoyu Wei", url="http://pypi.python.org/pypi/lappy", author_email="svlin2@illinois.edu", -- GitLab From eb67e78007c56b1464ff66a0cf29eb98cd0ceb58 Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2020 14:32:14 -0500 Subject: [PATCH 57/59] Install doc deps --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd06f82..bb102b2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ Documentation Sphinx: script: - export PY_EXE=python3 - export PYOPENCL_TEST=portable - - export EXTRA_INSTALL="pybind11 numpy" + - export EXTRA_INSTALL="pybind11 numpy sphinx sphinx_rtd_theme" - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-py-project.sh - ". ./build-py-project.sh" - cd doc && make html -- GitLab From 23727665bf892a9f5faab90c3b7a65e095dc9a9b Mon Sep 17 00:00:00 2001 From: xywei Date: Wed, 8 Apr 2020 19:24:55 -0500 Subject: [PATCH 58/59] Add simple indexing and some multi-insn support --- experiments/lazy_arange.py | 16 ++- lappy/__init__.py | 2 +- lappy/core/array.py | 62 +++++++-- lappy/core/basic_ops.py | 244 +++++++++++++++++------------------- lappy/core/conditional.py | 24 ++-- lappy/core/function_base.py | 1 + lappy/eval/compiler.py | 58 ++++++--- test/test_broadcast.py | 2 + 8 files changed, 240 insertions(+), 169 deletions(-) diff --git a/experiments/lazy_arange.py b/experiments/lazy_arange.py index c7b6a7f..2baab52 100644 --- a/experiments/lazy_arange.py +++ b/experiments/lazy_arange.py @@ -1,11 +1,17 @@ import pyopencl as cl -from lappy.core.function_base import arange, linspace +from lappy.core.function_base import arange ctx = cl.create_some_context() queue = cl.CommandQueue(ctx) -a = arange(10) -print(a.eval(queue)) +a = arange(64) +# print(a.eval(queue)) -b = linspace(0, 1, 10) -print(b.eval(queue)) +b = a.reshape([2, 2, 16]) +# print(b.eval(queue)) + +# FIXME: there needs to be a barrier after reshape's temporary + +c = b[:, 1, ::2] +print(c.ndim) +print(c.eval(queue)) diff --git a/lappy/__init__.py b/lappy/__init__.py index 18f6aa6..2ab6845 100644 --- a/lappy/__init__.py +++ b/lappy/__init__.py @@ -52,7 +52,7 @@ class ndarray(Array): # noqa: N801 self._closure = compiler(self) print(self._closure.kernel) - # print(self._closure.data_map) + print(self._closure.data_map) evaluator = Executor(self._closure) res = evaluator(queue) diff --git a/lappy/core/array.py b/lappy/core/array.py index 8d69104..301cc84 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -181,6 +181,20 @@ class LazyObjectBase(object): self.namespace[name] = (lapject.as_stateless(), tags) + def _check_namespace_integrity(self, namespace=None, name=None): + """Check that all namespace keys equals the name of the contained + object. + """ + if namespace is None: + namespace = self.namespace + if name is None: + name = self.name + for key, (val, tag) in namespace.items(): + if val is None: + assert name == key + else: + assert val.name == key + def _meld_namespace(self, namespace, stateless_self=None): """Merge with another namespace. The other namespace may contain a self-reference (reference to the name containing the namespace). @@ -262,6 +276,12 @@ class LazyObjectBase(object): return self lapject = self.namespace[member_name][0].with_state( env=self.env.copy(), namespace=self.namespace.copy()) + lapject.namespace[self.name] = ( + self.as_stateless(), + self.namespace[self.name][1].copy()) + lapject.namespace[member_name] = ( + None, + self.namespace[member_name][1].copy()) return lapject def _make_default_name(self): @@ -501,6 +521,7 @@ class Array(LazyObjectBase): self.inames = kwargs.pop("inames", self._make_default_inames(ndim)) else: self.inames = () + assert isinstance(self.inames, tuple) if self.expr is None: self.expr = self._make_default_expr(ndim) @@ -879,26 +900,22 @@ class Array(LazyObjectBase): def __getitem__(self, indices): """Returns a new array by sub-indexing the current array. - - :param idx: a tuple of indices or slices. """ if isinstance(indices, str): # structured dtype support raise NotImplementedError() if not isinstance(indices, tuple): - # masking + # advanced indexing with an array assert isinstance(indices, Array) assert indices.ndim == self.ndim - from lappy.core.basic_ops import MaskedArrayFactory - arr_fac = MaskedArrayFactory(self, indices) + indices = (indices, ) else: # indexing / mixture of indexing and masking assert isinstance(indices, tuple) - # FIXME only if no mixed use of indexing and masking - from lappy.core.basic_ops import SubArrayFactory - arr_fac = SubArrayFactory(self, indices) + from lappy.core.basic_ops import SubArrayFactory + arr_fac = SubArrayFactory(self, indices) sub_arr = arr_fac() return sub_arr @@ -1381,15 +1398,40 @@ class Array(LazyObjectBase): # {{{ extended public api - def pin(self): + def pin(self, name=None, barrier=False): """Declare that the array shall be treated as a temporary. """ - self.namespace[self.name][1]['is_temporary'] = True + if name is None: + name = self.name + self.namespace[name][1]['is_temporary'] = True + + def pin_cse(self, name=None): + """Declare that the array shall be treated as a CSE. + """ + if name is None: + name = self.name + self.namespace[name][1]['is_cse'] = True # }}} End extended public api # {{{ private api + def _has_trivial_expr(self): + """Whether the expression is more complex than a leaf node. + """ + # An array + if isinstance(self.expr, Subscript): + if self.expr.aggregate.__class__.init_arg_names == ('name',): + for idx in self.expr.index_tuple: + if self.expr.aggregate.__class__.init_arg_names == ('name',): + pass + else: + return False + return True + + # A scalar + return self.expr.__class__.init_arg_names == ('name',) + def _get_shape_vals(self, axis=None): """Returns the shape values where available, None otherwise. """ diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 235270e..552fc54 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -21,14 +21,13 @@ THE SOFTWARE. """ import numpy as np from pymbolic import var, evaluate, substitute -from pymbolic.primitives import Variable, Product +from pymbolic.primitives import Variable, Product, Subscript from lappy.exceptions import AxisError from lappy.core.ufuncs import minimum, maximum from lappy.core.conditional import conditional from lappy.core.array import ( - Array, Int, Unsigned, to_lappy_shape, to_lappy_unsigned, - isscalar, get_internal_name_prefix) + Array, to_lappy_shape, to_lappy_unsigned, isscalar) from lappy.core.preconditions import ( EvaluationPrecondition, make_size_conservation_condition) @@ -90,18 +89,28 @@ class SubArrayFactory(object): .. attribute:: ndim ndim of the output array. + + .. attribute:: advanced_indexing + + list of bools, whether is using advanced indexing for each axis. + + .. attribute:: boolean_masking + + list of bools, whether is masked with a boolean array for each axis. + Meaningful only if advanced_indexing is True. """ # {{{ constructor def __init__(self, base, selobj=None): """ - :param base: the base array. - :param selobj: the selection object. + :param base_arr: the base array. + :param selectors: the selection object. """ assert isinstance(base, Array) self.base_arr = base + # normalize the selectors list if isinstance(selobj, tuple): selist = list(selobj) elif isinstance(selobj, (int, Array, list, np.ndarray)): @@ -126,10 +135,15 @@ class SubArrayFactory(object): n_ell += 1 self.selectors = selist - self.ndim = self._get_out_ndim() self._expand_ellipsis() + self.advanced_indexing = self._prepare_advanced_indexing() + assert len(self.advanced_indexing) == self.ndim + + self.boolean_masking = self._prepare_boolean_masking() + assert len(self.boolean_masking) == self.ndim + # }}} End constructor # {{{ consume the selection object @@ -160,14 +174,26 @@ class SubArrayFactory(object): return max(ndim, self.base_arr.ndim, n_annihil_dim) - def _prepare_index_list(self, selobj): - """Identify if the selection object, :var:`selobj`, is/contains a lazy - array. There are three cases being considered: - - (1) basic slicing and indexing - (3) integer-typed advanced indexing - (4) boolean-typed advanced indexing + def _prepare_advanced_indexing(self): + """Identify if the selection object is/contains advanced indexing + for each output axis. + """ + advanced = [False, ] * self.ndim + for isel, sel in enumerate(self.selectors): + if isinstance(sel, Array): + advanced[isel] = True + return advanced + + def _prepare_boolean_masking(self): + """Identify if the selection object is/contains advanced indexing + via boolean masking for each output axis. """ + bool_masked = [False, ] * self.ndim + for isel, sel in enumerate(self.selectors): + if isinstance(sel, Array): + if sel.dtype == bool or self.dtype == np.bool_: + bool_masked[isel] = True + return bool_masked def _complete_slice(self, slc, iaxis): """Fill in all information for a partially specified slice. @@ -219,8 +245,8 @@ class SubArrayFactory(object): # {{{ basic indexing - def _basic_getitem(self): - """When the selectors consist of: + def _basic_getitem(self, basic_selectors): + """Tthe basic_selectors must only consist of: slices, integers, Ellipsis, and np.newaxis (None). """ ca = 0 # current axis @@ -228,152 +254,115 @@ class SubArrayFactory(object): new_shape = [] new_inames = [] rhs_index_exprs = [] - for i_sel, sel in enumerate(self.selectors): + + new_namespace = self.base_arr.namespace.copy() + new_namespace[self.base_arr.name] = ( + self.base_arr.as_stateless(), + self.base_arr.namespace[self.base_arr.name][1].copy()) + + new_name = self.base_arr._make_default_name() + + for i_sel, sel in enumerate(basic_selectors): if sel is None: # newaxis does not advance the ca pointer new_shape.append(to_lappy_unsigned(1)) new_inames.append(var('__indexing_%s_newaxis_inames_%d' % (self.base_arr.name, i_newaxis))) i_newaxis += 1 + elif ca >= self.base_arr.ndim: raise IndexError('too many indices for array') + elif isinstance(sel, slice): - # TODO + se = self._complete_slice(sel, iaxis=ca) + + se_size = (se.stop - se.start + se.step - 1) // se.step + new_shape.append(se_size) + + iname = self.base_arr.inames[ca] + new_inames.append(iname) + + if isinstance(se.start, Array): + start = se.start.expr + new_namespace[se.start.name] = ( + se.start, + se.start.namespace[se.start.name][1].copy()) + else: + start = se.start + + if isinstance(se.step, Array): + step = se.step.expr + new_namespace[se.step.name] = ( + se.step, + se.step.namespace[se.step.name][1].copy()) + else: + step = se.step + + rhs_index_exprs.append( + start + step * iname) ca += 1 + elif isscalar(sel): - # TODO + new_shape.append(1) + iname = self.base_arr.inames[ca] + new_inames.append(iname) + rhs_index_exprs.append(sel) ca += 1 + else: raise TypeError('invalid index type: %s' - % type(self.selectors[i_sel])) - - new_name = self.base_arr._make_default_name() - - new_interm = self.base_arr.temporaries.copy() - new_interm[self.base_arr.name] = self.base_arr.as_stateless() + % type(basic_selectors[i_sel])) - new_expr = rhs_index_exprs # TODO + # NOTE: since complex expressions cannot be subscrpted, + # if the expression is non trivial, this will automatically + # add a temporary. + new_expr = Subscript( + var(self.base_arr.name), + index=tuple(rhs_index_exprs)) obj = { 'name': new_name, - 'inames': new_inames, + 'inames': tuple(new_inames), 'expr': new_expr, 'value': None, 'domain_expr': self.base_arr.domain_expr, - 'arguments': self.base_arr.arguments.copy(), - 'bound_arguments': self.base_arr.bound_arguments.copy(), 'env': self.base_arr.env.copy(), + 'namespace': new_namespace, 'preconditions': self.base_arr.preconditions, - 'temporaries': new_interm, 'ndim': self.ndim, 'shape': new_shape, 'dtype': self.base_arr.dtype, 'is_integral': self.base_arr.is_integral, 'integer_domain': self.base_arr.integer_domain, } - arr = Array(**obj) + arr = self.base_arr.__class__(**obj) - # remove self reference - if self.base_arr.name in arr.arguments: - arr.arguments[self.base_arr.name] = self.base_arr.as_stateless() - elif self.base_arr.name in arr.bound_arguments: - arr.bound_arguments[self.base_arr.name] = \ - self.base_arr.as_stateless() + if not self.base_arr._has_trivial_expr(): + arr.pin(self.base_arr.name) return arr # }}} End basic indexing - def __call__(self): - """Create the sub-array. - """ - # step 1: figure out the new inames for each axis - # - keep the old iname if indexed by ':', by a lazy boolean array, - # or is part of an ellipsis - # - make a new iname otherwise - new_inames = [] - for idx, iname in zip(self.indices, self.base_arr.inames): - if idx is None: - new_inames.append(iname) - elif isinstance(idx, Array): - assert idx.ndim - new_inames.append(iname) - - # step 2: figure out the new shape - # - for surviving old inames, shapes are unchanged - # - for new inames, shapes are calculated - # - # NOTE: if the index is a slice with variable step, say '0:k:n', - # then 'k' must be known at the time of code generation. - # Otherwise the new shape 'n // k' would not be an affine expression. - - # step 3: figure out the new expression - # substitute the inames of changed axes with the properly updated - # expressions. - for idx in self.indices: - if idx is None: - # newaxis - raise NotImplementedError() - elif isinstance(idx, Ellipsis): - # wildcard ellipsis - raise NotImplementedError() - elif isinstance(idx, int): - # numerical indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, Int): - # symbolic indexing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, slice): - # slicing - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - elif isinstance(idx, tuple): - # fancy indexing with static shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - for fc_id in idx: - if isinstance(fc_id, int): - # static - raise NotImplementedError() - else: - assert isinstance(fc_id, Unsigned) - # (semi-)dynamic - raise NotImplementedError() - elif isinstance(idx, Array): - # fancy indexing with dynamic shape - if self.ndim == 0: - raise TypeError("invalid index to scalar variable") - raise NotImplementedError() - else: - raise IndexError( - "only integers, slices (`:`), ellipses (`...`), " - "numpy.newaxis (`None`) and integer or boolean " - "arrays are valid indices") + # {{{ advanced indexing - name_prefix = get_internal_name_prefix(self.base_arr.name) - name = (name_prefix + self.base_arr.name - + '_subarray%d' % self.base_arr.__class__._counter) - sub_arr = self.base_arr.with_name(name, False) + def _advanced_getitem(self): + """Handle advanced indexing of two cases: - new_inames = sub_arr._make_default_inames() - new_shape = tuple(Unsigned() for iax in range(self.base_arr.ndim)) - iname_maps = {} - new_expr = substitute(self.base_arr.expr, iname_maps) + 1) array of size_t + 2) array of bool + """ + raise NotImplementedError() - sub_arr.inames = new_inames - sub_arr._shape = new_shape - sub_arr.expr = new_expr + # }}} End advanced indexing - # add base array as an intermediate object - sub_arr.temporaries[self.base_arr.name] = \ - self.base_arr.as_stateless() + def __call__(self): + """Create the sub-array. + """ + if any(self.advanced_indexing): + return self._advanced_getitem() - raise NotImplementedError() + return self._basic_getitem(self.selectors) # }}} End indexing @@ -531,12 +520,13 @@ def reshape(array, newshape, order='C', name=None, inames=None): new_arr._meld_namespace(s.namespace, s.as_stateless()) new_arr._meld_env(s.env) - if name != array.name: - # if the reference is no longer to self - if array.name in new_arr.arguments.keys(): - new_arr.arguments[array.name] = array.as_stateless() - elif array.name in new_arr.bound_arguments.keys(): - new_arr.bound_arguments[array.name] = array.as_stateless() + new_arr.namespace[array.name] = ( + array.as_stateless(), + array.namespace[array.name][1].copy()) + new_arr.namespace[name] = (None, {}) + + if not array._has_trivial_expr(): + new_arr.pin(array.name) new_arr.expr = substitute(array.expr, iname_subs) diff --git a/lappy/core/conditional.py b/lappy/core/conditional.py index afd690c..6c79e1c 100644 --- a/lappy/core/conditional.py +++ b/lappy/core/conditional.py @@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import numpy as np from pymbolic import var, substitute from pymbolic.primitives import If @@ -38,6 +39,13 @@ def conditional(cond, con, alt, name=None): Behaves like the ternary operator ?: in C. """ + # if cond is simply a known boolean, simplify the result + if isinstance(cond, (int, bool)): + if cond: + return con + else: + return alt + if not isinstance(cond, Array): cond = to_lappy_array(cond) @@ -63,18 +71,15 @@ def conditional(cond, con, alt, name=None): # expressions differ, the resulting domains are the same raise NotImplementedError() - # currently does not support dtype expressions if con.dtype is None: if alt.dtype is None: new_dtype = None else: new_dtype = alt.dtype else: - assert (alt.dtype is None) or (alt.dtype == con.dtype) - new_dtype = con.dtype - obj['dtype'] = new_dtype + new_dtype = np.result_type(con.dtype, alt.dtype).type - assert con.dtype == alt.dtype + obj['dtype'] = new_dtype arr_class = Array obj['ndim'] = con.ndim @@ -128,8 +133,13 @@ def conditional(cond, con, alt, name=None): if con.integer_domain is None and alt.integer_domain is None: obj['integer_domain'] = None else: - # FIXME: compute the intersection of integer_domain of con and alt - raise NotImplementedError() + if con.integer_domain is None: + obj['integer_domain'] = alt.integer_domain + elif alt.integer_domain is None: + obj['integer_domain'] = con.integer_domain + else: + obj['integer_domain'] = alt.integer_domain.union( + con.integer_domain) arr = arr_class(**obj) diff --git a/lappy/core/function_base.py b/lappy/core/function_base.py index c81687d..1946e8d 100644 --- a/lappy/core/function_base.py +++ b/lappy/core/function_base.py @@ -79,6 +79,7 @@ def arange(start, stop=None, step=1, dtype=None, obj['dtype'] = dtype arr = ndarray(**obj) + arr.namespace[arr.name][1]['is_argument'] = False iname = arr.inames[0] arr.expr = start.expr + step.expr * iname diff --git a/lappy/eval/compiler.py b/lappy/eval/compiler.py index 3418e62..0b50365 100644 --- a/lappy/eval/compiler.py +++ b/lappy/eval/compiler.py @@ -273,22 +273,35 @@ class Compiler(object): if 'substitutions' in kwargs: rhs_expr = substitute(rhs_expr, kwargs['substitutions']) - is_temporary = kwargs.get('is_temporary', False) + tc = TemporaryCollector(arr) + temps = tc(rhs_expr) + is_temporary = kwargs.pop('is_temporary', False) + + if arr.name in temps: + # ignore self when dealing with temps of a temp + temps.remove(arr.name) if is_temporary: assignments.append( - lp.Assignment(lhs_expr, rhs_expr), - temp_var_type=arr.dtype) + lp.Assignment( + lhs_expr, rhs_expr, + id=arr.name, # FIXME: naming and id in a diamond situation? + )) + if arr.namespace[arr.name][1].get('barrier', False): + assignments.append( + lp.BarrierInstruction(id=arr.name + '_wait', + depends_on=arr.name)) else: - assignments.append(lp.Assignment(lhs_expr, rhs_expr)) + assignments.append( + lp.Assignment(lhs_expr, rhs_expr,)) # recursively add assignments for all temps - tc = TemporaryCollector(arr) - temps = tc(rhs_expr) if len(temps) > 0: for tmp_name in temps: tmp = arr._extract(tmp_name) - assignments.extend(self._make_assignments(tmp, **kwargs)) + assignments.extend( + self._make_assignments( + tmp, is_temporary=True, **kwargs)) return assignments @@ -340,18 +353,11 @@ class Compiler(object): inline_scalars = True if inline_scalars: scalar_map = dict() - for vn, (vv, vtag) in arr.namespace.items(): - if vv is None: - continue - if vv.ndim == 0: - scalar_map[vn] = arr._get_value(vn) - scalar_map = self._purge_nones(scalar_map) - context.update(scalar_map) assignments = self._make_assignments(arr, substitutions=scalar_map) else: assignments = self._make_assignments(arr) - # add more kernel args (reduces a log of guessing time) + # add more kernel args (reduces a lot of guessing time) # e.g., link array shape vars with the arrays, dtypes, etc. # # (the trick is to add no more than actually needed) @@ -367,13 +373,13 @@ class Compiler(object): else: dtype = varr.dtype - if tag.get('is_argument'): + if tag.get('is_argument', False): kernel_data.append( lp.GlobalArg( arr_name, dtype=dtype, shape=varr._shape_str)) - elif tag.get('is_temporary'): + elif tag.get('is_temporary', False): kernel_data.append( lp.TemporaryVariable( arr_name, @@ -385,8 +391,12 @@ class Compiler(object): dtype = np.int32 if varr.is_integral else np.float64 else: dtype = varr.dtype - kernel_data.append( - lp.ValueArg(arr_name, dtype=dtype)) + if tag.get('is_argument', False): + if arr_name in scalar_map: + pass + else: + kernel_data.append( + lp.ValueArg(arr_name, dtype=dtype)) else: pass # warnings.warn( @@ -401,6 +411,16 @@ class Compiler(object): knl = lp.set_options(knl, return_dict=True) + if inline_scalars: + for vn, (vv, vtag) in arr.namespace.items(): + if vv is None: + continue + if vv.ndim == 0: + scalar_map[vn] = arr._get_value(vn) + scalar_map = self._purge_nones(scalar_map) + context.update(scalar_map) + knl = lp.fix_parameters(knl, **scalar_map) + # help with dtype inference extra_dtype_dict = {} for argname, arg in knl.arg_dict.items(): diff --git a/test/test_broadcast.py b/test/test_broadcast.py index 7996cfa..1c62aca 100644 --- a/test/test_broadcast.py +++ b/test/test_broadcast.py @@ -92,6 +92,8 @@ def test_symbolic_broadcast_rules(shapes): # shape and size are now expressions # evaluate those concretely with a sum + for lar in laarrs: + broadcast_ctx.update(lar.env) # check shape lappy_res_shape = [ -- GitLab From 5f07aa91ee29bd901144bd821d83472729057e44 Mon Sep 17 00:00:00 2001 From: xywei Date: Wed, 8 Apr 2020 21:13:15 -0500 Subject: [PATCH 59/59] Update readme --- README.rst | 5 +++++ lappy/core/array.py | 16 ++++++++-------- lappy/core/basic_ops.py | 6 ++++-- lappy/eval/compiler.py | 28 +++++++++++++++++++--------- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index e8c921c..e3314d3 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,11 @@ An array object has six basic components: the shape, the dtype, the expression, Scalars are special arrays with shape being an empty tuple. If the dtype is an integer type, there can be assumptions associated with the array elements represented by an integer set (for scalars) or map (for non-scalars). +Documentation +------------- + +Please visit `https://xiaoyu-wei.com/docs/lappy/`_ for the latest documentation. + Status ------ diff --git a/lappy/core/array.py b/lappy/core/array.py index 301cc84..08b0aa2 100644 --- a/lappy/core/array.py +++ b/lappy/core/array.py @@ -1398,19 +1398,19 @@ class Array(LazyObjectBase): # {{{ extended public api - def pin(self, name=None, barrier=False): + def pin(self, name=None, cse=False, insert_barrier=False): """Declare that the array shall be treated as a temporary. """ if name is None: name = self.name - self.namespace[name][1]['is_temporary'] = True - def pin_cse(self, name=None): - """Declare that the array shall be treated as a CSE. - """ - if name is None: - name = self.name - self.namespace[name][1]['is_cse'] = True + if cse: + self.namespace[name][1]['is_cse'] = True + + if insert_barrier: + self.namespace[name][1]['has_barrier'] = True + + self.namespace[name][1]['is_temporary'] = True # }}} End extended public api diff --git a/lappy/core/basic_ops.py b/lappy/core/basic_ops.py index 552fc54..4e4083b 100644 --- a/lappy/core/basic_ops.py +++ b/lappy/core/basic_ops.py @@ -338,7 +338,8 @@ class SubArrayFactory(object): arr = self.base_arr.__class__(**obj) if not self.base_arr._has_trivial_expr(): - arr.pin(self.base_arr.name) + # make previous results cse to ensure correct dependence + arr.pin(self.base_arr.name, cse=True) return arr @@ -526,7 +527,8 @@ def reshape(array, newshape, order='C', name=None, inames=None): new_arr.namespace[name] = (None, {}) if not array._has_trivial_expr(): - new_arr.pin(array.name) + # make previous results cse to ensure correct dependencies + new_arr.pin(array.name, cse=True) new_arr.expr = substitute(array.expr, iname_subs) diff --git a/lappy/eval/compiler.py b/lappy/eval/compiler.py index 0b50365..fe7d245 100644 --- a/lappy/eval/compiler.py +++ b/lappy/eval/compiler.py @@ -81,6 +81,7 @@ class TemporaryCollector(CombineMapper): class Compiler(object): def __init__(self, check_preconditions=False): self.check_preconditions = check_preconditions + self._counter = 0 def __call__(self, *args): """When given a few of :class:`Array` objects, make one loopy @@ -264,6 +265,10 @@ class Compiler(object): """ assignments = [] + # a counter for generating unique names in diamond cases + sfx = '_%d' % self._counter + self._counter += 1 + rhs_expr = arr.expr if arr.ndim == 0: lhs_expr = var(arr.name)[var(arr.name + '_dummy_iname')] @@ -275,7 +280,10 @@ class Compiler(object): tc = TemporaryCollector(arr) temps = tc(rhs_expr) - is_temporary = kwargs.pop('is_temporary', False) + + is_temporary = arr.namespace[arr.name][1].get('is_temporary', False) + is_cse = arr.namespace[arr.name][1].get('is_cse', False) + has_barrier = arr.namespace[arr.name][1].get('has_barrier', False) if arr.name in temps: # ignore self when dealing with temps of a temp @@ -285,12 +293,12 @@ class Compiler(object): assignments.append( lp.Assignment( lhs_expr, rhs_expr, - id=arr.name, # FIXME: naming and id in a diamond situation? + id=arr.name + sfx, )) - if arr.namespace[arr.name][1].get('barrier', False): - assignments.append( - lp.BarrierInstruction(id=arr.name + '_wait', - depends_on=arr.name)) + if is_cse: + raise NotImplementedError() + if has_barrier: + raise NotImplementedError() else: assignments.append( lp.Assignment(lhs_expr, rhs_expr,)) @@ -300,8 +308,7 @@ class Compiler(object): for tmp_name in temps: tmp = arr._extract(tmp_name) assignments.extend( - self._make_assignments( - tmp, is_temporary=True, **kwargs)) + self._make_assignments(tmp, **kwargs)) return assignments @@ -320,7 +327,10 @@ class Compiler(object): shape_dtype = kwargs.pop('shape_dtype', np.int32) - loop_domain = arr._generate_index_domains() + if arr.domain_expr is None: + loop_domain = arr._generate_index_domains() + else: + raise NotImplementedError() # collect kernel args kernel_data = [] -- GitLab