diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..b8bbe44e766fc18bc633642d13f647ad6d23bc98 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +Python 3 POCL: + script: + - export PY_EXE=python3 + - export PYOPENCL_TEST=portable + - export EXTRA_INSTALL="pybind11 numpy mako" + - export LOOPY_NO_CACHE=1 + - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project.sh + - ". ./build-and-test-py-project.sh" + tags: + - python3 + - pocl + except: + - tags + artifacts: + reports: + junit: test/pytest.xml + +Pylint: + script: + - export PY_EXE=python3 + - EXTRA_INSTALL="pybind11 numpy mako" + - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-pylint.sh + - ". ./prepare-and-run-pylint.sh arrayzy test/test_*.py" + tags: + - python3 + except: + - tags + +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 arrayzy test" + tags: + - python3 + except: + - tags diff --git a/arrayzy/__init__.py b/arrayzy/__init__.py index a2831ea9c15fab158fe91932104cf01d711ac85e..9d6dae8be39aa272e87cfd7c4963614a69c60516 100644 --- a/arrayzy/__init__.py +++ b/arrayzy/__init__.py @@ -1 +1 @@ -from arrayzy.array import Array, Context, make_context +from arrayzy.array import Array, Context, make_context, make_sym # noqa diff --git a/arrayzy/array.py b/arrayzy/array.py index a365f311780bfe1662076a511f05a4a7b0575c75..4dd29851a853bf302dc2776f88021cbdd374e105 100644 --- a/arrayzy/array.py +++ b/arrayzy/array.py @@ -20,8 +20,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import islpy as isl import loopy as lp +import numpy as np + +import pymbolic import pymbolic.primitives as prim +from pytools import memoize_method class Context: @@ -44,7 +49,8 @@ class Context: *program* may not define any names starting with underscores. """ - def __init__(self, program, bindings, target): + def __init__(self, queue, program, bindings, target): + self.queue = queue self._program = program self.bindings = bindings self.target = target @@ -53,28 +59,39 @@ class Context: # update_program. @property def program(self): - self.program + return self._program + + def _infer_type(self, expr): + """Infer the type of an expression in the kernel being built.""" + from loopy.type_inference import TypeInferenceMapper + mapper = TypeInferenceMapper(self._program) + return mapper(expr) + + @property + def parameters(self): + pass def update_program(self, program): - self.program = program + self._program = program def get_parameter(self, name): - if name in self.program.all_variable_names(): - if name not in self.program.all_params(): - # FIXME: ... data dependent control flow? - raise ValueError( - f"'{name}' is not a domain parameter " - "in this context") - - else: - return prim.Variable(name) - else: + if name not in self.program.all_variable_names(): self.update_program( self.program.copy( - arguments=self.program.args + [ + args=self.program.args + [ lp.ValueArg(name, dtype=self.program.index_dtype) ])) + """ + if name not in self.program.all_params(): + # FIXME: ... data dependent control flow? + raise ValueError( + f"'{name}' is not a domain parameter " + "in this context") + """ + + return prim.Variable(name) + class Target: pass @@ -104,9 +121,9 @@ def make_context(arg): raise ValueError(f"invalid argument type: {type(arg).__name__}") import loopy as lp - program = lp.make_kernel("{[]:}", [], target=target.get_loopy_target()) + program = lp.make_kernel("{:}", [], target=target.get_loopy_target()) - return Context(program, {}, target) + return Context(arg, program, {}, target) class Array: @@ -121,8 +138,11 @@ class Array: An instance of :class:`loopy.types.LoopyType` or *None* to indicate that the type of the array is not yet known. - .. attribute:: expression + .. attribute:: is_materialized + + Whether this array is backed by actual storage. + .. attribute:: expression """ def __init__(self, context, shape, dtype, expression): @@ -135,34 +155,123 @@ class Array: pass def eval(self, **kwargs): + _, (out,) = self._knl()(self.context.queue, **kwargs) + return out + + @property + def _dim_names(self): + return tuple(f"_{i}" for i in range(len(self.shape))) + + def __getitem__(self, indices): + # TODO pass + @property + def T(self): # noqa + index_map = dict( + zip(self._dim_names, + map(prim.Variable, reversed(self._dim_names)))) + + expression = pymbolic.substitute(self.expression, index_map) + shape = tuple(reversed(self.shape)) + + return Array(self.context, shape, self.dtype, expression) + + def __mul__(self, other): + if not np.isscalar(other): + raise TypeError("only scalar multiplication supported") + new_expr = other * self.expression + return Array( + self.context, + self.shape, + self.context._infer_type(new_expr), + new_expr) + + __rmul__ = __mul__ + + @property + def ndim(self): + return len(self.shape) + + @memoize_method + def _knl(self): + knl = self.context.program + + out = lp.GlobalArg("_out", shape=self.shape, dtype=self.dtype, + order="C") + + # FIXME: Won't work for scalars. + out_inames = [f"_out_{i}" for i in range(self.ndim)] + + out_expr = pymbolic.substitute( + self.expression, + dict(zip(self._dim_names, map(prim.Variable, out_inames)))) + + # Build output domain. + params = [] + from loopy.symbolic import get_dependencies + for sdep in map(get_dependencies, self.shape): + for dep in sdep: + params.append(self.context.get_parameter(dep).name) + + dom = isl.BasicSet.universe( + isl.Space.create_from_names( + isl.DEFAULT_CONTEXT, + set=out_inames, + params=params)) + + from loopy.symbolic import aff_from_expr + affs = isl.affs_from_space(dom.space) + for iname, expr in zip(out_inames, self.shape): + dom &= affs[0].le_set(affs[iname]) + dom &= affs[iname].lt_set(aff_from_expr(dom.space, expr)) + dom, = dom.get_basic_sets() + + # Build output instruction. + from loopy.kernel.instruction import make_assignment + out_insn = make_assignment( + ( + prim.Variable("_out")[ + tuple(map(prim.Variable, out_inames))],), + out_expr, + id="_out", + within_inames=frozenset(out_inames)) + + return knl.copy( + domains=knl.domains + [dom], + instructions=knl.instructions + [out_insn], + args=knl.args + [out]) + def store(self, prefix="tmp"): + """Stores the array in a temporary.""" pass -def make_sym(context, name, shape, dtype=None): +def make_sym(context, name, shape, dtype=None, order="C"): if name in context.program.all_variable_names(): raise ValueError(f"name '{name}' already in use in context") - arg = lp.ArrayArg(name, shape=shape, dtype=dtype) - from loopy.symbolic import get_dependencies - for sdep in get_dependencies(si for si in arg.shape): - context.get_parameter(sdep) + arg = lp.GlobalArg(name, shape=shape, dtype=dtype, order=order) - context.update_program(context.program.copy( - arguments=context.program.arguments + [arg])) + shape = arg.shape + dtype = arg.dtype - # TODO make sure "name" is not taken + from loopy.symbolic import get_dependencies + for sdep in map(get_dependencies, arg.shape): + for dep in sdep: + context.get_parameter(dep) + + context.update_program( + context.program.copy( + args=context.program.args + [arg])) v_name = prim.Variable(name) - subscripts = tuple(prim.Variable(???) for i in range(len(shape))) + subscripts = tuple(prim.Variable(f"_{i}") for i in range(len(shape))) - return Array(context, expression=v_name[subscripts]) + return Array(context, shape, dtype, expression=v_name[subscripts]) def zeros(context, shape, dtype): pass - # vim: foldmethod=marker diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..7819c268b5156721d1621831b28d28570611ea8c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +git+https://github.com/inducer/pyopencl.git diff --git a/test/test_array.py b/test/test_array.py new file mode 100644 index 0000000000000000000000000000000000000000..14090f481cb99ebef8b6a3949319a9ba6aa9bff8 --- /dev/null +++ b/test/test_array.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +__copyright__ = "Copyright (C) 2020 Matt Wala" + +__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 arrayzy as az + +import numpy as np +import sys + +import pytest # noqa + +import pyopencl as cl +import pyopencl.array as cl_array # noqa +import pyopencl.cltypes as cltypes # noqa +import pyopencl.tools as cl_tools # noqa +from pyopencl.tools import ( # noqa + pytest_generate_tests_for_pyopencl as pytest_generate_tests) + + +def test_symbolic_array(ctx_getter): + cl_ctx = ctx_getter() + queue = cl.CommandQueue(cl_ctx) + ctx = az.make_context(queue) + x = az.make_sym(ctx, "x", shape="n", dtype=np.float64) + x_in = cl.array.to_device(queue, np.array([1., 2., 3., 4., 5.])) + assert (x.eval(x=x_in).get() == x_in.get()).all() + + +@pytest.mark.parametrize("dim", (1, 2, 3)) +def test_transpose(ctx_getter, dim): + cl_ctx = ctx_getter() + queue = cl.CommandQueue(cl_ctx) + ctx = az.make_context(queue) + shape = ("l", "m", "n")[-dim:] + x = az.make_sym(ctx, "x", shape=shape, dtype=np.float64) + x_in = cl.array.to_device( + queue, + np.arange(24, dtype=float).reshape((2, 3, -1)[-dim:])) + assert (x.T.eval(x=x_in).get() == x_in.get().T).all() + + +def test_scalar_multiply(ctx_getter): + cl_ctx = ctx_getter() + queue = cl.CommandQueue(cl_ctx) + ctx = az.make_context(queue) + x = az.make_sym(ctx, "x", shape="n", dtype=np.float64) + x_in = np.array([1., 2., 3., 4., 5.]) + x_in = cl.array.to_device(queue, np.array([1., 2., 3., 4., 5.])) + assert ((2*x).eval(x=x_in).get() == (2*x_in).get()).all() + + +if __name__ == "__main__": + # make sure that import failures get reported, instead of skipping the + # tests. + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + from pytest import main + main([__file__]) + +# vim: filetype=pyopencl:fdm=marker