From cb41ceef50302b7810bbf20bfaf67bdb0cce05a0 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 7 Mar 2021 18:03:39 -0600 Subject: [PATCH 01/17] Normalize Instruction.tags to use pytools.tag --- doc/ref_kernel.rst | 6 +++ loopy/__init__.py | 2 + loopy/kernel/creation.py | 40 ++++++++++++++++- loopy/kernel/instruction.py | 43 +++++++++++++++--- loopy/match.py | 13 +++++- loopy/target/ispc.py | 3 +- loopy/tools.py | 80 ++++++++++++++++++++++++++++++++++ loopy/transform/instruction.py | 5 ++- test/test_target.py | 2 +- 9 files changed, 183 insertions(+), 11 deletions(-) diff --git a/doc/ref_kernel.rst b/doc/ref_kernel.rst index f399d812e..922315685 100644 --- a/doc/ref_kernel.rst +++ b/doc/ref_kernel.rst @@ -493,6 +493,12 @@ Barrier Instructions .. autoclass:: BarrierInstruction +Instruction Tags +^^^^^^^^^^^^^^^^ + +.. autoclass:: LegacyStringInstructionTag +.. autoclass:: UseStreamingStoreTag + .. }}} Data: Arguments and Temporaries diff --git a/loopy/__init__.py b/loopy/__init__.py index 66ba75024..9c4bfa6d0 100644 --- a/loopy/__init__.py +++ b/loopy/__init__.py @@ -32,6 +32,7 @@ default_function_mangler, single_arg_function_mangler) from loopy.kernel.instruction import ( + LegacyStringInstructionTag, UseStreamingStoreTag, MemoryOrdering, memory_ordering, MemoryScope, memory_scope, VarAtomicity, OrderedAtomic, AtomicInit, AtomicUpdate, @@ -155,6 +156,7 @@ "LoopKernel", "KernelState", "kernel_state", # lower case is deprecated + "LegacyStringInstructionTag", "UseStreamingStoreTag", "MemoryOrdering", "memory_ordering", # lower case is deprecated "MemoryScope", "memory_scope", # lower case is deprecated diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py index 682bbc0fa..1588a6226 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -72,6 +72,38 @@ def __init__(self, name): # }}} +# {{{ tag normalization + +def _normalize_string_tag(tag): + from pytools.tag import Tag + + from loopy.kernel.instruction import ( + UseStreamingStoreTag, LegacyStringInstructionTag) + if tag == "!streaming_store": + return UseStreamingStoreTag() + else: + from loopy.tools import resolve_name + try: + tag_cls = resolve_name(tag) + except ImportError: + pass + except AttributeError: + pass + else: + if issubclass(tag_cls, Tag): + return tag_cls() + + return LegacyStringInstructionTag(tag) + + +def _normalize_tags(tags): + return frozenset( + _normalize_string_tag(t) if isinstance(t, str) else t + for t in tags) + +# }}} + + # {{{ expand defines WORD_RE = re.compile(r"\b([a-zA-Z0-9_]+)\b") @@ -328,9 +360,9 @@ def parse_nosync_option(opt_value): del new_predicates elif opt_key == "tags" and opt_value is not None: - result["tags"] = frozenset( + result["tags"] = _normalize_tags([ tag.strip() for tag in opt_value.split(":") - if tag.strip()) + if tag.strip()]) elif opt_key == "atomic": if is_with_block: @@ -803,6 +835,10 @@ def intern_if_str(s): | insn_options_stack[-1]["conflicts_with_groups"]), **kwargs) + norm_tags = _normalize_tags(insn.tags) + if norm_tags != insn.tags: + insn = insn.copy(tags=norm_tags) + new_instructions.append(insn) inames_to_dup.append([]) diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index ac992bc35..ae7d7922c 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -22,12 +22,45 @@ from sys import intern from pytools import ImmutableRecord, memoize_method +from pytools.tag import Tag, tag_dataclass from loopy.diagnostic import LoopyError from loopy.tools import Optional from warnings import warn import islpy as isl +# {{{ tags + +@tag_dataclass +class LegacyStringInstructionTag(Tag): + """A subclass of :class:`pytools.tag.Tag` for use in + :attr:`InstructionBase.tags` used for forward compatibility of the old + string-based tagging mechanism. String-based tags are automatically converted + to this type. + + .. attribute:: value + """ + value: str + + +@tag_dataclass +class UseStreamingStoreTag(Tag): + """A subclass of :class:`pytools.tag.Tag` for use in + :attr:`InstructionBase.tags` used to indicate that if the instruction is an + :class:`Assignment` or a :class:`CallInstruction`, then the 'store' part of + the assignment should be realized using streaming stores. + + .. warning:: + + This is a dodgy shortcut, and no promise is made that this will + continue to work. Whether this is safe is target-dependent and + program-dependent. No promise of safety is made. + """ + pass + +# }}} + + # {{{ instructions: base class class InstructionBase(ImmutableRecord): @@ -135,11 +168,11 @@ class InstructionBase(ImmutableRecord): .. attribute:: tags - A :class:`frozenset` of string identifiers that can be used to - identify groups of instructions. - - Tags starting with exclamation marks (``!``) are reserved and may have - specific meanings defined by :mod:`loopy` or its targets. + A :class:`frozenset` of subclasses of :class:`pytools.tag.Tag` used to + provide metadata on this object. Legacy string tags are converted to + :class:`LegacyStringInstructionTag` or, if they used to carry + a functional meaning, the tag carrying that same fucntional meaning + (e.g. :class:`UseStreamingStoreTag`). .. automethod:: __init__ .. automethod:: assignee_var_names diff --git a/loopy/match.py b/loopy/match.py index 625d98d4d..ce4e928ca 100644 --- a/loopy/match.py +++ b/loopy/match.py @@ -243,8 +243,19 @@ def __call__(self, kernel, matchable): class Tagged(GlobMatchExpressionBase): def __call__(self, kernel, matchable): + from loopy.kernel.instruction import LegacyStringInstructionTag if matchable.tags: - return any(self.re.match(tag) for tag in matchable.tags) + return any( + self.re.match(tag.value) + if isinstance(tag, LegacyStringInstructionTag) + else + + self.re.match(tag) if isinstance(tag, str) + else + + False + + for tag in matchable.tags) else: return False diff --git a/loopy/target/ispc.py b/loopy/target/ispc.py index 416160461..c1fadc55c 100644 --- a/loopy/target/ispc.py +++ b/loopy/target/ispc.py @@ -392,7 +392,8 @@ def emit_assignment(self, codegen_state, insn): # {{{ handle streaming stores - if "!streaming_store" in insn.tags: + from loopy.kernel.instruction import UseStreamingStoreTag + if UseStreamingStoreTag() in insn.tags: ary = ecm.find_array(lhs) from loopy.kernel.array import get_access_info diff --git a/loopy/tools.py b/loopy/tools.py index 4fd093153..0c46e2ae9 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -20,6 +20,8 @@ THE SOFTWARE. """ +import sys +import re import collections.abc as abc import numpy as np @@ -601,4 +603,82 @@ def is_interned(s): def intern_frozenset_of_ids(fs): return frozenset(intern(s) for s in fs) + +# {{{ resolve_name + +# https://github.com/python/cpython/commit/1ed61617a4a6632905ad6a0b440cd2cafb8b6414 + +_DOTTED_WORDS = r"[a-z_]\w*(\.[a-z_]\w*)*" +_NAME_PATTERN = re.compile(f"^({_DOTTED_WORDS})(:({_DOTTED_WORDS})?)?$", re.I) +del _DOTTED_WORDS + + +def resolve_name(name): + """ + Resolve a name to an object. + It is expected that `name` will be a string in one of the following + formats, where W is shorthand for a valid Python identifier and dot stands + for a literal period in these pseudo-regexes: + W(.W)* + W(.W)*:(W(.W)*)? + The first form is intended for backward compatibility only. It assumes that + some part of the dotted name is a package, and the rest is an object + somewhere within that package, possibly nested inside other objects. + Because the place where the package stops and the object hierarchy starts + can't be inferred by inspection, repeated attempts to import must be done + with this form. + In the second form, the caller makes the division point clear through the + provision of a single colon: the dotted name to the left of the colon is a + package to be imported, and the dotted name to the right is the object + hierarchy within that package. Only one import is needed in this form. If + it ends with the colon, then a module object is returned. + The function will return an object (which might be a module), or raise one + of the following exceptions: + ValueError - if `name` isn't in a recognised format + ImportError - if an import failed when it shouldn't have + AttributeError - if a failure occurred when traversing the object hierarchy + within the imported package to get to the desired object) + """ + # Delete this once we require Python 3.9 + if sys.version_info >= (3, 9): + # use the official version + import pkgutil + return pkgutil.resolve_name(name) + + import importlib + + m = _NAME_PATTERN.match(name) + if not m: + raise ValueError(f"invalid format: {name!r}") + groups = m.groups() + if groups[2]: + # there is a colon - a one-step import is all that's needed + mod = importlib.import_module(groups[0]) + parts = groups[3].split(".") if groups[3] else [] + else: + # no colon - have to iterate to find the package boundary + parts = name.split(".") + modname = parts.pop(0) + # first part *must* be a module/package. + mod = importlib.import_module(modname) + while parts: + p = parts[0] + s = f"{modname}.{p}" + try: + mod = importlib.import_module(s) + parts.pop(0) + modname = s + except ImportError: + break + # if we reach this point, mod is the module, already imported, and + # parts is the list of parts in the object hierarchy to be traversed, or + # an empty list if just the module is wanted. + result = mod + for p in parts: + result = getattr(result, p) + return result + +# }}} + + # vim: foldmethod=marker diff --git a/loopy/transform/instruction.py b/loopy/transform/instruction.py index 45ade0eca..055384ff1 100644 --- a/loopy/transform/instruction.py +++ b/loopy/transform/instruction.py @@ -209,11 +209,14 @@ def tag_instructions(kernel, new_tag, within=None): from loopy.match import parse_match within = parse_match(within) + from loopy.kernel.creation import _normalize_tags + new_tags = _normalize_tags([new_tag]) + new_insns = [] for insn in kernel.instructions: if within(kernel, insn): new_insns.append( - insn.copy(tags=insn.tags | frozenset([new_tag]))) + insn.copy(tags=insn.tags | new_tags)) else: new_insns.append(insn) diff --git a/test/test_target.py b/test/test_target.py index e18f9f191..e935e32ac 100644 --- a/test/test_target.py +++ b/test/test_target.py @@ -356,7 +356,7 @@ def test_ispc_streaming_stores(): knl = lp.preprocess_kernel(knl) knl = lp.get_one_scheduled_kernel(knl) - lp.generate_code_v2(knl).all_code() + assert "streaming_store(" in lp.generate_code_v2(knl).all_code() def test_cuda_short_vector(): From 9fe219f6beb3cd537d0bd2b750bcbfff29e50dec Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 7 Mar 2021 18:15:09 -0600 Subject: [PATCH 02/17] Silence pylint warning about non-existent pkgutil.resolve_name --- loopy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/tools.py b/loopy/tools.py index 0c46e2ae9..32b329704 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -643,7 +643,7 @@ def resolve_name(name): if sys.version_info >= (3, 9): # use the official version import pkgutil - return pkgutil.resolve_name(name) + return pkgutil.resolve_name(name) # pylint: disable=no-member import importlib From 93a64b42caa1adb4fec171c845fabe9e253ea005 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 7 Mar 2021 18:53:38 -0600 Subject: [PATCH 03/17] Fix stringification for typed InstructionBase.tags --- loopy/kernel/instruction.py | 4 +++- loopy/kernel/tools.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index ae7d7922c..d3ab776c7 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -380,7 +380,9 @@ def get_str_options(self): if self.priority: result.append("priority=%d" % self.priority) if self.tags: - result.append("tags=%s" % ":".join(self.tags)) + from loopy.kernel.tools import stringify_instruction_tag + result.append("tags=%s" % ":".join( + stringify_instruction_tag(t) for t in self.tags)) if hasattr(self, "atomicity") and self.atomicity: result.append("atomic=%s" % ":".join(str(a) for a in self.atomicity)) diff --git a/loopy/kernel/tools.py b/loopy/kernel/tools.py index 013c6c069..b5f8b5e9d 100644 --- a/loopy/kernel/tools.py +++ b/loopy/kernel/tools.py @@ -1415,6 +1415,14 @@ def conform_to_uniform_length(s): # {{{ stringify_instruction_list +def stringify_instruction_tag(tag): + from loopy.kernel.instruction import LegacyStringInstructionTag + if isinstance(tag, LegacyStringInstructionTag): + return f"S({tag.value})" + else: + return str(tag) + + def stringify_instruction_list(kernel): # {{{ topological sort @@ -1529,7 +1537,8 @@ def adapt_to_new_inames_list(new_inames): if insn.priority: options.append("priority=%d" % insn.priority) if insn.tags: - options.append("tags=%s" % ":".join(insn.tags)) + options.append("tags=%s" % ":".join( + stringify_instruction_tag(t) for t in insn.tags)) if isinstance(insn, lp.Assignment) and insn.atomicity: options.append("atomic=%s" % ":".join( str(a) for a in insn.atomicity)) From b4e9319dc281638a7d5294774b742096af393d5b Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 7 Mar 2021 18:54:25 -0600 Subject: [PATCH 04/17] Require pytools 2021.1.1 for pytools.tag support in persistent_dict --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fcf284bc8..3ef8677e1 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ def write_git_revision(package_name): python_requires="~=3.6", install_requires=[ - "pytools>=2021.1", + "pytools>=2021.1.1", "pymbolic>=2019.2", "genpy>=2016.1.2", "cgen>=2016.1", From 9529d6d5516d835b90e32b412091b557bdf4c2a4 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 7 Mar 2021 19:09:03 -0600 Subject: [PATCH 05/17] Placate flake8 --- loopy/kernel/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/kernel/tools.py b/loopy/kernel/tools.py index b5f8b5e9d..4791c4f75 100644 --- a/loopy/kernel/tools.py +++ b/loopy/kernel/tools.py @@ -1538,7 +1538,7 @@ def adapt_to_new_inames_list(new_inames): options.append("priority=%d" % insn.priority) if insn.tags: options.append("tags=%s" % ":".join( - stringify_instruction_tag(t) for t in insn.tags)) + stringify_instruction_tag(t) for t in insn.tags)) if isinstance(insn, lp.Assignment) and insn.atomicity: options.append("atomic=%s" % ":".join( str(a) for a in insn.atomicity)) From 11a5aa9623e81f3de553ff3cf863f490cff9b15d Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 8 Mar 2021 00:32:40 -0600 Subject: [PATCH 06/17] Move resolve_name to pytools --- loopy/kernel/creation.py | 2 +- loopy/tools.py | 79 ---------------------------------------- setup.py | 2 +- 3 files changed, 2 insertions(+), 81 deletions(-) diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py index 1588a6226..f4b3f673b 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -82,7 +82,7 @@ def _normalize_string_tag(tag): if tag == "!streaming_store": return UseStreamingStoreTag() else: - from loopy.tools import resolve_name + from pytools import resolve_name try: tag_cls = resolve_name(tag) except ImportError: diff --git a/loopy/tools.py b/loopy/tools.py index 32b329704..a626f3057 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -20,8 +20,6 @@ THE SOFTWARE. """ -import sys -import re import collections.abc as abc import numpy as np @@ -604,81 +602,4 @@ def intern_frozenset_of_ids(fs): return frozenset(intern(s) for s in fs) -# {{{ resolve_name - -# https://github.com/python/cpython/commit/1ed61617a4a6632905ad6a0b440cd2cafb8b6414 - -_DOTTED_WORDS = r"[a-z_]\w*(\.[a-z_]\w*)*" -_NAME_PATTERN = re.compile(f"^({_DOTTED_WORDS})(:({_DOTTED_WORDS})?)?$", re.I) -del _DOTTED_WORDS - - -def resolve_name(name): - """ - Resolve a name to an object. - It is expected that `name` will be a string in one of the following - formats, where W is shorthand for a valid Python identifier and dot stands - for a literal period in these pseudo-regexes: - W(.W)* - W(.W)*:(W(.W)*)? - The first form is intended for backward compatibility only. It assumes that - some part of the dotted name is a package, and the rest is an object - somewhere within that package, possibly nested inside other objects. - Because the place where the package stops and the object hierarchy starts - can't be inferred by inspection, repeated attempts to import must be done - with this form. - In the second form, the caller makes the division point clear through the - provision of a single colon: the dotted name to the left of the colon is a - package to be imported, and the dotted name to the right is the object - hierarchy within that package. Only one import is needed in this form. If - it ends with the colon, then a module object is returned. - The function will return an object (which might be a module), or raise one - of the following exceptions: - ValueError - if `name` isn't in a recognised format - ImportError - if an import failed when it shouldn't have - AttributeError - if a failure occurred when traversing the object hierarchy - within the imported package to get to the desired object) - """ - # Delete this once we require Python 3.9 - if sys.version_info >= (3, 9): - # use the official version - import pkgutil - return pkgutil.resolve_name(name) # pylint: disable=no-member - - import importlib - - m = _NAME_PATTERN.match(name) - if not m: - raise ValueError(f"invalid format: {name!r}") - groups = m.groups() - if groups[2]: - # there is a colon - a one-step import is all that's needed - mod = importlib.import_module(groups[0]) - parts = groups[3].split(".") if groups[3] else [] - else: - # no colon - have to iterate to find the package boundary - parts = name.split(".") - modname = parts.pop(0) - # first part *must* be a module/package. - mod = importlib.import_module(modname) - while parts: - p = parts[0] - s = f"{modname}.{p}" - try: - mod = importlib.import_module(s) - parts.pop(0) - modname = s - except ImportError: - break - # if we reach this point, mod is the module, already imported, and - # parts is the list of parts in the object hierarchy to be traversed, or - # an empty list if just the module is wanted. - result = mod - for p in parts: - result = getattr(result, p) - return result - -# }}} - - # vim: foldmethod=marker diff --git a/setup.py b/setup.py index 3ef8677e1..1d282cc11 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ def write_git_revision(package_name): python_requires="~=3.6", install_requires=[ - "pytools>=2021.1.1", + "pytools>=2021.1.2", "pymbolic>=2019.2", "genpy>=2016.1.2", "cgen>=2016.1", From 19c74e5a9deb2cbfe6e99126a020cf53508c09bc Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 8 Mar 2021 08:19:52 -0600 Subject: [PATCH 07/17] Add a note that UseStreamingStoreTag is advisory --- loopy/kernel/instruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index d3ab776c7..edfe2f18d 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -50,6 +50,12 @@ class UseStreamingStoreTag(Tag): :class:`Assignment` or a :class:`CallInstruction`, then the 'store' part of the assignment should be realized using streaming stores. + .. note:: + + This tag is advisory in nature and may be ignored by targets + that do not understand it or in situations where it does not + apply. + .. warning:: This is a dodgy shortcut, and no promise is made that this will From 3465c97caf9e3851d3cd401b065f0bee1d15ab53 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 8 Mar 2021 16:24:22 -0600 Subject: [PATCH 08/17] LoopyKeyBuilder.update_for_dict: don't sort, use unordered_hash instead (bump pytools req for that) --- loopy/tools.py | 11 ++++++++--- setup.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index a626f3057..2be041d00 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -64,9 +64,14 @@ class LoopyKeyBuilder(KeyBuilderBase): update_for_set = KeyBuilderBase.update_for_frozenset def update_for_dict(self, key_hash, key): - # Order matters for the hash--insert in sorted order. - for dict_key in sorted(key.keys()): - self.rec(key_hash, (dict_key, key[dict_key])) + from pytools import unordered_hash + + self.rec(key_hash, + unordered_hash( + self.new_hash, + (self.rec(self.new_hash(), (k, v)).digest() + for k, v in key.items()) + ).digest()) update_for_defaultdict = update_for_dict diff --git a/setup.py b/setup.py index 1d282cc11..57f5e895d 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ def write_git_revision(package_name): python_requires="~=3.6", install_requires=[ - "pytools>=2021.1.2", + "pytools>=2021.2", "pymbolic>=2019.2", "genpy>=2016.1.2", "cgen>=2016.1", From 035e318fdd15084143342171872bb6f8893c3cdd Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 8 Mar 2021 18:21:49 -0600 Subject: [PATCH 09/17] TaggedVariable: tag -> tags, normalize to pytools.tag --- loopy/match.py | 3 -- loopy/symbolic.py | 93 +++++++++++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/loopy/match.py b/loopy/match.py index ce4e928ca..48428636c 100644 --- a/loopy/match.py +++ b/loopy/match.py @@ -250,9 +250,6 @@ def __call__(self, kernel, matchable): if isinstance(tag, LegacyStringInstructionTag) else - self.re.match(tag) if isinstance(tag, str) - else - False for tag in matchable.tags) diff --git a/loopy/symbolic.py b/loopy/symbolic.py index 7faa28c00..6abdeb56d 100644 --- a/loopy/symbolic.py +++ b/loopy/symbolic.py @@ -237,7 +237,7 @@ def map_reduction(self, expr, prec): self.rec(expr.expr, PREC_NONE)) def map_tagged_variable(self, expr, prec): - return f"{expr.name}${expr.tag}" + return f"{expr.name}${{{', '.join(str(t) for t in expr.tags)}}}" def map_linear_subscript(self, expr, enclosing_prec): from pymbolic.mapper.stringifier import PREC_CALL, PREC_NONE @@ -311,7 +311,7 @@ def map_tagged_variable(self, expr, other, urecs): # Check if the variables match literally--that's ok, too. if (isinstance(other, TaggedVariable) and expr.name == other.name - and expr.tag == other.tag + and expr.tags == other.tags and expr.name not in self.lhs_mapping_candidates): return urecs else: @@ -560,16 +560,47 @@ class TaggedVariable(LoopyExpressionBase, p.Variable): 'one' identifies this specific use of the identifier. This mechanism may then be used to address these uses--such as by prefetching only accesses tagged a certain way. + + .. attribute:: tags + + A :class:`frozenset` of subclasses of :class:`pytools.tag.Tag` used to + provide metadata on this object. Legacy string tags are converted to + :class:`LegacyStringInstructionTag` or, if they used to carry + a functional meaning, the tag carrying that same fucntional meaning + (e.g. :class:`UseStreamingStoreTag`). + + Inherits from :class:`pymbolic.primitives.Variable`. """ - init_arg_names = ("name", "tag") + init_arg_names = ("name", "tags") - def __init__(self, name, tag): + def __init__(self, name, tags): super().__init__(name) - self.tag = tag + if isinstance(tags, str): + from loopy.kernel.creation import _normalize_string_tag + tags = frozenset({_normalize_string_tag(tags)}) + + assert isinstance(tags, frozenset) + assert tags + + self.tags = tags + + @property + def tag(self): + from warnings import warn + warn("Accessing TaggedVariable.tag is deprecated and will stop working " + "in 2022. Use TaggedVariable.tags instead.", DeprecationWarning, + stacklevel=2) + + if len(self.tags) != 1: + raise ValueError("cannot access TaggedVariable.tag: variable has " + f"{len(self.tags)} tags") + + tag, = self.tags + return tag def __getinitargs__(self): - return self.name, self.tag + return self.name, self.tags mapper_method = intern("map_tagged_variable") @@ -719,7 +750,7 @@ def get_dependencies(expr): def parse_tagged_name(expr): if isinstance(expr, TaggedVariable): - return expr.name, expr.tag + return expr.name, expr.tags elif isinstance(expr, p.Variable): return expr.name, None else: @@ -759,30 +790,30 @@ def map_call(self, expr): if not isinstance(expr.function, p.Variable): return IdentityMapper.map_call(self, expr) - name, tag = parse_tagged_name(expr.function) + name, tags = parse_tagged_name(expr.function) new_name = self.renames.get(name) if new_name is None: return IdentityMapper.map_call(self, expr) - if tag is None: - sym = p.Variable(new_name) + if tags: + sym = TaggedVariable(new_name, tags) else: - sym = TaggedVariable(new_name, tag) + sym = p.Variable(new_name) return type(expr)(sym, tuple(self.rec(child) for child in expr.parameters)) def map_variable(self, expr): - name, tag = parse_tagged_name(expr) + name, tags = parse_tagged_name(expr) new_name = self.renames.get(name) if new_name is None: return IdentityMapper.map_variable(self, expr) - if tag is None: - return p.Variable(new_name) + if tags: + return TaggedVariable(new_name, tags) else: - return TaggedVariable(new_name, tag) + return p.Variable(new_name) def rename_subst_rules_in_instructions(insns, renames): @@ -918,22 +949,22 @@ def __init__(self, rule_mapping_context): self.rule_mapping_context = rule_mapping_context def map_variable(self, expr, expn_state): - name, tag = parse_tagged_name(expr) + name, tags = parse_tagged_name(expr) if name not in self.rule_mapping_context.old_subst_rules: return IdentityMapper.map_variable(self, expr, expn_state) else: - return self.map_substitution(name, tag, (), expn_state) + return self.map_substitution(name, tags, (), expn_state) def map_call(self, expr, expn_state): if not isinstance(expr.function, p.Variable): return IdentityMapper.map_call(self, expr, expn_state) - name, tag = parse_tagged_name(expr.function) + name, tags = parse_tagged_name(expr.function) if name not in self.rule_mapping_context.old_subst_rules: return super().map_call(expr, expn_state) else: - return self.map_substitution(name, tag, self.rec( + return self.map_substitution(name, tags, self.rec( expr.parameters, expn_state), expn_state) @staticmethod @@ -948,16 +979,11 @@ def make_new_arg_context(rule_name, arg_names, arguments, arg_context): formal_arg_name: arg_subst_map(arg_value) for formal_arg_name, arg_value in zip(arg_names, arguments)} - def map_substitution(self, name, tag, arguments, expn_state): + def map_substitution(self, name, tags, arguments, expn_state): rule = self.rule_mapping_context.old_subst_rules[name] rec_arguments = self.rec(arguments, expn_state) - if tag is None: - tags = None - else: - tags = (tag,) - new_expn_state = expn_state.copy( stack=expn_state.stack + ((name, tags),), arg_context=self.make_new_arg_context( @@ -968,10 +994,10 @@ def map_substitution(self, name, tag, arguments, expn_state): new_name = self.rule_mapping_context.register_subst_rule( name, rule.arguments, result) - if tag is None: - sym = p.Variable(new_name) + if tags: + sym = TaggedVariable(new_name, tags) else: - sym = TaggedVariable(new_name, tag) + sym = p.Variable(new_name) if arguments: return sym(*rec_arguments) @@ -1091,12 +1117,7 @@ def __init__(self, rule_mapping_context, rules, within): self.rules = rules self.within = within - def map_substitution(self, name, tag, arguments, expn_state): - if tag is None: - tags = None - else: - tags = (tag,) - + def map_substitution(self, name, tags, arguments, expn_state): new_stack = expn_state.stack + ((name, tags),) if self.within(expn_state.kernel, expn_state.instruction, new_stack): @@ -1120,7 +1141,7 @@ def map_substitution(self, name, tag, arguments, expn_state): else: # do not expand return super().map_substitution( - name, tag, arguments, expn_state) + name, tags, arguments, expn_state) # }}} @@ -1890,7 +1911,7 @@ def map_variable(self, expr): def map_tagged_variable(self, expr): if expr.name in self.which_vars: - return TaggedVariable(expr.name+"'", expr.tag) + return TaggedVariable(expr.name+"'", expr.tags) else: return expr From 251b1c9bcc029729c69bd81267bc2589d6692899 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 10 Mar 2021 19:13:10 -0600 Subject: [PATCH 10/17] Update LoopyKeyBuilder.update_for_dict for hash-passing interface of unordered_hash --- loopy/tools.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index 2be041d00..5be4ca6b5 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -65,13 +65,10 @@ class LoopyKeyBuilder(KeyBuilderBase): def update_for_dict(self, key_hash, key): from pytools import unordered_hash - - self.rec(key_hash, - unordered_hash( - self.new_hash, - (self.rec(self.new_hash(), (k, v)).digest() - for k, v in key.items()) - ).digest()) + unordered_hash( + key_hash, + (self.rec(self.new_hash(), (k, v)).digest() + for k, v in key.items())) update_for_defaultdict = update_for_dict From 63813b834f1aed903d1e38c88d88313049db0d9e Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 10 Mar 2021 19:29:46 -0600 Subject: [PATCH 11/17] Qualify tag example references in TaggedVariable docs --- loopy/symbolic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loopy/symbolic.py b/loopy/symbolic.py index 6abdeb56d..422f731fa 100644 --- a/loopy/symbolic.py +++ b/loopy/symbolic.py @@ -565,9 +565,9 @@ class TaggedVariable(LoopyExpressionBase, p.Variable): A :class:`frozenset` of subclasses of :class:`pytools.tag.Tag` used to provide metadata on this object. Legacy string tags are converted to - :class:`LegacyStringInstructionTag` or, if they used to carry + :class:`~loopy.LegacyStringInstructionTag` or, if they used to carry a functional meaning, the tag carrying that same fucntional meaning - (e.g. :class:`UseStreamingStoreTag`). + (e.g. :class:`~loopy.UseStreamingStoreTag`). Inherits from :class:`pymbolic.primitives.Variable`. """ From 9a4d2e51aa0757d0203c2191ee23fbc52d111dc8 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 00:06:40 -0600 Subject: [PATCH 12/17] Improve 'instruction tags' section label --- loopy/kernel/instruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index edfe2f18d..d3e0ea4eb 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -29,7 +29,7 @@ import islpy as isl -# {{{ tags +# {{{ instruction tags @tag_dataclass class LegacyStringInstructionTag(Tag): From 369079ce395875a40afa57b6ccbba3b36b509526 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 00:07:02 -0600 Subject: [PATCH 13/17] Fix handling of tagged variables in C codegen --- loopy/target/c/codegen/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/target/c/codegen/expression.py b/loopy/target/c/codegen/expression.py index 9ec99c784..d5666d34c 100644 --- a/loopy/target/c/codegen/expression.py +++ b/loopy/target/c/codegen/expression.py @@ -179,7 +179,7 @@ def base_impl(expr, type_context): def make_var(name): from loopy import TaggedVariable if isinstance(expr.aggregate, TaggedVariable): - return TaggedVariable(name, expr.aggregate.tag) + return TaggedVariable(name, expr.aggregate.tags) else: return var(name) From a70bf4f9f3357b08261069ce83cf0abce1918505 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 00:07:18 -0600 Subject: [PATCH 14/17] Fix handling of tags in add_prefetch --- loopy/transform/data.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/loopy/transform/data.py b/loopy/transform/data.py index 2319ea635..4851ffdec 100644 --- a/loopy/transform/data.py +++ b/loopy/transform/data.py @@ -258,21 +258,24 @@ def add_prefetch(kernel, var_name, sweep_inames=[], dim_arg_names=None, # }}} - # {{{ fish out tag + # {{{ fish out tags from loopy.symbolic import TaggedVariable if isinstance(parsed_var_name, TaggedVariable): var_name = parsed_var_name.name - tag = parsed_var_name.tag + tags = parsed_var_name.tags else: var_name = parsed_var_name.name - tag = None + tags = () # }}} c_name = var_name - if tag is not None: - c_name = c_name + "_" + tag + from loopy.kernel.instruction import LegacyStringInstructionTag + tag_suffix = "_".join(tag.value for tag in tags + if isinstance(tag, LegacyStringInstructionTag)) + if tag_suffix: + c_name = c_name + "_" + tag_suffix var_name_gen = kernel.get_var_name_generator() From cc93aafd3875b0a620b9ffcaeab7c3091ee6e0bf Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 00:29:07 -0600 Subject: [PATCH 15/17] Refactor statistics to allow pytools variable tags --- loopy/statistics.py | 56 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/loopy/statistics.py b/loopy/statistics.py index 7e9bb36b5..0192aa27d 100755 --- a/loopy/statistics.py +++ b/loopy/statistics.py @@ -589,10 +589,11 @@ class MemAccess(Record): A :class:`str` that specifies the variable name of the data accessed. - .. attribute:: variable_tag + .. attribute:: variable_tags - A :class:`str` that specifies the variable tag of a - :class:`loopy.symbolic.TaggedVariable`. + A :class:`frozenset` of subclasses of :class:`~pytools.tag.Tag` + that reflects :attr:`~loopy.symbolic.TaggedVariable.tags` of + an accessed variable. .. attribute:: count_granularity @@ -610,7 +611,8 @@ class MemAccess(Record): """ def __init__(self, mtype=None, dtype=None, lid_strides=None, gid_strides=None, - direction=None, variable=None, variable_tag=None, + direction=None, variable=None, + *, variable_tags=None, variable_tag=None, count_granularity=None): if count_granularity not in CountGranularity.ALL+[None]: @@ -618,19 +620,51 @@ def __init__(self, mtype=None, dtype=None, lid_strides=None, gid_strides=None, "not allowed. count_granularity options: %s" % (count_granularity, CountGranularity.ALL+[None])) + # {{{ normalize variable_tags + + if variable_tags is not None and variable_tag is not None: + raise TypeError( + "may not specify both 'variable_tags' and 'variable_tag'") + if variable_tag is not None: + from loopy.kernel.creation import _normalize_string_tag + variable_tags = frozenset({_normalize_string_tag(variable_tag)}) + + from warnings import warn + warn("Passing 'variable_tag' to MemAccess is deprecated and will " + "stop working in 2022. Pass variable_tags instead.") + + if variable_tags is None: + variable_tags = frozenset() + + # }}} + if dtype is None: Record.__init__(self, mtype=mtype, dtype=dtype, lid_strides=lid_strides, gid_strides=gid_strides, direction=direction, - variable=variable, variable_tag=variable_tag, + variable=variable, variable_tags=variable_tags, count_granularity=count_granularity) else: from loopy.types import to_loopy_type Record.__init__(self, mtype=mtype, dtype=to_loopy_type(dtype), lid_strides=lid_strides, gid_strides=gid_strides, direction=direction, variable=variable, - variable_tag=variable_tag, + variable_tags=variable_tags, count_granularity=count_granularity) + @property + def variable_tag(self): + from warnings import warn + warn("Accessing MemAccess.variable_tag is deprecated and will stop working " + "in 2022. Use MemAccess.variable_tags instead.", DeprecationWarning, + stacklevel=2) + + if len(self.variable_tags) != 1: + raise ValueError("cannot access MemAccess.variable_tag: access has " + f"{len(self.variable_tags)} tags") + + tag, = self.variable_tags + return tag + def __hash__(self): # Note that this means lid_strides and gid_strides must be sorted # in self.__repr__() @@ -647,7 +681,7 @@ def __repr__(self): sorted(self.gid_strides.items())), self.direction, self.variable, - self.variable_tag, + self.variable_tags, self.count_granularity) # }}} @@ -1031,9 +1065,9 @@ def map_variable(self, expr): def map_subscript(self, expr): name = expr.aggregate.name try: - var_tag = expr.aggregate.tag + var_tags = expr.aggregate.tags except AttributeError: - var_tag = None + var_tags = frozenset() if name in self.knl.arg_dict: array = self.knl.arg_dict[name] @@ -1062,7 +1096,7 @@ def map_subscript(self, expr): lid_strides=dict(sorted(lid_strides.items())), gid_strides=dict(sorted(gid_strides.items())), variable=name, - variable_tag=var_tag, + variable_tags=var_tags, count_granularity=count_granularity ): 1} ) + self.rec(expr.index_tuple) @@ -1678,7 +1712,7 @@ def get_mem_access_map(knl, numpy_types=True, count_redundant_work=False, gid_strides=mem_access.gid_strides, direction=mem_access.direction, variable=mem_access.variable, - variable_tag=mem_access.variable_tag, + variable_tags=mem_access.variable_tags, count_granularity=mem_access.count_granularity): ct for mem_access, ct in access_map.count_map.items()}, From 2f34e4fcd10ff40800a5c6b3cfa2f8513d521854 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 18:47:27 -0600 Subject: [PATCH 16/17] InstructionBase and TaggedVariable: inherit from Taggable --- loopy/frontend/fortran/translator.py | 9 +++++---- loopy/kernel/instruction.py | 11 +++++++++-- loopy/symbolic.py | 12 +++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/loopy/frontend/fortran/translator.py b/loopy/frontend/fortran/translator.py index ff357ae8a..78eddfb54 100644 --- a/loopy/frontend/fortran/translator.py +++ b/loopy/frontend/fortran/translator.py @@ -34,6 +34,7 @@ from islpy import dim_type from loopy.symbolic import IdentityMapper from loopy.diagnostic import LoopyError +from loopy.kernel.instruction import LegacyStringInstructionTag from pymbolic.primitives import Wildcard @@ -640,16 +641,16 @@ def map_Comment(self, node): stripped_comment_line) if begin_tag_match: - tag = begin_tag_match.group(1) + tag = LegacyStringInstructionTag(begin_tag_match.group(1)) if tag in self.instruction_tags: - raise TranslationError("nested begin tag for tag '%s'" % tag) + raise TranslationError(f"nested begin tag for tag '{tag.value}'") self.instruction_tags.append(tag) elif end_tag_match: - tag = end_tag_match.group(1) + tag = LegacyStringInstructionTag(end_tag_match.group(1)) if tag not in self.instruction_tags: raise TranslationError( - "end tag without begin tag for tag '%s'" % tag) + f"end tag without begin tag for tag '{tag.value}'") self.instruction_tags.remove(tag) elif faulty_loopy_pragma_match is not None: diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index d3e0ea4eb..c80fdbedd 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -22,7 +22,7 @@ from sys import intern from pytools import ImmutableRecord, memoize_method -from pytools.tag import Tag, tag_dataclass +from pytools.tag import Tag, tag_dataclass, Taggable from loopy.diagnostic import LoopyError from loopy.tools import Optional from warnings import warn @@ -69,7 +69,7 @@ class UseStreamingStoreTag(Tag): # {{{ instructions: base class -class InstructionBase(ImmutableRecord): +class InstructionBase(ImmutableRecord, Taggable): """A base class for all types of instruction that can occur in a kernel. @@ -187,6 +187,8 @@ class InstructionBase(ImmutableRecord): .. automethod:: write_dependency_names .. automethod:: dependency_names .. automethod:: copy + + Inherits from :class:`pytools.tag.Taggable`. """ # within_inames_is_final is deprecated and will be removed in version 2017.x. @@ -296,8 +298,13 @@ def __init__(self, id, depends_on, depends_on_is_final, within_inames=within_inames, priority=priority, predicates=predicates, + # Yes, tags is set by both this and the Taggable constructor. + # Here, we set it so that ImmutableRecord knows about it. + # The Taggable constructor call does extra validation. tags=tags) + Taggable.__init__(self, tags) + # {{{ abstract interface def read_dependency_names(self): diff --git a/loopy/symbolic.py b/loopy/symbolic.py index 2b3631754..29c99b2bf 100644 --- a/loopy/symbolic.py +++ b/loopy/symbolic.py @@ -29,6 +29,7 @@ from pytools import memoize, memoize_method, ImmutableRecord import pytools.lex +from pytools.tag import Taggable import pymbolic.primitives as p @@ -555,8 +556,8 @@ def __getinitargs__(self): mapper_method = intern("map_type_cast") -class TaggedVariable(LoopyExpressionBase, p.Variable): - """This is an identifier with a tag, such as 'matrix$one', where +class TaggedVariable(LoopyExpressionBase, p.Variable, Taggable): + """This is an identifier with tags, such as ``matrix$one``, where 'one' identifies this specific use of the identifier. This mechanism may then be used to address these uses--such as by prefetching only accesses tagged a certain way. @@ -569,13 +570,14 @@ class TaggedVariable(LoopyExpressionBase, p.Variable): a functional meaning, the tag carrying that same fucntional meaning (e.g. :class:`~loopy.UseStreamingStoreTag`). - Inherits from :class:`pymbolic.primitives.Variable`. + Inherits from :class:`pymbolic.primitives.Variable` + and :class:`pytools.tag.Taggable`. """ init_arg_names = ("name", "tags") def __init__(self, name, tags): - super().__init__(name) + p.Variable.__init__(self, name) if isinstance(tags, str): from loopy.kernel.creation import _normalize_string_tag tags = frozenset({_normalize_string_tag(tags)}) @@ -583,7 +585,7 @@ def __init__(self, name, tags): assert isinstance(tags, frozenset) assert tags - self.tags = tags + Taggable.__init__(self, tags) @property def tag(self): From 09bf5f384a08c4a20d70fd699c1a451e56fbfb03 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 11 Mar 2021 22:40:36 -0600 Subject: [PATCH 17/17] Add a comment about deprecating LegacyStringInstructionTag --- loopy/kernel/instruction.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index c80fdbedd..81b174653 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -42,6 +42,11 @@ class LegacyStringInstructionTag(Tag): """ value: str + # FIXME: This class should be deprecated as soon as there is a viable + # alternative. For now, pattern matching and the textual syntax are + # only able to generate string tags, which is why the deprecation is not + # yet in effect. + @tag_dataclass class UseStreamingStoreTag(Tag):