From b6d6ef374bb1432955b1760dd3fd806ddcdec716 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner <inform@tiker.net> Date: Tue, 6 Feb 2018 13:49:15 -0600 Subject: [PATCH] Add check for variable ordering and language versioning scheme --- doc/misc.rst | 4 +- doc/ref_creation.rst | 2 + examples/python/hello-loopy.py | 3 +- loopy/__init__.py | 4 ++ loopy/check.py | 115 +++++++++++++++++++++++++++++++++ loopy/kernel/creation.py | 35 ++++++++++ loopy/options.py | 11 ++++ loopy/version.py | 52 +++++++++++++++ test/test_dg.py | 3 +- test/test_loopy.py | 13 ++-- 10 files changed, 235 insertions(+), 7 deletions(-) diff --git a/doc/misc.rst b/doc/misc.rst index cd6fe102c..2c9c9a92b 100644 --- a/doc/misc.rst +++ b/doc/misc.rst @@ -90,7 +90,9 @@ regarding OpenCL drivers. User-visible Changes ==================== -Version 2017.2 +See also :ref:`language-versioning`. + +Version 2018.1 -------------- .. note:: diff --git a/doc/ref_creation.rst b/doc/ref_creation.rst index 92eff09c9..6b715033c 100644 --- a/doc/ref_creation.rst +++ b/doc/ref_creation.rst @@ -30,4 +30,6 @@ To Copy between Data Formats .. autofunction:: make_copy_kernel +.. automodule:: loopy.version + .. vim: tw=75:spell:fdm=marker diff --git a/examples/python/hello-loopy.py b/examples/python/hello-loopy.py index 7c5de5a1b..e7ab13c16 100644 --- a/examples/python/hello-loopy.py +++ b/examples/python/hello-loopy.py @@ -15,7 +15,8 @@ a = cl.array.arange(queue, n, dtype=np.float32) # ------ knl = lp.make_kernel( "{ [i]: 0<=i<n }", - "out[i] = 2*a[i]") + "out[i] = 2*a[i]", + lang_version=(2018, 1)) # transform # --------- diff --git a/loopy/__init__.py b/loopy/__init__.py index 5e8a3fb06..0f4697f92 100644 --- a/loopy/__init__.py +++ b/loopy/__init__.py @@ -65,6 +65,8 @@ from loopy.library.reduction import register_reduction_parser # {{{ import transforms +from loopy.version import VERSION, MOST_RECENT_LANGUAGE_VERSION + from loopy.transform.iname import ( set_loop_priority, prioritize_loops, split_iname, chunk_iname, join_inames, tag_inames, duplicate_inames, @@ -171,6 +173,8 @@ __all__ = [ "register_reduction_parser", + "VERSION", "MOST_RECENT_LANGUAGE_VERSION", + # {{{ transforms "set_loop_priority", "prioritize_loops", diff --git a/loopy/check.py b/loopy/check.py index 7e661b566..b4e117e25 100644 --- a/loopy/check.py +++ b/loopy/check.py @@ -379,6 +379,120 @@ def check_has_schedulable_iname_nesting(kernel): "to get hints about which iname to duplicate. Here are some " "options:\n%s" % opt_str) + +class IndirectDependencyEdgeFinder(object): + def __init__(self, kernel): + self.kernel = kernel + self.dep_edge_cache = {} + + def __call__(self, depender_id, dependee_id): + cache_key = (depender_id, dependee_id) + + try: + return self.dep_edge_cache[cache_key] + except KeyError: + pass + + depender = self.kernel.id_to_insn[depender_id] + + if dependee_id in depender.depends_on: + self.dep_edge_cache[cache_key] = True + return True + + for dep in depender.depends_on: + if self(dep, dependee_id): + self.dep_edge_cache[cache_key] = True + return True + + return False + + +def needs_no_sync_with(kernel, var_scope, dep_a_id, dep_b_id): + dep_a = kernel.id_to_insn[dep_a_id] + dep_b = kernel.id_to_insn[dep_b_id] + + from loopy.kernel.data import temp_var_scope + if var_scope == temp_var_scope.GLOBAL: + search_scopes = ["global", "any"] + elif var_scope == temp_var_scope.LOCAL: + search_scopes = ["local", "any"] + elif var_scope == temp_var_scope.PRIVATE: + search_scopes = ["any"] + else: + raise ValueError("unexpected value of 'temp_var_scope'") + + for scope in search_scopes: + if (dep_a_id, scope) in dep_b.no_sync_with: + return True + if (dep_b_id, scope) in dep_a.no_sync_with: + return True + + return False + + +def check_variable_access_ordered(kernel): + """Checks that all writes are ordered with respect to all other access to + the written variable. + """ + checked_variables = ( + kernel.get_written_variables() + | set(kernel.temporary_variables) + | set(arg for arg in kernel.arg_dict)) + + wmap = kernel.writer_map() + rmap = kernel.reader_map() + + from loopy.kernel.data import GlobalArg, ValueArg, temp_var_scope + + depfind = IndirectDependencyEdgeFinder(kernel) + + for name in checked_variables: + if name in kernel.temporary_variables: + scope = kernel.temporary_variables[name].scope + else: + arg = kernel.arg_dict[name] + if isinstance(arg, GlobalArg): + scope = temp_var_scope.GLOBAL + elif isinstance(arg, ValueArg): + scope = temp_var_scope.PRIVATE + else: + raise ValueError("could not determine scope of '%s'" % name) + + # Check even for PRIVATE scope, to ensure intentional program order. + + readers = rmap.get(name, set()) + writers = wmap.get(name, set()) + + for writer_id in writers: + for other_id in readers | writers: + if writer_id == other_id: + continue + + has_ordering_relationship = ( + needs_no_sync_with(kernel, scope, other_id, writer_id) + or + depfind(writer_id, other_id) + or + depfind(other_id, writer_id)) + + if not has_ordering_relationship: + msg = ("No ordering relationship found between " + "'{writer_id}' which writes '{var}' and " + "'{other_id}' which also accesses '{var}'. " + "Please either add a (possibly indirect) dependency " + "between the two, or add one to the other's no_sync set " + "to indicate that no ordering is intended." + .format( + writer_id=writer_id, + other_id=other_id, + var=name)) + if kernel.options.enforce_check_variable_access_ordered: + raise LoopyError(msg) + else: + from loopy.diagnostic import warn_with_kernel + warn_with_kernel( + kernel, "variable_access_ordered", msg) + # }}} @@ -397,6 +511,7 @@ def pre_schedule_checks(kernel): check_bounds(kernel) check_write_destinations(kernel) check_has_schedulable_iname_nesting(kernel) + check_variable_access_ordered(kernel) logger.debug("%s: pre-schedule check: done" % kernel.name) except KeyboardInterrupt: diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py index 4a08c28bd..15cb29c22 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -1909,6 +1909,23 @@ def make_kernel(domains, instructions, kernel_data=["..."], **kwargs): will be fixed to *value*. *name* may refer to :ref:`domain-parameters` or :ref:`arguments`. See also :func:`loopy.fix_parameters`. + :arg lang_version: The language version against which the kernel was + written, a tuple. To ensure future compatibility, copy the current value of + :data:`loopy.MOST_RECENT_LANGUAGE_VERSION` and pass that value. + + (If you just pass :data:`loopy.MOST_RECENT_LANGUAGE_VERSION` directly, + breaking language changes *will* apply to your kernel without asking, + likely breaking your code.) + + If not given, this value defaults to version **(2017, 2, 1)** and + a warning will be issued. + + See also :ref:`language-versioning`. + + .. versionchanged:: 2017.2.1 + + *lang_version* added. + .. versionchanged:: 2017.2 *fixed_parameters* added. @@ -1953,6 +1970,24 @@ def make_kernel(domains, instructions, kernel_data=["..."], **kwargs): from loopy.options import make_options options = make_options(options) + lang_version = kwargs.pop("lang_version", None) + if lang_version is None: + from warnings import warn + from loopy.diagnostic import LoopyWarning + from loopy.version import ( + MOST_RECENT_LANGUAGE_VERSION, + FALLBACK_LANGUAGE_VERSION) + warn("'lang_version' was not passed to make_kernel(). " + "To avoid this warning, pass " + "lang_version=%r in this invocation." + % (MOST_RECENT_LANGUAGE_VERSION,), + LoopyWarning, stacklevel=2) + + lang_version = FALLBACK_LANGUAGE_VERSION + + if lang_version >= (2018, 1): + options = options.copy(enforce_check_variable_access_ordered=True) + if isinstance(silenced_warnings, str): silenced_warnings = silenced_warnings.split(";") diff --git a/loopy/options.py b/loopy/options.py index 13d0b752d..4277d999a 100644 --- a/loopy/options.py +++ b/loopy/options.py @@ -162,6 +162,14 @@ class Options(ImmutableRecord): .. rubric:: Features .. attribute:: disable_global_barriers + + .. attribute:: enforce_check_variable_access_ordered + + If *True*, require that + :func:`loopy.check.check_variable_access_ordered` passes. + Required for language versions 2018.1 and above. This check + helps find and eliminate unintentionally unordered access + to variables. """ _legacy_options_map = { @@ -216,6 +224,9 @@ class Options(ImmutableRecord): disable_global_barriers=kwargs.get("disable_global_barriers", False), check_dep_resolution=kwargs.get("check_dep_resolution", True), + + enforce_check_variable_access_ordered=kwargs.get( + "enforce_check_variable_access_ordered", False), ) # {{{ legacy compatibility diff --git a/loopy/version.py b/loopy/version.py index 7141a6782..21c920ce4 100644 --- a/loopy/version.py +++ b/loopy/version.py @@ -33,3 +33,55 @@ else: _islpy_version = islpy.version.VERSION_TEXT DATA_MODEL_VERSION = "v76-islpy%s" % _islpy_version + + +FALLBACK_LANGUAGE_VERSION = (2017, 2, 1) +MOST_RECENT_LANGUAGE_VERSION = (2018, 1) + +__doc__ = """ + +.. currentmodule:: loopy +.. data:: VERSION + + A tuple representing the current version number of loopy, for example + **(2017, 2, 1)**. Direct comparison of these tuples will always yield + valid version comparisons. + +.. _language-versioning: + +Loopy Language Versioning +------------------------- + +At version 2018.1, :mod:`loopy` introduced a language versioning scheme to make +it easier to evolve the language while retaining backward compatibility. What +prompted this is the addition of +:attr:`loopy.Options.enforce_check_variable_access_ordered`, which (despite +its name) serves to enable a new check that helps ensure that all variable +access in a kernel is ordered as intended. Since that has the potential to +break existing programs, kernels now have to declare support for a given +language version to let them take advantage of this check. + +As a result, :mod:`loopy` will now issue a warning when a call to +:func:`loopy.make_kernel` does not declare a language version. Such kernels will +(indefinitely) default to language version 2017.2.1. + +Language versions will generally reflect the version number of :mod:`loopy` in +which they were introduced, though it is possible that some versions of +:mod:`loopy` do not introduce new user-visible language features. In such +situations, the previous language version number remains. + + +.. data:: MOST_RECENT_LANGUAGE_VERSION + + A tuple representing the most recent language version number of loopy, for + example **(2018, 1)**. Direct comparison of these tuples will always + yield valid version comparisons. + +History of Language Versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``(2018, 1)``: :attr:`loopy.Options.enforce_check_variable_access_ordered` + is turned on by default. + +* ``(2017, 2, 1)``: Initial legacy language version. +""" diff --git a/test/test_dg.py b/test/test_dg.py index d65c68ed4..ef4a31373 100644 --- a/test/test_dg.py +++ b/test/test_dg.py @@ -72,7 +72,8 @@ def test_dg_volume(ctx_factory): order=order), lp.ValueArg("K", np.int32, approximately=1000), ], - name="dg_volume", assumptions="K>=1") + name="dg_volume", assumptions="K>=1", + lang_version=(2018, 1)) knl = lp.fix_parameters(knl, Np=Np) diff --git a/test/test_loopy.py b/test/test_loopy.py index e36a4c2c3..02002c5cd 100644 --- a/test/test_loopy.py +++ b/test/test_loopy.py @@ -67,7 +67,8 @@ def test_globals_decl_once_with_multi_subprogram(ctx_factory): [lp.TemporaryVariable( 'cnst', shape=('n'), initializer=cnst, scope=lp.temp_var_scope.GLOBAL, - read_only=True), '...']) + read_only=True), '...'], + lang_version=(2018, 1)) knl = lp.fix_parameters(knl, n=16) knl = lp.add_barrier(knl, "id:first", "id:second") @@ -88,7 +89,8 @@ def test_complicated_subst(ctx_factory): h(x) := 1 + g(x) + 20*g$two(x) a[i] = h$one(i) * h$two(i) - """) + """, + lang_version=(2018, 1)) knl = lp.expand_subst(knl, "... > id:h and tag:two > id:g and tag:two") @@ -119,7 +121,8 @@ def test_type_inference_no_artificial_doubles(ctx_factory): lp.GlobalArg("c", np.float32, shape=("n",)), lp.ValueArg("n", np.int32), ], - assumptions="n>=1") + assumptions="n>=1", + lang_version=(2018, 1)) knl = lp.preprocess_kernel(knl, ctx.devices[0]) for k in lp.generate_loop_schedules(knl): @@ -139,7 +142,9 @@ def test_type_inference_with_type_dependencies(): c = b + c <>d = b + 2 + 1j """, - "...") + "...", + lang_version=(2018, 1)) + knl = lp.infer_unknown_types(knl) from loopy.types import to_loopy_type -- GitLab