# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2017-2026, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors: R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
# Modified by I. Kavcic and L. Turner, Met Office
# Modified by C.M. Maynard, Met Office / University of Reading
# Modified by J. Henrichs, Bureau of Meteorology
# -----------------------------------------------------------------------------
''' This module provides generic support for PSyclone's PSy code optimisation
and generation. The classes in this method need to be specialised for a
particular API and implementation. '''
from __future__ import annotations
from dataclasses import dataclass
import inspect
import os
from collections import OrderedDict
import abc
from typing import Any, Dict, Optional, Union
import warnings
try:
from sphinx.util.typing import stringify_annotation
except ImportError:
# No Sphinx available so use our own, simpler version.
from psyclone.utils import stringify_annotation
from psyclone.configuration import Config, LFRIC_API_NAMES, GOCEAN_API_NAMES
from psyclone.core import AccessType
from psyclone.errors import GenerationError, InternalError, FieldNotFoundError
from psyclone.parse.algorithm import BuiltInCall, KernelCall
from psyclone.psyir.backend.fortran import FortranWriter
from psyclone.psyir.nodes import (
ArrayReference, Call, Container, Literal, Loop, Node, OMPDoDirective,
Reference, Directive, Routine, Schedule, Statement, Assignment,
IntrinsicCall, BinaryOperation, FileContainer)
from psyclone.psyir.symbols import (
ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, ScalarType,
UnresolvedType, ImportInterface, RoutineSymbol)
from psyclone.psyir.symbols.symbol_table import SymbolTable
# The types of 'intent' that an argument to a Fortran subroutine
# may have
FORTRAN_INTENT_NAMES = ["inout", "out", "in"]
def object_index(alist, item):
'''
A version of the `list.index()` method that checks object identity
rather that the content of the object.
TODO this is a workaround for the fact that fparser2 overrides the
comparison operator for all nodes in the parse tree. See fparser
issue 174.
:param alist: single object or list of objects to search.
:type alist: list or :py:class:`fparser.two.utils.Base`
:param obj item: object to search for in the list.
:returns: index of the item in the list.
:rtype: int
:raises ValueError: if object is not in the list.
'''
if item is None:
raise InternalError("Cannot search for None item in list.")
for idx, entry in enumerate(alist):
if entry is item:
return idx
raise ValueError(f"Item '{item}' not found in list: {alist}")
def args_filter(arg_list, arg_types=None, arg_accesses=None, arg_meshes=None):
'''
Return all arguments in the supplied list that are of type
arg_types and with access in arg_accesses. If these are not set
then return all arguments.
:param arg_list: list of kernel arguments to filter.
:type arg_list: list of :py:class:`psyclone.parse.kernel.Descriptor`
:param arg_types: list of argument types (e.g. "GH_FIELD").
:type arg_types: list of str
:param arg_accesses: list of access types that arguments must have.
:type arg_accesses: list of \
:py:class:`psyclone.core.access_type.AccessType`
:param arg_meshes: list of meshes that arguments must be on.
:type arg_meshes: list of str
:returns: list of kernel arguments matching the requirements.
:rtype: list of :py:class:`psyclone.parse.kernel.Descriptor`
'''
arguments = []
for argument in arg_list:
if arg_types:
if argument.argument_type.lower() not in arg_types:
continue
if arg_accesses:
if argument.access not in arg_accesses:
continue
if arg_meshes:
if argument.mesh not in arg_meshes:
continue
arguments.append(argument)
return arguments
[docs]
class PSyFactory():
'''
Creates a specific version of the PSy.
:param str api: name of the PSyclone API (domain) for which to create \
a factory.
:param bool distributed_memory: whether or not the PSy object created \
will include support for distributed-memory parallelism.
:raises TypeError: if the distributed_memory argument is not a bool.
'''
def __init__(self, api="", distributed_memory=None):
if distributed_memory is None:
_distributed_memory = Config.get().distributed_memory
else:
_distributed_memory = distributed_memory
if _distributed_memory not in [True, False]:
raise TypeError(
"The distributed_memory flag in PSyFactory must be set to"
" 'True' or 'False'")
Config.get().api = api
Config.get().distributed_memory = _distributed_memory
self._type = api
[docs]
def create(self, invoke_info):
'''
Create the API-specific PSy instance.
:param invoke_info: information on the invoke()s found by parsing
the Algorithm layer.
:type invoke_info: :py:class:`psyclone.parse.algorithm.FileInfo`
:returns: an instance of the API-specific sub-class of PSy.
:rtype: subclass of :py:class:`psyclone.psyGen.PSy`
:raises InternalError: if this factory is found to have an
unsupported type (API).
'''
# Conditional run-time importing is a part of this factory
# implementation.
# pylint: disable=import-outside-toplevel
if self._type in LFRIC_API_NAMES:
from psyclone.domain.lfric import LFRicPSy as PSyClass
elif self._type in GOCEAN_API_NAMES:
from psyclone.gocean1p0 import GOPSy as PSyClass
else:
raise InternalError(
f"PSyFactory: Unsupported API type '{self._type}' found. "
f"Expected one of {Config.get().supported_apis}.")
return PSyClass(invoke_info)
[docs]
class PSy():
'''
Base class to help manage and generate PSy code for a single
algorithm file. Takes the invocation information output from the
function :func:`parse.algorithm.parse` as its input and stores this in a
way suitable for optimisation and code generation.
:param FileInfo invoke_info: An object containing the required \
invocation information for code \
optimisation and generation. Produced \
by the function :func:`parse.algorithm.parse`.
:type invoke_info: :py:class:`psyclone.parse.algorithm.FileInfo`
For example:
>>> from psyclone.parse.algorithm import parse
>>> ast, info = parse("argspec.F90")
>>> from psyclone.psyGen import PSyFactory
>>> api = "..."
>>> psy = PSyFactory(api).create(info)
>>> print(psy.gen)
'''
def __init__(self, invoke_info):
self._name = invoke_info.name
self._invokes = None
# Create an empty PSy layer PSyIR file with a Container (module) inside
# TODO 1010: Alternatively the PSy object could be a Container itself
module = Container(self.name)
FileContainer(self.name, children=[module])
self._container = module
@property
def container(self):
'''
:returns: the container associated with this PSy object
:rtype: :py:class:`psyclone.psyir.nodes.Container`
'''
return self._container
@property
def invokes(self):
''':returns: the list of invokes.
:rtype: :py:class:`psyclone.psyGen.Invokes` or derived class
'''
return self._invokes
@property
def name(self):
''':returns: the name of the PSy object.
:rtype: str
'''
return "psy_"+self._name
@property
def gen(self) -> str:
'''
Generate PSy-layer code associated with this PSy object.
Note that the necessary PSy-layer symbols are added to the temporary
copy of the tree before code-generation is begun. Since this tree
copy is discarded at the end of this routine, those symbols will
still not be present in the original tree.
:returns: the generated Fortran source.
'''
# Before the backend we need to add the Invoke initialisations and
# declarations, this modifies the PSyIR tree, so we operate on a
# copy of the tree.
original_container = self.container
new_container = self.container.copy()
self._container = new_container
# We need to update the internal reference to the Schedule, this could
# be improved by making all PSy-layer PSyIR DSL nodes, instead of using
# PSY->Invokes->Invoke->InvokeSchedule classes
for invsch in self.container.walk(InvokeSchedule):
invsch.invoke.schedule = invsch
# Now do the declarations/initialisation on the copied tree
for invoke in self.invokes.invoke_list:
invoke.setup_psy_layer_symbols()
# Use the PSyIR Fortran backend to generate Fortran code of the
# supplied PSyIR tree.
fortran_writer = FortranWriter(
check_global_constraints=Config.get().backend_checks_enabled,
disable_copy=True) # We already made the copy manually above
result = fortran_writer(new_container)
# Restore original container (see comment above)
self._container = original_container
for invsch in self.container.walk(InvokeSchedule):
invsch.invoke.schedule = invsch
return result
[docs]
class Invokes():
'''Manage the invoke calls.
:param alg_calls: a list of invoke metadata extracted by the \
parser.
:type alg_calls: list of \
:py:class:`psyclone.parse.algorithm.InvokeCall`
:param invoke_cls: an api-specific Invoke class.
:type invoke_cls: subclass of :py:class:`psyclone.psyGen.Invoke`
:param psy: the PSy instance containing this Invokes instance.
:type psy: subclass of :py:class`psyclone.psyGen.PSy`
'''
def __init__(self, alg_calls, invoke_cls, psy):
self._psy = psy
self.invoke_map = {}
self.invoke_list = []
for idx, alg_invocation in enumerate(alg_calls):
my_invoke = invoke_cls(alg_invocation, idx, self)
self.invoke_map[my_invoke.name] = my_invoke
self.invoke_list.append(my_invoke)
@property
def psy(self):
'''
:returns: the PSy instance that contains this instance.
:rtype: subclass of :py:class:`psyclone.psyGen.PSy`
'''
return self._psy
@property
def names(self):
return self.invoke_map.keys()
[docs]
def get(self, invoke_name):
'''
Gets the Invoke with the supplied name. If the name does not already
begin with ``invoke_`` then a new name with this prepended is included
in the search if no exact match is found initially.
:param str invoke_name: the name of the Invoke to get (not case-
sensitive).
:returns: the invoke with the specified name.
:rtype: :py:class:`psyclone.psyGen.Invoke`
:raises RuntimeError: if no Invoke with the supplied name (with or
without ``invoke_`` prepended) exists.
'''
search_names = [invoke_name.lower()]
if not search_names[0].startswith("invoke_"):
search_names.append("invoke_"+search_names[0])
for name in search_names:
try:
return self.invoke_map[name]
except KeyError:
pass
search_list = " or ".join(f"'{name}'" for name in search_names)
raise RuntimeError(f"Cannot find an invoke named {search_list} "
f"in {list(self.names)}")
[docs]
class Invoke():
r'''Manage an individual invoke call.
:param alg_invocation: metadata from the parsed code capturing \
information for this Invoke instance.
:type alg_invocation: :py:class:`psyclone.parse.algorithm.InvokeCall`
:param int idx: position/index of this invoke call in the subroutine. \
If not None, this number is added to the name ("invoke\_").
:param schedule_class: the schedule class to create for this invoke.
:type schedule_class: :py:class:`psyclone.psyGen.InvokeSchedule`
:param invokes: the Invokes instance that contains this Invoke \
instance.
:type invokes: :py:class:`psyclone.psyGen.Invokes`
'''
def __init__(self, alg_invocation, idx, schedule_class, invokes):
'''Construct an invoke object.'''
self._invokes = invokes
self._name = "invoke"
self._alg_unique_args = []
if alg_invocation is None and idx is None:
return
# create a name for the call if one does not already exist
if alg_invocation.name is not None:
# In Python2 unicode strings must be converted to str()
self._name = str(alg_invocation.name)
elif len(alg_invocation.kcalls) == 1 and \
alg_invocation.kcalls[0].type == "kernelCall":
# use the name of the kernel call with the position appended.
# Appended position is needed in case we have two separate invokes
# in the same algorithm code containing the same (single) kernel
self._name = "invoke_" + str(idx) + "_" + \
alg_invocation.kcalls[0].ktype.name
else:
# use the position of the invoke
self._name = "invoke_" + str(idx)
# Get a reference to the parent container, if any
container = None
if self.invokes:
container = self.invokes.psy.container
# create the schedule (Routine sub-class). Routines use a
# symbol input argument.
schedule_symbol = RoutineSymbol(self._name)
self._schedule = schedule_class(schedule_symbol,
alg_invocation.kcalls,
parent=container)
# Add the new Schedule to the top-level PSy Container
if container:
container.addchild(self._schedule)
# let the schedule have access to me
self._schedule.invoke = self
# extract the argument list for the algorithm call and psy
# layer subroutine.
self._alg_unique_args = []
self._psy_unique_vars = []
tmp_arg_names = []
for call in self.schedule.kernels():
for arg in call.arguments.args:
if arg.text is not None:
if arg.text not in self._alg_unique_args:
self._alg_unique_args.append(arg.text)
if arg.name not in tmp_arg_names:
tmp_arg_names.append(arg.name)
self._psy_unique_vars.append(arg)
else:
# literals have no name
pass
def __str__(self):
return self._name+"("+", ".join([str(arg) for arg in
self._alg_unique_args])+")"
@property
def invokes(self):
'''
:returns: the Invokes instance that contains this instance.
:rtype: :py:class`psyclone.psyGen.Invokes`
'''
return self._invokes
[docs]
def setup_psy_layer_symbols(self):
''' Declare, initialise and deallocate all symbols required by the
PSy-layer Invoke subroutine.
By default does nothing - PSyKAL DSLs can specialise this method.
Currently this is done at "lowering", but we could move it to psy-layer
creation time to have the symbols available in the transformation
scripts.
'''
@property
def name(self):
return self._name
@property
def alg_unique_args(self):
return self._alg_unique_args
@property
def psy_unique_vars(self):
return self._psy_unique_vars
@property
def schedule(self):
return self._schedule
@schedule.setter
def schedule(self, obj):
self._schedule = obj
[docs]
def unique_declarations(self, argument_types, access=None,
intrinsic_type=None):
'''
Returns a list of all required declarations for the specified
API argument types. If access is supplied (e.g. "write") then
only declarations with that access are returned. If an intrinsic
type is supplied then only declarations with that intrinsic type
are returned.
:param argument_types: the types of the kernel argument for the \
particular API.
:type argument_types: list of str
:param access: optional AccessType that the declaration should have.
:type access: :py:class:`psyclone.core.access_type.AccessType`
:param intrinsic_type: optional intrinsic type of argument data.
:type intrinsic_type: str
:returns: a list of all declared kernel arguments.
:rtype: list of :py:class:`psyclone.psyGen.KernelArgument`
:raises InternalError: if at least one kernel argument type is \
not valid for the particular API.
:raises InternalError: if an invalid access is specified.
:raises InternalError: if an invalid intrinsic type is specified.
'''
# First check for invalid argument types, access and intrinsic type
const = Config.get().api_conf().get_constants()
if any(argtype not in const.VALID_ARG_TYPE_NAMES for
argtype in argument_types):
raise InternalError(
f"Invoke.unique_declarations() called with at least one "
f"invalid argument type. Expected one of "
f"{const.VALID_ARG_TYPE_NAMES} but found {argument_types}.")
if access and not isinstance(access, AccessType):
raise InternalError(
f"Invoke.unique_declarations() called with an invalid "
f"access type. Type is '{access}' instead of AccessType.")
if (intrinsic_type and intrinsic_type not in
const.VALID_INTRINSIC_TYPES):
raise InternalError(
f"Invoke.unique_declarations() called with an invalid "
f"intrinsic argument data type. Expected one of "
f"{const.VALID_INTRINSIC_TYPES} but found '{intrinsic_type}'.")
# Initialise dictionary of kernel arguments to get the
# argument list from
declarations = OrderedDict()
# Find unique kernel arguments using their declaration names
for call in self.schedule.kernels():
for arg in call.arguments.args:
if not intrinsic_type or arg.intrinsic_type == intrinsic_type:
if not access or arg.access == access:
if arg.text is not None:
if arg.argument_type in argument_types:
test_name = arg.declaration_name
if test_name not in declarations:
declarations[test_name] = arg
return list(declarations.values())
[docs]
def first_access(self, arg_name):
''' Returns the first argument with the specified name passed to
a kernel in our schedule '''
for call in self.schedule.kernels():
for arg in call.arguments.args:
if arg.text is not None:
if arg.declaration_name == arg_name:
return arg
raise GenerationError(f"Failed to find any kernel argument with name "
f"'{arg_name}'")
[docs]
def unique_declns_by_intent(self, argument_types, intrinsic_type=None):
'''
Returns a dictionary listing all required declarations for each
type of intent ('inout', 'out' and 'in').
:param argument_types: the types of the kernel argument for the \
particular API for which the intent is required.
:type argument_types: list of str
:param intrinsic_type: optional intrinsic type of argument data.
:type intrinsic_type: str
:returns: dictionary containing 'intent' keys holding the kernel \
arguments as values for each type of intent.
:rtype: dict of :py:class:`psyclone.psyGen.KernelArgument`
:raises InternalError: if at least one kernel argument type is \
not valid for the particular API.
:raises InternalError: if an invalid intrinsic type is specified.
'''
# First check for invalid argument types and intrinsic type
const = Config.get().api_conf().get_constants()
if any(argtype not in const.VALID_ARG_TYPE_NAMES for
argtype in argument_types):
raise InternalError(
f"Invoke.unique_declns_by_intent() called with at least one "
f"invalid argument type. Expected one of "
f"{const.VALID_ARG_TYPE_NAMES} but found {argument_types}.")
if (intrinsic_type and intrinsic_type not in
const.VALID_INTRINSIC_TYPES):
raise InternalError(
f"Invoke.unique_declns_by_intent() called with an invalid "
f"intrinsic argument data type. Expected one of "
f"{const.VALID_INTRINSIC_TYPES} but found '{intrinsic_type}'.")
# We will return a dictionary containing as many lists
# as there are types of intent
declns = {}
for intent in FORTRAN_INTENT_NAMES:
declns[intent] = []
for arg in self.unique_declarations(argument_types,
intrinsic_type=intrinsic_type):
first_arg = self.first_access(arg.declaration_name)
if first_arg.access in [AccessType.WRITE, AccessType.REDUCTION]:
# If the first access is a write then the intent is
# out irrespective of any other accesses. Note,
# sum_args behave as if they are write_args from the
# PSy-layer's perspective.
declns["out"].append(arg)
continue
# if all accesses are read, then the intent is in,
# otherwise the intent is inout (as we have already
# dealt with intent out).
read_only = True
for call in self.schedule.kernels():
for tmp_arg in call.arguments.args:
if tmp_arg.text is not None and \
tmp_arg.declaration_name == arg.declaration_name:
if tmp_arg.access != AccessType.READ:
# readwrite_args behave in the
# same way as inc_args from the
# perspective of intents
read_only = False
break
if not read_only:
break
if read_only:
declns["in"].append(arg)
else:
declns["inout"].append(arg)
return declns
[docs]
class InvokeSchedule(Routine):
'''
Stores schedule information for an invocation call. Schedules can be
optimised using transformations.
>>> from psyclone.parse.algorithm import parse
>>> ast, info = parse("algorithm.f90")
>>> from psyclone.psyGen import PSyFactory
>>> api = "..."
>>> psy = PSyFactory(api).create(info)
>>> invokes = psy.invokes
>>> invokes.names
>>> invoke = invokes.get("name")
>>> schedule = invoke.schedule
>>> print(schedule.view())
:param symbol: RoutineSymbol representing the invoke.
:type symbol: :py:class:`psyclone.psyir.symbols.RoutineSymbol`
:param type KernFactory: class instance of the factory to use when \
creating Kernels. e.g. \
:py:class:`psyclone.domain.lfric.LFRicKernCallFactory`.
:param type BuiltInFactory: class instance of the factory to use when \
creating built-ins. e.g. \
:py:class:`psyclone.domain.lfric.lfric_builtins.LFRicBuiltInCallFactory`.
:param alg_calls: list of Kernel calls in the schedule.
:type alg_calls: list of :py:class:`psyclone.parse.algorithm.KernelCall`
:param kwargs: additional keyword arguments provided to the super class.
:type kwargs: unwrapped dict.
'''
# Textual description of the node.
_text_name = "InvokeSchedule"
def __init__(self, symbol, KernFactory, BuiltInFactory, alg_calls=None,
**kwargs):
super().__init__(symbol, **kwargs)
self._invoke = None
# We need to separate calls into loops (an iteration space really)
# and calls so that we can perform optimisations separately on the
# two entities.
if alg_calls is None:
alg_calls = []
for call in alg_calls:
if isinstance(call, BuiltInCall):
self.addchild(BuiltInFactory.create(call, parent=self))
else:
self.addchild(KernFactory.create(call, parent=self))
@property
def invoke(self):
return self._invoke
@invoke.setter
def invoke(self, my_invoke):
self._invoke = my_invoke
[docs]
def node_str(self, colour=True):
'''
Returns the name of this node with appropriate control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
return f"{self.coloured_name(colour)}[invoke='{self.name}']"
def __str__(self):
result = self.coloured_name(False) + ":\n"
for entity in self._children:
result += str(entity) + "\n"
result += "End " + self.coloured_name(False) + "\n"
return result
[docs]
class HaloExchange(Statement):
'''
Generic Halo Exchange class which can be added to and
manipulated in, a schedule.
:param field: the field that this halo exchange will act on
:type field: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param check_dirty: optional argument default True indicating whether \
this halo exchange should be subject to a run-time \
check for clean/dirty halos.
:type check_dirty: bool
:param vector_index: optional vector index (default None) to identify \
which index of a vector field this halo exchange is \
responsible for.
:type vector_index: int
:param parent: optional parent (default None) of this object
:type parent: :py:class:`psyclone.psyir.nodes.Node`
'''
# Textual description of the node.
_children_valid_format = "<LeafNode>"
_text_name = "HaloExchange"
_colour = "blue"
def __init__(self, field, check_dirty=True,
vector_index=None, parent=None):
Node.__init__(self, children=[], parent=parent)
import copy
self._field = copy.copy(field)
if field:
# Update fields values appropriately
# Here "readwrite" denotes how the class HaloExchange
# accesses a field rather than the field's continuity
self._field.access = AccessType.READWRITE
self._field.call = self
self._halo_type = None
self._halo_depth = None
self._check_dirty = check_dirty
self._vector_index = vector_index
@property
def vector_index(self):
'''If the field is a vector then return the vector index associated
with this halo exchange. Otherwise return None'''
return self._vector_index
@property
def halo_depth(self):
''' Return the depth of the halo exchange '''
return self._halo_depth
@halo_depth.setter
def halo_depth(self, value):
''' Set the depth of the halo exchange '''
self._halo_depth = value
@property
def field(self):
''' Return the field that the halo exchange acts on '''
return self._field
@property
def dag_name(self):
'''
:returns: the name to use in a dag for this node.
:rtype: str
'''
name = f"{self._text_name}({self._field.name})_{self.position}"
if self._check_dirty:
name = "check" + name
return name
@property
def args(self):
'''Return the list of arguments associated with this node. Override the
base method and simply return our argument. '''
return [self._field]
[docs]
def node_str(self, colour=True):
'''
Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
return (f"{self.coloured_name(colour)}[field='{self._field.name}', "
f"type='{self._halo_type}', depth={self._halo_depth}, "
f"check_dirty={self._check_dirty}]")
[docs]
class Kern(Statement):
'''Base class representing a call to a sub-program unit from within the
PSy layer. It is possible for this unit to be in-lined within the
PSy layer.
:param parent: parent of this node in the PSyIR.
:type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`
:param call: information on the call itself, as obtained by parsing \
the Algorithm layer code.
:type call: :py:class:`psyclone.parse.algorithm.KernelCall`
:param str name: the name of the routine being called.
:param ArgumentsClass: class to create the object that holds all \
information on the kernel arguments, as extracted from kernel \
meta-data (and accessible here via call.ktype).
:type ArgumentsClass: type of :py:class:`psyclone.psyGen.Arguments`
:param bool check: whether to check for consistency between the \
kernel metadata and the algorithm layer. Defaults to True.
:raises GenerationError: if any of the arguments to the call are \
duplicated.
'''
# Textual representation of the valid children for this node.
_children_valid_format = "<LeafNode>"
def __init__(self, parent, call, name, ArgumentsClass, check=True):
# pylint: disable=too-many-arguments
super().__init__(parent=parent)
self._name = name
self._iterates_over = call.ktype.iterates_over
self._arguments = ArgumentsClass(call, self, check=check)
# check algorithm arguments are unique for a kernel or
# built-in call
arg_names = []
for arg in self._arguments.args:
if arg.text:
text = arg.text.lower().replace(" ", "")
if text in arg_names:
raise GenerationError(
f"Argument '{arg.text}' is passed into kernel "
f"'{self._name}' code more than once from the "
f"algorithm layer. This is not allowed.")
arg_names.append(text)
self._arg_descriptors = None
# Initialise any reduction information
const = Config.get().api_conf().get_constants()
args = args_filter(self._arguments.args,
arg_types=const.VALID_SCALAR_NAMES,
arg_accesses=[AccessType.REDUCTION])
if args:
self._reduction = True
if len(args) != 1:
raise GenerationError(
"PSyclone currently only supports a single reduction "
"in a kernel or builtin")
self._reduction_arg = args[0]
else:
self._reduction = False
self._reduction_arg = None
@property
def args(self):
'''Return the list of arguments associated with this node. Override the
base method and simply return our arguments. '''
return self.arguments.args
[docs]
def node_str(self, colour=True):
''' Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
return (self.coloured_name(colour) + " " + self.name +
"(" + self.arguments.names + ")")
@property
def is_reduction(self):
'''
:returns: whether this kernel/built-in contains a reduction variable.
:rtype: bool
'''
return self._reduction
@property
def reduction_arg(self):
'''
:returns: the reduction variable if this kernel/built-in
contains one and `None` otherwise.
:rtype: :py:class:`psyclone.psyGen.KernelArgument` or `NoneType`
'''
return self._reduction_arg
@property
def reprod_reduction(self):
'''
:returns: whether this kernel/built-in is enclosed within an OpenMP
do loop. If so report whether it has the reproducible flag
set. Note, this also catches OMPParallelDo Directives but
they have reprod set to False so it is OK.
:rtype: bool
'''
ancestor = self.ancestor(OMPDoDirective)
if ancestor:
return ancestor.reprod
return False
[docs]
def initialise_reduction_variable(self) -> None:
'''
Generate PSyIR to zero the reduction variable and to zero the local
reduction variable if one exists. The latter is used for reproducible
reductions, if specified.
:raises GenerationError: if the variable to initialise is not a scalar.
:raises GenerationError: if the reprod_pad_size (read from the
configuration file) is less than 1.
:raises GenerationError: for a reduction into a scalar that is
neither 'real' nor 'integer'.
'''
var_arg = self._reduction_arg
# Check for a non-scalar argument
if not var_arg.is_scalar:
raise GenerationError(
f"Kern.initialise_reduction_variable() should be a scalar but "
f"found '{var_arg.argument_type}'.")
# Lookup the reduction variable
variable = self.scope.symbol_table.lookup_with_tag(
f"AlgArgs_{var_arg.text}")
if not (isinstance(variable.datatype, ScalarType) and
variable.datatype.intrinsic in [ScalarType.Intrinsic.INTEGER,
ScalarType.Intrinsic.REAL]):
raise GenerationError(
f"Kern.initialise_reduction_variable() should be either a "
f"'real' or an 'integer' scalar but found scalar of type "
f"'{var_arg.intrinsic_type}'.")
# Find a safe location to initialise it.
insert_loc = self.ancestor((Loop, Directive))
while insert_loc:
loc = insert_loc.ancestor((Loop, Directive))
if not loc:
break
insert_loc = loc
cursor = insert_loc.position
insert_loc = insert_loc.parent
new_node = Assignment.create(
lhs=Reference(variable),
rhs=Literal("0", variable.datatype.copy()))
new_node.append_preceding_comment("Initialise reduction variable")
insert_loc.addchild(new_node, cursor)
cursor += 1
if self.reprod_reduction:
local_var = self.scope.symbol_table.lookup_with_tag(
f"{self.name}:{self._reduction_arg.name}:local")
assign = Assignment.create(
lhs=Reference(local_var),
rhs=Literal("0", variable.datatype.copy())
)
insert_loc.addchild(assign, cursor)
return new_node
[docs]
def reduction_sum_loop(self,
parent: Node,
position: int,
table: SymbolTable) -> None:
'''
Generate the appropriate code to place after the end parallel
region.
This method is designed to be used *after* a Kern has been lowered
(and thus detached) and therefore does not use `self.scope`.
:param parent: the node to which to add the Loop as a child.
:param position: where in the parent's list of children to add
the new Loop.
:param table: the SymbolTable to use.
:raises GenerationError: for an unsupported reduction access in
LFRicBuiltIn.
'''
tag = f"{self.name}:{self._reduction_arg.name}:local"
local_symbol = table.lookup_with_tag(tag)
symtab = table
thread_idx = symtab.lookup_with_tag("omp_thread_index")
nthreads = symtab.lookup_with_tag("omp_num_threads")
do_loop = Loop.create(
thread_idx,
start=Literal("1", ScalarType.integer_type()),
stop=Reference(nthreads),
step=Literal("1", ScalarType.integer_type()),
children=[])
parent.addchild(do_loop, position+1)
var_symbol = table.lookup_with_tag(
f"AlgArgs_{self._reduction_arg.text}")
do_loop.loop_body.addchild(Assignment.create(
lhs=Reference(var_symbol),
rhs=BinaryOperation.create(
BinaryOperation.Operator.ADD,
Reference(var_symbol),
ArrayReference.create(local_symbol,
[Literal("1", ScalarType.integer_type()),
Reference(thread_idx)]))))
do_loop.append_preceding_comment(
"sum the partial results sequentially")
do_loop.parent.addchild(
IntrinsicCall.create(IntrinsicCall.Intrinsic.DEALLOCATE,
[Reference(local_symbol)]),
do_loop.position+1)
def _reduction_reference(self):
'''
Return the reference to the reduction variable if OpenMP is set to
be unreproducible, as we will be using the OpenMP reduction clause.
Otherwise we will be computing the reduction ourselves and therefore
need to store values into a (padded) array separately for each
thread.
:returns: reference to the variable to be reduced.
:rtype: :py:class:`psyclone.psyir.nodes.Reference` or
:py:class:`psyclone.psyir.nodes.ArrayReference`
'''
symtab = self.ancestor(InvokeSchedule).symbol_table
if self.reprod_reduction:
local_var = symtab.lookup_with_tag(
f"{self.name}:{self._reduction_arg.name}:local")
# Return a multi-valued ArrayReference for a reproducible reduction
array_dim = [
Literal("1", ScalarType.integer_type()),
Reference(symtab.lookup_with_tag("omp_thread_index"))]
return ArrayReference.create(local_var, array_dim)
# Return a single-valued Reference for a non-reproducible reduction
local_var = symtab.lookup_with_tag(
f"AlgArgs_{self._reduction_arg.text}")
return Reference(local_var)
@property
def arg_descriptors(self):
return self._arg_descriptors
@arg_descriptors.setter
def arg_descriptors(self, obj):
self._arg_descriptors = obj
@property
def arguments(self):
return self._arguments
@property
def name(self) -> str:
'''
:returns: the name of the kernel.
'''
return self._name
[docs]
def is_coloured(self) -> bool:
'''
:returns: True if this kernel is being called from within a
coloured loop.
'''
parent_loop = self.ancestor(Loop)
while parent_loop:
if parent_loop.loop_type in ("cells_in_colour", "tiles_in_colour"):
return True
parent_loop = parent_loop.ancestor(Loop)
return False
@property
def iterates_over(self):
return self._iterates_over
[docs]
def lower_to_language_level(self):
'''
Replace this Kern with the equivalent, language-level PSyIR.
'''
if not self.is_reduction:
# If this kernel does not perform a reduction then there's
# no special action to take.
return super().lower_to_language_level()
table = self.ancestor(InvokeSchedule).symbol_table
arg_sym = table.lookup_with_tag("AlgArgs_"+self.reduction_arg.text)
if self.reprod_reduction:
# For reproducible reductions, we need a rank-2 array to store
# the thread-local results.
nthreads = table.lookup_with_tag("omp_num_threads")
if Config.get().reprod_pad_size < 1:
raise GenerationError(
f"REPROD_PAD_SIZE in {Config.get().filename} should be a "
f"positive integer, but it is set to "
f"'{Config.get().reprod_pad_size}'.")
pad_size = Literal(str(Config.get().reprod_pad_size),
ScalarType.integer_type())
array_type = ArrayType(arg_sym.datatype,
2*[ArrayType.Extent.DEFERRED])
local_var = table.find_or_create_tag(
root_name="local_"+self._reduction_arg.name,
tag=f"{self.name}:{self._reduction_arg.name}:local",
symbol_type=DataSymbol, datatype=array_type)
alloc = IntrinsicCall.create(
IntrinsicCall.Intrinsic.ALLOCATE,
[ArrayReference.create(local_var,
[pad_size, Reference(nthreads)])])
# Find a safe location to allocate it.
insert_loc = self.ancestor((Loop, Directive))
while insert_loc:
loc = insert_loc.ancestor((Loop, Directive))
if not loc:
break
insert_loc = loc
cursor = insert_loc.position
insert_loc = insert_loc.parent
insert_loc.addchild(alloc, cursor)
# This Kernel performs a reduction.
# Initialise the variable that will hold the result.
self.initialise_reduction_variable()
return super().lower_to_language_level()
[docs]
class CodedKern(Kern):
'''
Class representing a call to a PSyclone Kernel with a user-provided
implementation. The kernel may or may not be in-lined.
:param type KernelArguments: the API-specific sub-class of
:py:class:`psyclone.psyGen.Arguments` to create.
:param call: Details of the call to this kernel in the Algorithm layer.
:param parent: the parent of this Node (kernel call) in the Schedule.
:param check: whether to check for consistency between the
kernel metadata and the algorithm layer. Defaults to True.
'''
# Textual description of the node.
_text_name = "CodedKern"
_colour = "magenta"
def __init__(self,
KernelArguments: type,
call: KernelCall,
parent: Optional[Node] = None,
check: Optional[bool] = True):
# Set module_name first in case there is an error when
# processing arguments, as we can then return the module_name
# from where it happened.
self._module_name = call.module_name
super().__init__(parent, call,
call.ktype.procedure.name,
KernelArguments, check)
self._module_code = call.ktype._ast
self._kernel_code = call.ktype.procedure
self._fp2_ast = None #: The fparser2 AST for the kernel
#: PSyIR schedule(s) for the kernel
self._schedules = None
#: Whether or not this kernel has been transformed
self._modified = False
#: Whether or not to in-line this kernel into the module containing
#: the PSy layer
self._module_inline = False
self._opencl_options = {'local_size': 64, 'queue_number': 1}
self.arg_descriptors = call.ktype.arg_descriptors
# If we have an ancestor InvokeSchedule then add the necessary
# symbols.
# TODO #2054 - this 'routine' property can be replaced once this
# class sub-classes Call.
self.routine: Optional[Reference] = None
container = self.ancestor(Container)
if container:
symtab = container.symbol_table
csymbol = symtab.find_or_create(
self._module_name,
symbol_type=ContainerSymbol)
rsymbol = symtab.find_or_create(
self._name,
symbol_type=RoutineSymbol,
interface=ImportInterface(csymbol))
self.routine = Reference(rsymbol)
[docs]
def get_interface_symbol(self) -> None:
'''
By default, a Kern is not polymorphic and therefore has no interface
symbol.
'''
return None
[docs]
def get_callees(self):
'''
Returns the PSyIR Schedule(s) representing the kernel code. The
Schedules are just generated on first invocation, this allows us to
retain transformations that may subsequently be applied to the
Schedule(s).
:returns: Schedule(s) representing the kernel code.
:rtype: list[:py:class:`psyclone.psyir.nodes.KernelSchedule`]
:raises NotImplementedError: must be overridden in sub-class.
'''
raise NotImplementedError(
f"get_callees() must be overridden in class {self.__class__}")
@property
def opencl_options(self):
'''
:returns: dictionary of OpenCL options regarding the kernel.
:rtype: dictionary
'''
return self._opencl_options
[docs]
def set_opencl_options(self, options):
'''
Validate and store a set of options associated with the Kernel to
tune the OpenCL code generation.
:param options: a set of options to tune the OpenCL code.
:type options: dictionary of <string>:<value>
'''
valid_opencl_kernel_options = ['local_size', 'queue_number']
# Validate that the options given are supported
for key, value in options.items():
if key in valid_opencl_kernel_options:
if key == "local_size":
if not isinstance(value, int):
raise TypeError(
"CodedKern OpenCL option 'local_size' should be "
"an integer.")
if key == "queue_number":
if not isinstance(value, int):
raise TypeError(
"CodedKern OpenCL option 'queue_number' should be "
"an integer.")
else:
raise AttributeError(
f"CodedKern does not support the OpenCL option '{key}'. "
f"The supported options are: "
f"{valid_opencl_kernel_options}.")
self._opencl_options[key] = value
def __str__(self):
return "kern call: " + self._name
@property
def module_name(self):
'''
:returns: The name of the Fortran module that contains this kernel
:rtype: string
'''
return self._module_name
@property
def dag_name(self):
'''
:returns: the name to use in the DAG for this node.
:rtype: str
'''
_, position = self._find_position(self.ancestor(Routine))
return f"kernel_{self.name}_{position}"
@property
def module_inline(self) -> bool:
'''
:returns: whether or not this kernel is being module-inlined.
'''
# TODO #2054 - once this class sub-classes Call, this method should
# probably live in the super class.
if (not self.routine or self.routine.symbol.is_import or
self.routine.symbol.is_unresolved):
return False
return True
[docs]
def node_str(self, colour: Optional[bool] = True) -> str:
''' Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
'''
return (self.coloured_name(colour) + " " + self.name + "(" +
self.arguments.names + ") " + "[module_inline=" +
str(self.module_inline) + "]")
[docs]
def lower_to_language_level(self) -> Node:
'''
In-place replacement of CodedKern concept into language level
PSyIR constructs. The CodedKern is implemented as a Call to a
routine with the appropriate arguments.
:returns: the lowered version of this node.
'''
symtab = self.ancestor(InvokeSchedule).symbol_table
rsymbol = symtab.lookup(self._name)
# Create Call to the rsymbol with the argument expressions as children
# of the new node
call_node = Call.create(rsymbol, self.arguments.psyir_expressions())
# Swap itself with the appropriate Call node
self.replace_with(call_node)
return call_node
[docs]
def incremented_arg(self) -> str:
''' Returns the argument that has INC access.
:returns: a Fortran argument name.
:raises FieldNotFoundError: if none is found.
'''
for arg in self.arguments.args:
if arg.access == AccessType.INC:
return arg
raise FieldNotFoundError(f"Kernel {self.name} does not have an "
f"argument with "
f"{AccessType.INC.api_specific_name()} "
f"access")
@property
def ast(self):
'''
Generate and return the fparser2 AST of the kernel source.
:returns: fparser2 AST of the Fortran file containing this kernel.
:rtype: :py:class:`fparser.two.Fortran2003.Program`
'''
from fparser.common.readfortran import FortranStringReader
from fparser.two import parser
# If we've already got the AST then just return it
if self._fp2_ast:
return self._fp2_ast
# Use the fparser1 AST to generate Fortran source
fortran = self._module_code.tofortran()
# Create an fparser2 Fortran parser
std = Config.get().fortran_standard
my_parser = parser.ParserFactory().create(std=std)
# Parse that Fortran using our parser
reader = FortranStringReader(fortran)
self._fp2_ast = my_parser(reader)
return self._fp2_ast
[docs]
class InlinedKern(Kern):
'''A class representing a kernel that is inlined.
It has one child which stores the Schedule for the child nodes.
:param psyir_nodes: the list of PSyIR nodes that represent the body
of this kernel.
:type psyir_nodes: list of :py:class:`psyclone.psyir.nodes.Node`
:param parent: the parent of this node in the PSyIR.
:type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`
'''
# Textual description of the node.
_children_valid_format = "Schedule"
_text_name = "InlinedKern"
_colour = "magenta"
def __init__(self, psyir_nodes, parent=None):
# pylint: disable=non-parent-init-called, super-init-not-called
Node.__init__(self, parent=parent)
schedule = Schedule(children=psyir_nodes, parent=self)
self.children = [schedule]
self._arguments = None
@staticmethod
def _validate_child(position, child):
'''
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
return position == 0 and isinstance(child, Schedule)
[docs]
def node_str(self, colour=True):
''' Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
return self.coloured_name(colour) + "[]"
[docs]
class BuiltIn(Kern):
'''
Parent class for all built-ins (field operations for which the user
does not have to provide an implementation).
'''
# Textual description of the node.
_text_name = "BuiltIn"
_colour = "magenta"
def __init__(self):
# We cannot call Kern.__init__ as don't have necessary information
# here. Instead we provide a load() method that can be called once
# that information is available.
self._arg_descriptors = None
self._func_descriptors = None
self._fs_descriptors = None
self._reduction = None
@property
def dag_name(self):
'''
:returns: the name to use in the DAG for this node.
:rtype: str
'''
_, position = self._find_position(self.ancestor(Routine))
return f"builtin_{self.name}_{position}"
[docs]
def load(self, call, arguments, parent=None):
''' Set-up the state of this BuiltIn call '''
name = call.ktype.name
super(BuiltIn, self).__init__(parent, call, name, arguments)
[docs]
class Arguments():
'''
Arguments abstract base class.
:param parent_call: kernel call with which the arguments are associated.
:type parent_call: sub-class of :py:class:`psyclone.psyGen.Kern`
'''
def __init__(self, parent_call):
# TODO #2503: This reference is not kept updated when copign the
# parent
self._parent_call = parent_call
# The container object holding information on all arguments
# (derived from both kernel meta-data and the kernel call
# in the Algorithm layer).
self._args = []
[docs]
@abc.abstractmethod
def psyir_expressions(self):
'''
:returns: the PSyIR expressions representing this Argument list.
:rtype: list of :py:class:`psyclone.psyir.nodes.Node`
'''
@property
def names(self):
'''
:returns: the Algorithm-visible kernel arguments in a \
comma-delimited string.
:rtype: str
'''
return ",".join([arg.name for arg in self.args])
@property
def args(self):
return self._args
[docs]
def iteration_space_arg(self) -> str:
'''
Returns an argument that can be iterated over, i.e. modified
(has WRITE, READWRITE or INC access), but not the result of
a reduction operation.
:returns: a Fortran argument name
:raises GenerationError: if none such argument is found.
'''
for arg in self._args:
if arg.access in AccessType.all_write_accesses() and \
arg.access != AccessType.REDUCTION:
return arg
raise GenerationError("psyGen:Arguments:iteration_space_arg Error, "
"we assume there is at least one writer, "
"reader/writer, or increment as an argument")
@property
def acc_args(self):
'''
:returns: the list of quantities that must be available on an \
OpenACC device before the associated kernel can be launched
:rtype: list of str
'''
raise NotImplementedError(
"Arguments.acc_args must be implemented in sub-class")
@property
def scalars(self):
'''
:returns: the list of scalar quantities belonging to this object
:rtype: list of str
'''
raise NotImplementedError(
"Arguments.scalars must be implemented in sub-class")
[docs]
def append(self, name, argument_type):
''' Abstract method to append KernelArguments to the Argument
list.
:param str name: name of the appended argument.
:param str argument_type: type of the appended argument.
'''
raise NotImplementedError(
"Arguments.append must be implemented in sub-class")
[docs]
class DataAccess():
'''A helper class to simplify the determination of dependencies due to
overlapping accesses to data associated with instances of the
Argument class.
'''
def __init__(self, arg):
'''Store the argument associated with the instance of this class and
the Call, HaloExchange or GlobalReduction (or a subclass thereof)
instance with which the argument is associated.
:param arg: the argument that we are concerned with. An
argument can be found in a `Kern` a `HaloExchange` or a
`GlobalReduction` (or a subclass thereof)
:type arg: :py:class:`psyclone.psyGen.Argument`
'''
# the `psyclone.psyGen.Argument` we are concerned with
self._arg = arg
# initialise _covered and _vector_index_access to keep pylint
# happy
self._covered = None
self._vector_index_access = None
# Now actually set them to the required initial values
self.reset_coverage()
[docs]
def overlaps(self, arg):
'''Determine whether the accesses to the provided argument overlap
with the accesses of the source argument. Overlap means that
the accesses share at least one memory location. For example,
the arguments both access the 1st index of the same field.
We do not currently deal with accesses to a subset of an
argument (unless it is a vector). This distinction will need
to be added once loop splitting is supported.
:param arg: the argument to compare with our internal argument
:type arg: :py:class:`psyclone.psyGen.Argument`
:return bool: True if there are overlapping accesses between \
arguments (i.e. accesses share at least one memory \
location) and False if not.
'''
if self._arg.name != arg.name:
# the arguments are different args so do not overlap
return False
if isinstance(self._arg._call, HaloExchange) and \
isinstance(arg.call, HaloExchange) and \
(self._arg.vector_size > 1 or arg.vector_size > 1):
# This is a vector field and both accesses come from halo
# exchanges. As halo exchanges only access a particular
# vector, the accesses do not overlap if the vector indices
# being accessed differ.
# sanity check
if self._arg.vector_size != arg.vector_size:
raise InternalError(
f"DataAccess.overlaps(): vector sizes differ for field "
f"'{arg.name}' in two halo exchange calls. Found "
f"'{self._arg.vector_size}' and '{arg.vector_size}'")
if self._arg._call.vector_index != arg.call.vector_index:
# accesses are to different vector indices so do not overlap
return False
# accesses do overlap
return True
[docs]
def reset_coverage(self):
'''Reset internal state to allow re-use of the object for a different
situation.
'''
# False unless all data accessed by our local argument has
# also been accessed by other arguments.
self._covered = False
# Used to store individual vector component accesses when
# checking that all vector components have been accessed.
self._vector_index_access = []
[docs]
def update_coverage(self, arg):
'''Record any overlap between accesses to the supplied argument and
the internal argument. Overlap means that the accesses to the
two arguments share at least one memory location. If the
overlap results in all of the accesses to the internal
argument being covered (either directly or as a combination
with previous arguments) then ensure that the covered() method
returns True. Covered means that all memory accesses by the
internal argument have at least one corresponding access by
the supplied arguments.
:param arg: the argument used to compare with our internal \
argument in order to update coverage information
:type arg: :py:class:`psyclone.psyGen.Argument`
'''
if not self.overlaps(arg):
# There is no overlap so there is nothing to update.
return
if isinstance(arg.call, HaloExchange) and \
(hasattr(self._arg, 'vector_size') and self._arg.vector_size > 1):
# The supplied argument is a vector field coming from a
# halo exchange and therefore only accesses one of the
# vectors
if isinstance(self._arg._call, HaloExchange):
# I am also a halo exchange so only access one of the
# vectors. At this point the vector indices of the two
# halo exchange fields must be the same, which should
# never happen due to checks in the `overlaps()`
# method earlier
raise InternalError(
f"DataAccess:update_coverage() The halo exchange vector "
f"indices for '{self._arg.name}' are the same. This "
f"should never happen")
else:
# I am not a halo exchange so access all components of
# the vector. However, the supplied argument is a halo
# exchange so only accesses one of the
# components. This results in partial coverage
# (i.e. the overlap in accesses is partial). Therefore
# record the index that is accessed and check whether
# all indices are now covered (which would mean `full`
# coverage).
if arg.call.vector_index in self._vector_index_access:
raise InternalError(
"DataAccess:update_coverage() Found more than one "
"dependent halo exchange with the same vector index")
self._vector_index_access.append(arg.call.vector_index)
if len(self._vector_index_access) != self._arg.vector_size:
return
# This argument is covered i.e. all accesses by the
# internal argument have a corresponding access in one of the
# supplied arguments.
self._covered = True
@property
def covered(self):
'''Returns True if all of the data associated with this argument has
been covered by the arguments provided in update_coverage
:return bool: True if all of an argument is covered by \
previous accesses and False if not.
'''
return self._covered
[docs]
class Argument():
'''
Argument base class. Captures information on an argument that is passed
to a Kernel from an Invoke.
:param call: the kernel call that this argument is associated with.
:type call: :py:class:`psyclone.psyGen.Kern`
:param arg_info: Information about this argument collected by \
the parser.
:type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
:param access: the way in which this argument is accessed in \
the 'Kern'. Valid values are specified in the config \
object of the current API.
:type access: str
'''
# pylint: disable=too-many-instance-attributes
def __init__(self, call, arg_info, access):
self._call = call
if arg_info is not None:
self._text = arg_info.text
self._orig_name = arg_info.varname
self._form = arg_info.form
self._is_literal = arg_info.is_literal()
else:
self._text = ""
self._orig_name = ""
self._form = ""
self._is_literal = False
# Initialise access
self._access = access
# Default the precision, data type and module to 'None' (no
# explicit property specified)
self._precision = None
self._data_type = None
self._module_name = None
# Default the name to the original name for debugging
# purposes. This may be updated when _complete_init() is
# called.
self._name = self._orig_name
def _complete_init(self, arg_info):
'''Provides the initialisation of name, text and the declaration of
symbols in the symbol table if required. This initialisation
is not performed in the constructor as subclasses may need to
perform additional initialisation before infer_datatype is
called (in order to determine the values of precision,
data_type and module_name).
:param arg_info: Information about this argument collected by
the parser.
:type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
'''
if self._orig_name is None:
# this is an infrastructure call literal argument. Therefore
# we do not want an argument (_text=None) but we do want to
# keep the value (_name)
self._name = arg_info.text
self._text = None
else:
# There are unit-tests where we create Arguments without an
# associated call or InvokeSchedule.
if self._call and self._call.ancestor(InvokeSchedule):
symtab = self._call.ancestor(InvokeSchedule).symbol_table
# Keep original list of arguments
previous_arguments = symtab.argument_list
# Find the tag to use
tag = f"AlgArgs_{self._text}"
# Prepare the Argument Interface Access value
argument_access = ArgumentInterface.Access.READWRITE
# Find the tag or create a new symbol with expected attributes
data_type = self.infer_datatype()
# Ensure that the symbol will have a unique name in the final
# PSy routine.
self._orig_name = self._ensure_unique_name(self._orig_name)
new_argument = symtab.find_or_create_tag(
tag, root_name=self._orig_name, symbol_type=DataSymbol,
datatype=data_type,
interface=ArgumentInterface(argument_access))
self._name = new_argument.name
# Unless the argument already exists with another interface
# (e.g. import) they come from the invoke argument list
if (isinstance(new_argument.interface, ArgumentInterface) and
new_argument not in previous_arguments):
symtab.specify_argument_list(previous_arguments +
[new_argument])
@classmethod
def _ensure_unique_name(cls, name: str) -> str:
'''
Given the proposed argument name, returns a new name that will be
unique in the final PSy routine.
This base implementation just returns the supplied name unchanged.
:param name: the proposed name of a kernel argument.
:returns: a new name for the kernel argument.
'''
return name
[docs]
@abc.abstractmethod
def psyir_expression(self):
'''
:returns: the PSyIR expression represented by this Argument.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
[docs]
def infer_datatype(self):
''' Infer the datatype of this argument using the API rules. If no
specialisation of this method has been provided make the type
UnresolvedType for now (it may be provided later in the execution).
:returns: the datatype of this argument.
:rtype: :py:class::`psyclone.psyir.symbols.DataType`
'''
return UnresolvedType()
def __str__(self):
return self._name
@property
def name(self):
return self._name
@property
def text(self):
return self._text
@property
def form(self):
return self._form
@property
def is_literal(self):
return self._is_literal
@property
def access(self):
return self._access
@access.setter
def access(self, value):
'''Set the access type for this argument.
:param value: new access type.
:type value: :py:class:`psyclone.core.access_type.AccessType`.
:raises InternalError: if value is not an AccessType.
'''
if not isinstance(value, AccessType):
raise InternalError(f"Invalid access type '{value}' of type "
f"'{type(value)}.")
self._access = value
@property
def argument_type(self):
'''
Returns the type of the argument. APIs that do not have this
concept can use this base class version which just returns "field"
in all cases. APIs with this concept can override this method.
:returns: the API type of the kernel argument.
:rtype: str
'''
return "field"
@property
@abc.abstractmethod
def intrinsic_type(self):
'''
Abstract property for the intrinsic type of the argument with
specific implementations in different APIs.
:returns: the intrinsic type of this argument.
:rtype: str
'''
@property
def precision(self):
'''
:returns: the precision of this argument. Default value is None, \
explicit implementation is left to a specific API.
:rtype: str or NoneType
'''
return self._precision
@property
def data_type(self):
'''
:returns: the data type of this argument. Default value is None, \
explicit implementation is left to a specific API.
:rtype: str or NoneType
'''
return self._data_type
@property
def module_name(self):
'''
:returns: the name of the Fortran module that contains definitions \
for the argument data type. Default value is None, \
explicit implementation is left to a specific API.
:rtype: str or NoneType
'''
return self._module_name
@property
def call(self):
''' Return the call that this argument is associated with '''
return self._call
@call.setter
def call(self, value):
''' set the node that this argument is associated with '''
self._call = value
[docs]
def backward_dependence(self) -> Union[Argument, None]:
'''Returns the preceding argument that this argument has a direct
dependence with, or None if there is not one. The argument may
exist in a Call, a HaloExchange, or a GlobalReduction.
:returns: the first preceding argument that has a dependence
on this argument.
'''
nodes = self._call.preceding(reverse=True)
return self._find_argument(nodes)
[docs]
def forward_write_dependencies(
self,
ignore_halos: bool = False) -> list[Argument]:
'''Returns a list of following write arguments that this argument has
dependencies with. The arguments may exist in a Call, a
HaloExchange (unless `ignore_halos` is `True`), or a GlobalReduction.
If none are found then return an empty list. If self is not a
reader then return an empty list.
:param ignore_halos: if `True` then any write dependencies
involving a halo exchange are ignored. Defaults to `False`.
:returns: the arguments that have a following write
dependence on this argument.
'''
nodes = self._call.following()
results = self._find_write_arguments(nodes, ignore_halos=ignore_halos)
return results
[docs]
def backward_write_dependencies(
self, ignore_halos: bool = False) -> list[Argument]:
'''Returns a list of previous write arguments that this argument has
dependencies with. The arguments may exist in a Call, a
HaloExchange (unless `ignore_halos` is `True`), or a GlobalReduction.
If none are found then return an empty list. If self is not a
reader then return an empty list.
:param ignore_halos: if `True` then any write dependencies
involving a halo exchange are ignored. Defaults to `False`.
:returns: a list of arguments that have a preceding write
dependence on this argument.
'''
nodes = self._call.preceding(reverse=True)
results = self._find_write_arguments(nodes, ignore_halos=ignore_halos)
return results
[docs]
def forward_dependence(self) -> Union[Argument, None]:
'''Returns the following argument that this argument has a direct
dependence on, or `None` if there is not one. The argument may
exist in a Call, a HaloExchange or a GlobalReduction.
:returns: the first following argument that has a dependence
on this argument.
'''
nodes = self._call.following()
return self._find_argument(nodes)
[docs]
def forward_read_dependencies(self) -> list[Argument]:
'''Returns a list of following read arguments that this argument has
dependencies with. The arguments may exist in a Call, a
HaloExchange or a GlobalReduction. If none are found then
return an empty list. If self is not a writer then return an
empty list.
:returns: a list of following arguments that have a read
dependence on this argument.
'''
nodes = self._call.following()
return self._find_read_arguments(nodes)
def _find_argument(self, nodes):
'''Return the first argument in the list of nodes that has a
dependency with self. If one is not found return None
:param nodes: the list of nodes that this method examines.
:type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
:returns: An argument object or None.
:rtype: :py:class:`psyclone.psyGen.Argument`
'''
# pylint: disable=import-outside-toplevel
from psyclone.domain.common.psylayer import GlobalReduction
nodes_with_args = [x for x in nodes if
isinstance(x, (Kern, HaloExchange,
GlobalReduction))]
for node in nodes_with_args:
for argument in node.args:
if self._depends_on(argument):
return argument
return None
def _find_read_arguments(self, nodes):
'''Return a list of arguments from the list of nodes that have a read
dependency with self. If none are found then return an empty
list. If self is not a writer then return an empty list.
:param nodes: the list of nodes that this method examines.
:type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
:returns: a list of arguments that have a read dependence on \
this argument.
:rtype: list of :py:class:`psyclone.psyGen.Argument`
'''
if self.access not in AccessType.all_write_accesses():
# I am not a writer so there will be no read dependencies
return []
# pylint: disable=import-outside-toplevel
from psyclone.domain.common.psylayer import GlobalReduction
# We only need consider nodes that have arguments
nodes_with_args = [x for x in nodes if
isinstance(x, (Kern, HaloExchange,
GlobalReduction))]
access = DataAccess(self)
arguments = []
for node in nodes_with_args:
for argument in node.args:
# look at all arguments in our nodes
if argument.access in AccessType.all_read_accesses() and \
access.overlaps(argument):
arguments.append(argument)
if argument.access in AccessType.all_write_accesses():
access.update_coverage(argument)
if access.covered:
# We have now found all arguments upon which
# this argument depends so return the list.
return arguments
# we did not find a terminating write dependence in the list
# of nodes so we return any read dependencies that were found
return arguments
def _find_write_arguments(self, nodes, ignore_halos=False):
'''Return a list of arguments from the list of nodes that have a write
dependency with self. If none are found then return an empty
list. If self is not a reader then return an empty list.
:param nodes: the list of nodes that this method examines.
:type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
:param bool ignore_halos: if `True` then any write dependencies \
involving a halo exchange are ignored. Defaults to `False`.
:returns: a list of arguments that have a write dependence with \
this argument.
:rtype: list of :py:class:`psyclone.psyGen.Argument`
'''
if self.access not in AccessType.all_read_accesses():
# I am not a reader so there will be no write dependencies
return []
# pylint: disable=import-outside-toplevel
from psyclone.domain.common.psylayer import GlobalReduction
# We only need consider nodes that have arguments
nodes_with_args = [x for x in nodes if
isinstance(x, (Kern, GlobalReduction)) or
(isinstance(x, HaloExchange) and not ignore_halos)]
access = DataAccess(self)
arguments = []
for node in nodes_with_args:
for argument in node.args:
# look at all arguments in our nodes
if argument.access not in AccessType.all_write_accesses():
# no dependence if not a writer
continue
if not access.overlaps(argument):
# Accesses are independent of each other
continue
arguments.append(argument)
access.update_coverage(argument)
if access.covered:
# sanity check
if not isinstance(node, HaloExchange) and \
len(arguments) > 1:
raise InternalError(
"Found a writer dependence but there are already "
"dependencies. This should not happen.")
# We have now found all arguments upon which this
# argument depends so return the list.
return arguments
if arguments:
raise InternalError(
"Argument()._field_write_arguments() There are no more nodes "
"but there are already dependencies. This should not happen.")
# no dependencies have been found
return []
def _depends_on(self, argument):
'''If there is a dependency between the argument and self then return
True, otherwise return False. We consider there to be a
dependency between two arguments if the names are the same and
if one reads and one writes, or if both write. Dependencies
are often defined as being read-after-write (RAW),
write-after-read (WAR) and write after write (WAW). These
dependencies can be considered to be forward dependencies, in
the sense that RAW means that the read is after the write in
the schedule. Similarly for WAR and WAW. We capture these
dependencies in this method. However we also capture
dependencies in the opposite direction (backward
dependencies). These are the same dependencies as forward
dependencies but are reversed. One could consider these to be
read-before-write, write-before-read, and
write-before-write. The terminology of forward and backward to
indicate whether the argument we depend on is after or before
us in the schedule is borrowed from loop dependence analysis
where a forward dependence indicates a dependence in a future
loop iteration and a backward dependence indicates a
dependence on a previous loop iteration. Note, we currently
assume that any read or write to an argument results in a
dependence i.e. we do not consider the internal structure of
the argument (e.g. it may be an array). However, this
assumption is OK as all elements of an array are typically
accessed. However, we may need to revisit this when we change
the iteration spaces of loops e.g. for overlapping
communication and computation.
:param argument: the argument we will check to see whether \
there is a dependence on this argument instance (self).
:type argument: :py:class:`psyclone.psyGen.Argument`
:returns: True if there is a dependence and False if not.
:rtype: bool
'''
if argument.name == self._name:
if self.access in AccessType.all_write_accesses() and \
argument.access in AccessType.all_read_accesses():
return True
if self.access in AccessType.all_read_accesses() and \
argument.access in AccessType.all_write_accesses():
return True
if self.access in AccessType.all_write_accesses() and \
argument.access in AccessType.all_write_accesses():
return True
return False
[docs]
class KernelArgument(Argument):
'''
This class provides information about individual kernel-call
arguments as specified by the kernel argument metadata and the
kernel invocation in the Algorithm layer.
:param arg: information obtained from the metadata for this kernel \
argument.
:type arg: :py:class:`psyclone.parse.kernel.Descriptor`
:param arg_info: information on how this argument is specified in \
the Algorithm layer.
:type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
:param call: the PSyIR kernel node to which this argument pertains.
:type call: :py:class:`psyclone.psyGen.Kern`
'''
def __init__(self, arg, arg_info, call):
self._arg = arg
super().__init__(call, arg_info, arg.access)
@property
def space(self):
return self._arg.function_space
@property
def stencil(self):
return self._arg.stencil
@property
@abc.abstractmethod
def is_scalar(self):
''':returns: whether this variable is a scalar variable or not.
:rtype: bool'''
@property
def metadata_index(self):
'''
:returns: the position of the corresponding argument descriptor in \
the kernel metadata.
:rtype: int
'''
return self._arg.metadata_index
[docs]
class TransInfo():
'''
This class provides information about, and access to, the available
transformations in this implementation of PSyclone. New transformations
will be picked up automatically as long as they subclass the abstract
Transformation class.
.. warning::
This utility will not find Transformations under the new file
structure (TODO #620) and is deprecated.
For example:
>>> from psyclone.psyGen import TransInfo
>>> t = TransInfo()
>>> print(t.list)
There is 1 transformation available:
1: SwapTrans, A test transformation
>>> # accessing a transformation by index
>>> trans = t.get_trans_num(1)
>>> # accessing a transformation by name
>>> trans = t.get_trans_name("SwapTrans")
'''
def __init__(self, module=None, base_class=None):
''' if module and/or baseclass are provided then use these else use
the default module "Transformations" and the default base_class
"Transformation"'''
# TODO #620: This need to be improved to support the new
# layout, where transformations are in different directories and files.
# Leaving local imports so they will be removed once TransInfo is
# replaced.
warnings.warn("PSyclone Deprecation Warning: the TransInfo class is "
"deprecated. User transformation scripts should import "
"the required Transformation classes directly.",
DeprecationWarning, 2)
# pylint: disable=import-outside-toplevel
from psyclone import transformations
if module is None:
# default to the transformation module
module = transformations
if base_class is None:
base_class = Transformation
# find our transformations
self._classes = self._find_subclasses(module, base_class)
# create our transformations
self._objects = []
self._obj_map = {}
for my_class in self._classes:
my_object = my_class()
self._objects.append(my_object)
self._obj_map[my_object.name] = my_object
# TODO #620:
# Transformations that are in psyir and other subdirectories
# are not found by TransInfo, so we add some that are used in
# tests and examples explicitly. I'm leaving this import here
# so it is obvious it can be removed.
from psyclone.psyir.transformations import LoopFuseTrans
my_object = LoopFuseTrans()
# Only add the loop-fuse statement if base_class and module
# match for the loop fusion transformation.
if isinstance(my_object, base_class) and module == transformations:
self._objects.append(LoopFuseTrans())
self._obj_map["LoopFuseTrans"] = self._objects[-1]
@property
def list(self):
''' return a string with a human readable list of the available
transformations '''
if len(self._objects) == 1:
result = "There is 1 transformation available:"
else:
result = (f"There are {len(self._objects)} transformations "
f"available:")
result += os.linesep
for idx, my_object in enumerate(self._objects):
result += " " + str(idx+1) + ": " + my_object.name + ": " + \
str(my_object) + os.linesep
return result
@property
def num_trans(self):
''' return the number of transformations available '''
return len(self._objects)
[docs]
def get_trans_num(self, number):
''' return the transformation with this number (use list() first to
see available transformations) '''
if number < 1 or number > len(self._objects):
raise GenerationError("Invalid transformation number supplied")
return self._objects[number-1]
[docs]
def get_trans_name(self, name):
''' return the transformation with this name (use list() first to see
available transformations) '''
try:
return self._obj_map[name]
except KeyError:
raise GenerationError(f"Invalid transformation name: got {name} "
f"but expected one of "
f"{self._obj_map.keys()}")
def _find_subclasses(self, module: type, base_class: type) -> list[type]:
'''
Return a list of classes defined within the specified module that
are a subclass of the specified baseclass.
Takes care to exclude the 'Dynamo0p3' wrapper classes that are only
there for backwards compatibility.
:param module: the module in which to look for classes.
:param base_class: the base class which classes must subclass.
:returns: the classes in the supplied module that subclass the
supplied class.
'''
return [cls for name, cls in inspect.getmembers(module)
if inspect.isclass(cls) and not inspect.isabstract(cls) and
issubclass(cls, base_class) and cls is not base_class
and name[:9] != "Dynamo0p3"]
@dataclass
class ValidOption:
'''Class used to specify the valid options dict for a Transformation.
:param default: The default value for this option.
:param type: The type of this option.
:param typename: The (doc)string representation of type.
'''
default: object
type: object
typename: str
# For Sphinx AutoAPI documentation generation
__all__ = ['PSyFactory', 'PSy', 'Invokes', 'Invoke', 'InvokeSchedule',
'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern',
'BuiltIn', 'Arguments', 'DataAccess', 'Argument', 'KernelArgument',
'TransInfo', 'Transformation']