diff --git a/src/psyclone/domain/common/psylayer/__init__.py b/src/psyclone/domain/common/psylayer/__init__.py index d581422170..3983be9973 100644 --- a/src/psyclone/domain/common/psylayer/__init__.py +++ b/src/psyclone/domain/common/psylayer/__init__.py @@ -35,6 +35,6 @@ '''A package module for psyclone.domain.common.psylayer''' +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.domain.common.psylayer.psyloop import PSyLoop -__all__ = ["PSyLoop"] diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py new file mode 100644 index 0000000000..9c76f89d8d --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -0,0 +1,76 @@ +import copy + +from psyclone.configuration import Config +from psyclone.core import AccessType +from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import KernelArgument +from psyclone.psyir.nodes import Statement +from psyclone.psyir.nodes.node import Node + + +class GlobalReduction(Statement): + ''' + Represents a global-reduction in the PSyIR. + + :raises GenerationError: if distributed memory is not enabled. + :raises InternalError: if the supplied argument doesn't represent a scalar. + + ''' + # TODO is this really a leaf - it could have operands as children? + #: Textual description of the node. + _children_valid_format = "" + _text_name = "GlobalReduction" + #: The colour to use when creating a view of this node. + _colour = "cyan" + + def __init__(self, scalar: KernelArgument, parent: Node = None): + super().__init__(children=[], parent=parent) + # Check that distributed memory is enabled + if not Config.get().distributed_memory: + raise GenerationError( + f"It makes no sense to create a {self._text_name} object " + f"when distributed memory is not enabled (dm=False).") + + # Check that the global sum argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"{self._text_name}.init(): A global reduction argument should" + f" be a scalar but found argument of type " + f"'{scalar.argument_type}'.") + + self._scalar = copy.copy(scalar) + if scalar: + # Update scalar values appropriately + # Here "readwrite" denotes how the class GlobalSum + # accesses/updates a scalar + self._scalar.access = AccessType.READWRITE + self._scalar.call = self + + def node_str(self, colour: bool = True) -> str: + ''' + Returns a text description 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 f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" + + @property + def scalar(self): + ''' Return the scalar field that this global reduction acts on ''' + return self._scalar + + @property + def dag_name(self) -> str: + ''' + :returns: the name to use in the DAG for this node. + ''' + return f"{self._text_name}({self._scalar.name})_{self.position}" + + @property + def args(self): + ''' Return the list of arguments associated with this node. Override + the base method and simply return our argument.''' + return [self._scalar] diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py new file mode 100644 index 0000000000..e891ad4674 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -0,0 +1,10 @@ + +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction + + +class GlobalSum(GlobalReduction): + ''' + Generic GlobalSum class which can be added to a Schedule. + + ''' + _text_name = "GlobalSum" diff --git a/src/psyclone/domain/lfric/__init__.py b/src/psyclone/domain/lfric/__init__.py index 1cf86ec9b5..25837a226b 100644 --- a/src/psyclone/domain/lfric/__init__.py +++ b/src/psyclone/domain/lfric/__init__.py @@ -68,6 +68,9 @@ from psyclone.domain.lfric.lfric_kern_call_factory import LFRicKernCallFactory from psyclone.domain.lfric.lfric_collection import LFRicCollection from psyclone.domain.lfric.lfric_fields import LFRicFields +from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax +from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin +from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum from psyclone.domain.lfric.lfric_run_time_checks import LFRicRunTimeChecks from psyclone.domain.lfric.lfric_invokes import LFRicInvokes from psyclone.domain.lfric.lfric_scalar_args import LFRicScalarArgs @@ -78,34 +81,3 @@ from psyclone.domain.lfric.lfric_invoke_schedule import LFRicInvokeSchedule from psyclone.domain.lfric.lfric_dofmaps import LFRicDofmaps from psyclone.domain.lfric.lfric_stencils import LFRicStencils - - -__all__ = [ - 'ArgOrdering', - 'FunctionSpace', - 'KernCallAccArgList', - 'KernCallArgList', - 'KernelInterface', - 'KernStubArgList', - 'LFRicArgDescriptor', - 'LFRicCellIterators', - 'LFRicCollection', - 'LFRicConstants', - 'LFRicDofmaps', - 'LFRicDriverCreator', - 'LFRicFields', - 'LFRicHaloDepths', - 'LFRicInvoke', - 'LFRicInvokes', - 'LFRicInvokeSchedule', - 'LFRicKern', - 'LFRicKernCallFactory', - 'LFRicKernMetadata', - 'LFRicLoop', - 'LFRicLoopBounds', - 'LFRicPSy', - 'LFRicRunTimeChecks', - 'LFRicScalarArgs', - 'LFRicScalarArrayArgs', - 'LFRicStencils', - 'LFRicSymbolTable'] diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py new file mode 100644 index 0000000000..694bb50e21 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -0,0 +1,72 @@ +from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.errors import GenerationError +from psyclone.lfric import LFRicKernelArgument +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import ( + Assignment, Call, Reference, StructureReference) +from psyclone.psyir.nodes.node import Node +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalMax(GlobalReduction): + ''' + LFRic specific global max class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global min. + :param parent: the parent node of this node in the PSyIR. + + :raises InternalError: if the supplied argument is not a scalar. + :raises GenerationError: if the scalar is not of "real" intrinsic type. + + ''' + _text_name = "LFRicGlobalMax" + + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): + # Initialise the parent class + super().__init__(scalar, parent=parent) + + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalMax currently only supports real scalars, but " + f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + + ''' + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + sum_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + sum_name = symtab.find_or_create_tag("global_max", + symbol_type=DataSymbol, + datatype=sum_type) + tmp_var = symtab.lookup(name) + + # Create the assignments + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Perform global max" + self.parent.addchild(assign1, self.position) + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_max"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py new file mode 100644 index 0000000000..eaeae13e86 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -0,0 +1,77 @@ +from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.lfric import LFRicKernelArgument +from psyclone.errors import GenerationError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import ( + Assignment, Call, Node, Reference, StructureReference) +from psyclone.psyir.nodes.node import Node +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalMin(GlobalReduction): + ''' + LFRic specific global min class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global min. + :param parent: the parent node of this node in the PSyIR. + + :raises GenerationError: if the scalar is not of "real" or "integer" + intrinsic type. + + ''' + _text_name = "LFRicGlobalMin" + + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): + # Initialise the parent class + super().__init__(scalar, parent=parent) + + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type not in ["real", "integer"]: + raise GenerationError( + f"LFRicGlobalMin currently only supports real or integer " + f"scalars, but argument '{scalar.name}' in Kernel " + f"'{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + ''' + + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + # The Container from which to import the scalar type. + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + # The scalar type. + scal_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + # An instance of scalar type that we will use to get the global min. + sum_name = symtab.find_or_create_tag("global_min", + symbol_type=DataSymbol, + datatype=scal_type) + tmp_var = symtab.lookup(name) + + # Assign the value of the local scalar to the new scalar_type quantity + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Obtain global min" + self.parent.addchild(assign1, self.position) + # Use the 'get_min' method to compute the global min. + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_min"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py new file mode 100644 index 0000000000..4e3e109e75 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -0,0 +1,70 @@ +from psyclone.domain.common.psylayer.global_sum import GlobalReduction +from psyclone.errors import GenerationError +from psyclone.lfric import LFRicKernelArgument +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import ( + Assignment, Call, Reference, StructureReference) +from psyclone.psyir.nodes.node import Node +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalSum(GlobalReduction): + ''' + LFRic specific global sum class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global sum. + :param parent: the parent node of this node in the PSyIR. + + :raises GenerationError: if the scalar is not of "real" intrinsic type. + + ''' + _text_name = "LFRicGlobalSum" + + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): + # Initialise the parent class + super().__init__(scalar, parent=parent) + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalSum currently only supports real scalars, but " + f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + ''' + + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + sum_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + sum_name = symtab.find_or_create_tag("global_sum", + symbol_type=DataSymbol, + datatype=sum_type) + tmp_var = symtab.lookup(name) + + # Create the assignments + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Perform global sum" + self.parent.addchild(assign1, self.position) + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 8da97a72ef..6411adc1e2 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -40,10 +40,15 @@ ''' This module implements the LFRic-specific implementation of the Invoke base class from psyGen.py. ''' +from typing import TYPE_CHECKING + from psyclone.configuration import Config from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn +if TYPE_CHECKING: # pragma: no cover + from psyclone.domain.lfric.lfric_invokes import LFRicInvokes from psyclone.domain.lfric.lfric_loop import LFRicLoop -from psyclone.errors import GenerationError, FieldNotFoundError +from psyclone.errors import FieldNotFoundError, GenerationError, InternalError +from psyclone.parse.algorithm import InvokeCall from psyclone.psyGen import Invoke from psyclone.psyir.nodes import Assignment, Reference, Call, Literal from psyclone.psyir.symbols import ( @@ -57,26 +62,25 @@ class LFRicInvoke(Invoke): require. :param alg_invocation: object containing the invoke call information. - :type alg_invocation: :py:class:`psyclone.parse.algorithm.InvokeCall` - :param int idx: the position of the invoke in the list of invokes - contained in the Algorithm. - :param invokes: the Invokes object containing this LFRicInvoke - object. - :type invokes: :py:class:`psyclone.domain.lfric.LFRicInvokes` + :param idx: the position of the invoke in the list of invokes + contained in the Algorithm. + :param invokes: the Invokes object containing this LFRicInvoke. :raises GenerationError: if integer reductions are required in the - PSy-layer. - :raises GenerationError: if a global reduction operation other than sum - is required - TODO #2381. + PSy-layer. + :raises InternalError: if an unrecognised global reduction operation + is encountered. ''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-locals - def __init__(self, alg_invocation, idx, invokes): + def __init__(self, + alg_invocation: InvokeCall, + idx: int, + invokes: "LFRicInvokes"): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel from psyclone.domain.lfric import LFRicInvokeSchedule - Invoke.__init__(self, alg_invocation, idx, LFRicInvokeSchedule, - invokes) + super().__init__(alg_invocation, idx, LFRicInvokeSchedule, invokes) # The base class works out the algorithm code's unique argument # list and stores it in the 'self._alg_unique_args' @@ -85,15 +89,15 @@ def __init__(self, alg_invocation, idx, invokes): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel - from psyclone.lfric import (LFRicFunctionSpaces, LFRicGlobalSum, + from psyclone.lfric import (LFRicFunctionSpaces, LFRicLMAOperators, LFRicReferenceElement, LFRicCMAOperators, LFRicBasisFunctions, LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( - LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, - LFRicRunTimeChecks, + LFRicCellIterators, LFRicGlobalMax, LFRicGlobalMin, LFRicGlobalSum, + LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicScalarArrayArgs, LFRicFields, LFRicDofmaps, LFRicStencils) @@ -190,11 +194,16 @@ def __init__(self, alg_invocation, idx, invokes): if kern.reduction_type == LFRicBuiltIn.ReductionType.SUM: global_red = LFRicGlobalSum(kern.reduction_arg, parent=loop.parent) + elif kern.reduction_type == LFRicBuiltIn.ReductionType.MIN: + global_red = LFRicGlobalMin(kern.reduction_arg, + parent=loop.parent) + elif kern.reduction_type == LFRicBuiltIn.ReductionType.MAX: + global_red = LFRicGlobalMax(kern.reduction_arg, + parent=loop.parent) else: - raise GenerationError( - f"TODO #2381 - currently only global *sum* " - f"reductions are supported but kernel '{kern.name}' " - f"performs a {kern.reduction_type}") + raise InternalError( + f"Unrecognised reduction '{kern.reduction_type}' " + f"found for kernel '{kern.name}'.") loop.parent.children.insert(loop.position+1, global_red) # Add the halo depth(s) for any kernel(s) that operate in the halos diff --git a/src/psyclone/lfric.py b/src/psyclone/lfric.py index 89d2b0c842..d1bfe2531b 100644 --- a/src/psyclone/lfric.py +++ b/src/psyclone/lfric.py @@ -63,7 +63,7 @@ from psyclone.parse.kernel import getkerneldescriptors from psyclone.parse.utils import ParseError from psyclone.psyGen import (Arguments, DataAccess, InvokeSchedule, Kern, - KernelArgument, HaloExchange, GlobalSum) + KernelArgument, HaloExchange) from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import ( Reference, ACCEnterDataDirective, ArrayOfStructuresReference, @@ -3894,79 +3894,6 @@ def initialise(self, cursor): return cursor -class LFRicGlobalSum(GlobalSum): - ''' - LFRic specific global sum class which can be added to and - manipulated in a schedule. - - :param scalar: the kernel argument for which to perform a global sum. - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` - :param parent: the parent node of this node in the PSyIR. - :type parent: :py:class:`psyclone.psyir.nodes.Node` - - :raises GenerationError: if distributed memory is not enabled. - :raises InternalError: if the supplied argument is not a scalar. - :raises GenerationError: if the scalar is not of "real" intrinsic type. - - ''' - def __init__(self, scalar, parent=None): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - "It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False).") - # Check that the global sum argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"LFRicGlobalSum.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if scalar.intrinsic_type != "real": - raise GenerationError( - f"LFRicGlobalSum currently only supports real scalars, but " - f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " - f"'{scalar.intrinsic_type}' intrinsic type.") - # Initialise the parent class - super().__init__(scalar, parent=parent) - - def lower_to_language_level(self): - ''' - :returns: this node lowered to language-level PSyIR. - :rtype: :py:class:`psyclone.psyir.nodes.Node` - ''' - - # Get the name strings to use - name = self._scalar.name - type_name = self._scalar.data_type - mod_name = self._scalar.module_name - - # Get the symbols from the given names - symtab = self.ancestor(InvokeSchedule).symbol_table - sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) - sum_type = symtab.find_or_create(type_name, - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(sum_mod)) - sum_name = symtab.find_or_create_tag("global_sum", - symbol_type=DataSymbol, - datatype=sum_type) - tmp_var = symtab.lookup(name) - - # Create the assignments - assign1 = Assignment.create( - lhs=StructureReference.create(sum_name, ["value"]), - rhs=Reference(tmp_var) - ) - assign1.preceding_comment = "Perform global sum" - self.parent.addchild(assign1, self.position) - assign2 = Assignment.create( - lhs=Reference(tmp_var), - rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) - ) - return self.replace_with(assign2) - - def _create_depth_list(halo_info_list, parent): '''Halo exchanges may have more than one dependency. This method simplifies multiple dependencies to remove duplicates and any @@ -6603,7 +6530,6 @@ def data_on_device(self, _): 'LFRicInterGrid', 'LFRicBasisFunctions', 'LFRicBoundaryConditions', - 'LFRicGlobalSum', 'LFRicHaloExchange', 'LFRicHaloExchangeStart', 'LFRicHaloExchangeEnd', diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index dd7ed4f505..dea18e81da 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -693,65 +693,6 @@ def __str__(self): return result -class GlobalSum(Statement): - ''' - Generic Global Sum class which can be added to and manipulated - in, a schedule. - - :param scalar: the scalar that the global sum is stored into - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` - :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 = "" - _text_name = "GlobalSum" - _colour = "cyan" - - def __init__(self, scalar, parent=None): - Node.__init__(self, children=[], parent=parent) - import copy - self._scalar = copy.copy(scalar) - if scalar: - # Update scalar values appropriately - # Here "readwrite" denotes how the class GlobalSum - # accesses/updates a scalar - self._scalar.access = AccessType.READWRITE - self._scalar.call = self - - @property - def scalar(self): - ''' Return the scalar field that this global sum acts on ''' - return self._scalar - - @property - def dag_name(self): - ''' - :returns: the name to use in the DAG for this node. - :rtype: str - ''' - return f"globalsum({self._scalar.name})_{self.position}" - - @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' - return [self._scalar] - - def node_str(self, colour=True): - ''' - Returns a text description 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)}[scalar='{self._scalar.name}']" - - class HaloExchange(Statement): ''' Generic Halo Exchange class which can be added to and @@ -1838,12 +1779,12 @@ class DataAccess(): def __init__(self, arg): '''Store the argument associated with the instance of this class and - the Call, HaloExchange or GlobalSum (or a subclass thereof) + 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 \ - `GlobalSum` (or a subclass thereof) + :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` ''' @@ -2217,9 +2158,9 @@ def call(self, value): def backward_dependence(self): '''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 globalsum. + exist in a call, a haloexchange, or a GlobalReduction. - :returns: the first preceding argument that has a dependence \ + :returns: the first preceding argument that has a dependence on this argument. :rtype: :py:class:`psyclone.psyGen.Argument` @@ -2230,14 +2171,14 @@ def backward_dependence(self): def forward_write_dependencies(self, ignore_halos=False): '''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 globalsum. If - none are found then return an empty list. If self is not 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 bool ignore_halos: if `True` then any write dependencies \ + :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 following write \ + :returns: a list of arguments that have a following write dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2249,15 +2190,15 @@ def forward_write_dependencies(self, ignore_halos=False): def backward_write_dependencies(self, ignore_halos=False): '''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 globalsum. If - none are found then return an empty list. If self is not 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 \ + :param ignore_halos: if `True` then any write dependencies involving a halo exchange are ignored. Defaults to `False`. :type ignore_halos: bool - :returns: a list of arguments that have a preceding write \ + :returns: a list of arguments that have a preceding write dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2269,9 +2210,9 @@ def backward_write_dependencies(self, ignore_halos=False): def forward_dependence(self): '''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 globalsum. + exist in a call, a haloexchange, or a GlobalReduction. - :returns: the first following argument that has a dependence \ + :returns: the first following argument that has a dependence on this argument. :rtype: :py:class:`psyclone.psyGen.Argument` @@ -2282,11 +2223,11 @@ def forward_dependence(self): def forward_read_dependencies(self): '''Returns a list of following read arguments that this argument has dependencies with. The arguments may exist in a call, a - haloexchange, or a globalsum. If none are found then + 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 \ + :returns: a list of following arguments that have a read dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2305,8 +2246,11 @@ def _find_argument(self, nodes): :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, GlobalSum))] + isinstance(x, (Kern, HaloExchange, + GlobalReduction))] for node in nodes_with_args: for argument in node.args: if self._depends_on(argument): @@ -2330,9 +2274,13 @@ def _find_read_arguments(self, nodes): # 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, GlobalSum))] + isinstance(x, (Kern, HaloExchange, + GlobalReduction))] access = DataAccess(self) arguments = [] for node in nodes_with_args: @@ -2371,9 +2319,12 @@ def _find_write_arguments(self, nodes, ignore_halos=False): # 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, GlobalSum)) or + isinstance(x, (Kern, GlobalReduction)) or (isinstance(x, HaloExchange) and not ignore_halos)] access = DataAccess(self) arguments = [] @@ -2835,6 +2786,6 @@ def validate_options(self, **kwargs): # For Sphinx AutoAPI documentation generation __all__ = ['PSyFactory', 'PSy', 'Invokes', 'Invoke', 'InvokeSchedule', - 'GlobalSum', 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', + 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', 'BuiltIn', 'Arguments', 'DataAccess', 'Argument', 'KernelArgument', 'TransInfo', 'Transformation'] diff --git a/src/psyclone/psyir/transformations/extract_trans.py b/src/psyclone/psyir/transformations/extract_trans.py index 550c4cb57b..920d2b7992 100644 --- a/src/psyclone/psyir/transformations/extract_trans.py +++ b/src/psyclone/psyir/transformations/extract_trans.py @@ -39,7 +39,8 @@ of an Invoke into a stand-alone application." ''' -from psyclone.psyGen import BuiltIn, Kern, HaloExchange, GlobalSum +from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.psyGen import BuiltIn, Kern, HaloExchange from psyclone.psyir.nodes import (CodeBlock, ExtractNode, Loop, Schedule, Directive, OMPParallelDirective, ACCParallelDirective) @@ -64,15 +65,15 @@ class ExtractTrans(PSyDataTrans): Loops containing a Kernel or BuiltIn call) or entire Invokes. This functionality does not support distributed memory. - :param node_class: The Node class of which an instance will be inserted \ + :param node_class: The Node class of which an instance will be inserted into the tree (defaults to ExtractNode), but can be any derived class. - :type node_class: :py:class:`psyclone.psyir.nodes.ExtractNode` or \ + :type node_class: :py:class:`psyclone.psyir.nodes.ExtractNode` or derived class ''' # The types of node that this transformation cannot enclose excluded_node_types = (CodeBlock, ExtractNode, - HaloExchange, GlobalSum) + HaloExchange, GlobalReduction) def __init__(self, node_class=ExtractNode): # This function is required to provide the appropriate default diff --git a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py new file mode 100644 index 0000000000..04f2e7dd58 --- /dev/null +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -0,0 +1,147 @@ +# ----------------------------------------------------------------------------- +# 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: I. Kavcic, L. Turner, O. Brunt and J. G. Wallwork, Met Office +# Modified: A. B. G. Chalk, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Performs py.test tests on the GlobalReduction class. ''' + +import pytest + +from psyclone.core import AccessType +from psyclone.errors import GenerationError, InternalError +from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.psyir.nodes import Literal +from psyclone.psyir.nodes.node import colored +from psyclone.psyir.symbols import INTEGER_TYPE +from psyclone.tests.utilities import get_invoke + + +def test_globalreduction_node_str_and_dag_name(): + '''test the node_str and dag_name methods in the GlobalReduction class. The + simplest way to do this is to use an LFRic builtin example which contains a + scalar and then call node_str() on that. + + ''' + _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", + api="lfric", + dist_mem=True, idx=0) + gsum = None + for child in invoke.schedule.children: + if isinstance(child, GlobalReduction): + gsum = child + break + assert gsum + gred = GlobalReduction(gsum.scalar) + output = gred.node_str() + expected_output = (colored("GlobalReduction", GlobalReduction._colour) + + "[scalar='asum']") + assert expected_output in output + assert gred.dag_name == "GlobalReduction(asum)_0" + + +def test_globalreduction_children_validation(): + '''Test that a GlobalReduction does not accept any children.''' + _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", api="lfric", + idx=0, dist_mem=True) + gsum = None + for child in invoke.schedule.children: + if isinstance(child, GlobalReduction): + gsum = child + break + with pytest.raises(GenerationError) as excinfo: + gsum.addchild(Literal("2", INTEGER_TYPE)) + assert ("Item 'Literal' can't be child 0 of 'LFRicGlobalSum'. " + "LFRicGlobalSum is a LeafNode and doesn't accept children." + in str(excinfo.value)) + + +def test_globalsum_nodm_error(): + ''' Check that an instance of the GlobalReduction class raises an + exception if it is instantiated with no distributed memory enabled + (dm=False). We use the LFRic API to test this. + + ''' + # Get an instance of a real scalar + _, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", + api="lfric", dist_mem=False, idx=0) + schedule = invoke.schedule + loop = schedule.children[0] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(GenerationError) as err: + _ = GlobalReduction(argument) + assert ("It makes no sense to create a GlobalReduction object when " + "distributed memory is not enabled (dm=False)." + in str(err.value)) + + +def test_globalreduction_unsupported_argument(): + ''' Check that an instance of the GlobalReduction class raises an + exception for an unsupported argument type. ''' + # Get an instance of a non-scalar argument + _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + loop = schedule.children[4] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(InternalError) as err: + _ = GlobalReduction(argument) + assert ("GlobalReduction.init(): A global reduction argument should be a " + "scalar but found argument of type 'gh_field'." in str(err.value)) + + +def test_globalreduction_arg(): + ''' Check that the global-reduction argument is defined as gh_readwrite and + points to the GlobalReduction node ''' + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", idx=0, dist_mem=True) + schedule = invoke.schedule + glob_sum = schedule.children[2] + glob_sum_arg = glob_sum.scalar + assert glob_sum_arg.access == AccessType.READWRITE + assert glob_sum_arg.call == glob_sum + + +def test_globalreduction_args(): + '''Test that the globalreduction class args method returns the appropriate + argument ''' + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + global_sum = schedule.children[2] + assert len(global_sum.args) == 1 + assert global_sum.args[0] == global_sum.scalar diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 6aca9ecd4c..d879d182d4 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1975,7 +1975,7 @@ def test_int_to_real_x_precision(tmpdir, kind_name): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_minmaxval_x(fortran_writer): +def test_minmaxval_x(fortran_writer, tmp_path): ''' Tests for the minval_x and maxval_x builtins. ''' @@ -1995,23 +1995,26 @@ def test_minmaxval_x(fortran_writer): _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, dist_mem=False) kerns = invoke.schedule.kernels() - assert str(kerns[0]) == ("Built-in: minval_X (compute the global minimum " + assert str(kerns[1]) == ("Built-in: minval_X (compute the global minimum " "value contained in a field)") - code = fortran_writer(kerns[0]) + code = fortran_writer(kerns[1]) assert "amin = MIN(amin, f1_data(df))" in code, code - assert str(kerns[1]) == ("Built-in: maxval_X (compute the global maximum " + assert str(kerns[2]) == ("Built-in: maxval_X (compute the global maximum " "value contained in a field)") - code = fortran_writer(kerns[1]) + code = fortran_writer(kerns[2]) assert "amax = MAX(amax, f1_data(df))" in code, code - # Currently psy-layer generation with DM enabled won't work because we only - # have support for global sums. TODO #2381. - with pytest.raises(GenerationError) as err: - _ = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, - dist_mem=True) - assert ("TODO #2381 - currently only global *sum* reductions are supported" - in str(err.value)) + psy, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, + dist_mem=True) + output = fortran_writer(invoke.schedule) + assert "global_min%value = amin" in output + assert "amin = global_min%get_min()" in output + assert "global_max%value = amax" in output + assert "amax = global_max%get_max()" in output + + # Test compilation of generated code + assert LFRicBuild(tmp_path).code_compiles(psy) def test_real_to_int_x(fortran_writer): diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py new file mode 100644 index 0000000000..e7331325e0 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -0,0 +1,7 @@ +from psyclone.domain.lfric_global_min import LFRicGlobalMin + + +def test_lfricglobalmin(): + ''' + ''' + lgm = LFRicGlobalMin(arg) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py new file mode 100644 index 0000000000..ff476efe48 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py @@ -0,0 +1,67 @@ +# ----------------------------------------------------------------------------- +# 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: I. Kavcic, L. Turner, O. Brunt and J. G. Wallwork, Met Office +# Modified: A. B. G. Chalk, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Performs py.test tests on the LFRicGlobalSum class. ''' + +import pytest + +from psyclone.domain.lfric import LFRicGlobalSum +from psyclone.errors import GenerationError +from psyclone.tests.utilities import get_invoke + +TEST_API = "lfric" + + +def test_lfricglobalsum_unsupported_scalar(): + ''' Check that an instance of the LFRicGlobalSum class raises an + exception if an unsupported scalar type is provided when distributed + memory is enabled (dm=True). + + ''' + # Get an instance of an integer scalar + _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + schedule = invoke.schedule + loop = schedule.children[4] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[1] + with pytest.raises(GenerationError) as err: + _ = LFRicGlobalSum(argument) + assert ("LFRicGlobalSum currently only supports real scalars, but " + "argument 'iflag' in Kernel 'testkern_one_int_scalar_code' " + "has 'integer' intrinsic type." in str(err.value)) diff --git a/src/psyclone/tests/domain/lfric/lfric_invoke_test.py b/src/psyclone/tests/domain/lfric/lfric_invoke_test.py new file mode 100644 index 0000000000..dac8a32e19 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_invoke_test.py @@ -0,0 +1,165 @@ +import pytest + + +from psyclone.configuration import Config +from psyclone.domain.lfric import ( + FunctionSpace, LFRicConstants, LFRicGlobalMax, LFRicGlobalSum) +from psyclone.psyir.nodes import Loop +from psyclone.psyir.transformations import OMPParallelTrans +from psyclone.errors import GenerationError, InternalError +from psyclone.tests.lfric_build import LFRicBuild +from psyclone.tests.utilities import get_invoke +from psyclone.transformations import LFRicOMPLoopTrans + +TEST_API = "lfric" + + +def test_lfricinvoke_first_access(): + ''' Tests that we raise an error if LFRicInvoke.first_access(name) is + called for an argument name that doesn't exist ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + with pytest.raises(GenerationError) as excinfo: + invoke.first_access("not_an_arg") + assert ("Failed to find any kernel argument with name" + in str(excinfo.value)) + + +def test_lfricinvoke_arg_for_fs(): + ''' Test that LFRicInvoke.arg_for_funcspace() raises an error if + passed an invalid or unused function space. + + ''' + _, invoke = get_invoke("1_single_invoke.f90", api=TEST_API, idx=0, + dist_mem=True) + with pytest.raises(InternalError) as err: + _ = invoke.arg_for_funcspace(FunctionSpace("waah", "waah")) + const = LFRicConstants() + assert (f"Unrecognised function space 'waah'. The supported spaces are " + f"{const.VALID_FUNCTION_SPACE_NAMES}" in str(err.value)) + with pytest.raises(GenerationError) as excinfo: + invoke.arg_for_funcspace(FunctionSpace("wtheta", None)) + assert "No argument found on 'wtheta' space" in str(excinfo.value) + + +def test_lfricinvoke_uniq_declns_intent_inv_argtype(): + ''' Tests that we raise an error when LFRicInvoke.unique_declns_by_intent() + is called with at least one invalid argument type. ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + with pytest.raises(InternalError) as excinfo: + invoke.unique_declns_by_intent(["gh_invalid"]) + const = LFRicConstants() + assert (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 ['gh_invalid']." + in str(excinfo.value)) + + +def test_lfricinvoke_uniq_declns_intent_invalid_intrinsic(): + ''' Tests that we raise an error when Invoke.unique_declns_by_intent() + is called for an invalid intrinsic type. ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", idx=0, + api=TEST_API, dist_mem=True) + with pytest.raises(InternalError) as excinfo: + invoke.unique_declns_by_intent(["gh_scalar"], intrinsic_type="triple") + const = LFRicConstants() + assert (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 'triple'." + in str(excinfo.value)) + + +def test_lfricinvoke_uniq_declns_intent_ops(tmp_path): + ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct + list of arguments for operator arguments. ''' + psy, invoke = get_invoke("4.4_multikernel_invokes.f90", idx=0, + api=TEST_API, dist_mem=True) + args = invoke.unique_declns_by_intent(["gh_operator"]) + assert args['inout'] == [] + args_out = [arg.declaration_name for arg in args['out']] + assert args_out == ['op'] + assert args['in'] == [] + + assert LFRicBuild(tmp_path).code_compiles(psy) + + +def test_lfricinvoke_uniq_declns_intent_cma_ops(tmp_path): + ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct + list of arguments for columnwise operator arguments. ''' + psy, invoke = get_invoke("20.5_multi_cma_invoke.f90", idx=0, + api=TEST_API, dist_mem=True) + args = invoke.unique_declns_by_intent(["gh_columnwise_operator"]) + args_out = [arg.declaration_name for arg in args['out']] + assert args_out == ['cma_op1'] + args_inout = [arg.declaration_name for arg in args['inout']] + assert args_inout == ['cma_opc'] + args_in = [arg.declaration_name for arg in args['in']] + assert args_in == ['cma_opb'] + + assert LFRicBuild(tmp_path).code_compiles(psy) + + +def test_lfricinvoke_global_reductions(): + ''' + Check the construction of an LFRicInvoke containing a GlobalSum. + ''' + _, invoke = get_invoke("15.9.2_X_innerproduct_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert isinstance(invoke.schedule[1], LFRicGlobalSum) + _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert isinstance(invoke.schedule[4], LFRicGlobalMax) + + +def test_lfricinvoke_setup_psy_layer_symbols(monkeypatch, dist_mem): + ''' + Tests for the setup_psy_layer_symbols() method. + ''' + config = Config.get() + monkeypatch.setattr(config, "_reproducible_reductions", True) + _, invoke = get_invoke("15.9.2_X_innerproduct_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=dist_mem) + schedule = invoke.schedule + otrans = LFRicOMPLoopTrans() + rtrans = OMPParallelTrans() + # Apply an OpenMP do to the loop + for child in schedule.children: + if isinstance(child, Loop): + otrans.apply(child, {"reprod": True}) + # Apply an OpenMP Parallel for all loops + rtrans.apply(schedule.children[0:2]) + # Check that setup_psy_layer_symbols() populates the symbol table. + assert "f1_proxy" not in invoke.schedule.symbol_table + invoke.setup_psy_layer_symbols() + assert "f1_proxy" in invoke.schedule.symbol_table + assert invoke.schedule.symbol_table.lookup_with_tag("omp_num_threads") + assert "omp_get_max_threads" in invoke.schedule.symbol_table + + +def test_lfricinvoke_invalid_reduction(monkeypatch): + ''' + Check that the LFRicInvoke constructor raises the expected error if it + encounters an unknown type of reduction. + + ''' + # This is not easy to trigger so we resort to monkeypatching the definition + # of one of the kernels to give it an invalid reduction type. + from psyclone.domain.lfric.lfric_builtins import LFRicMaxvalXKern + monkeypatch.setattr(LFRicMaxvalXKern, "_reduction_type", "wrong") + + with pytest.raises(InternalError) as err: + _ = get_invoke("15.10.9_min_max_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert ("Unrecognised reduction 'wrong' found for kernel 'maxval_x'" + in str(err.value)) + + +def test_lfricinvoke_halo_depths(): + ''' + Test that the construction of an LFRicInvoke sets up the symbols + holding the various halo depths. + ''' + _, invoke = get_invoke("1.4_into_halos_invoke.f90", idx=0, + api=TEST_API, dist_mem=True) + assert invoke._alg_unique_halo_depth_args == ["hdepth"] diff --git a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index 8305e42ade..bb2e3328e0 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -48,11 +48,11 @@ from psyclone.core import AccessType, Signature from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans -from psyclone.domain.lfric import LFRicLoop +from psyclone.domain.lfric import LFRicGlobalSum, LFRicLoop from psyclone.lfric import (LFRicHaloExchangeStart, LFRicHaloExchangeEnd, LFRicHaloExchange) from psyclone.errors import GenerationError, InternalError -from psyclone.psyGen import InvokeSchedule, GlobalSum, BuiltIn +from psyclone.psyGen import InvokeSchedule, BuiltIn from psyclone.psyir.backend.visitor import VisitorError from psyclone.psyir.nodes import ( colored, Loop, Schedule, Literal, Directive, OMPDoDirective, @@ -3837,7 +3837,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): ompdefault = colored("OMPDefaultClause", Directive._colour) ompprivate = colored("OMPPrivateClause", Directive._colour) ompfprivate = colored("OMPFirstprivateClause", Directive._colour) - gsum = colored("GlobalSum", GlobalSum._colour) + gsum = colored("LFRicGlobalSum", LFRicGlobalSum._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index a26a0c6b3d..11e7f27792 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -53,7 +53,7 @@ LFRicKernMetadata, LFRicLoop) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.lfric import ( - LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicGlobalSum, + LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloReadAccess, KernCallArgList) from psyclone.errors import FieldNotFoundError, GenerationError, InternalError @@ -614,100 +614,6 @@ def test_invoke_uniq_declns_valid_access(): assert fields_proxy_readwritten == ["f1_proxy"] -def test_lfricinvoke_first_access(): - ''' Tests that we raise an error if LFRicInvoke.first_access(name) is - called for an argument name that doesn't exist ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(GenerationError) as excinfo: - psy.invokes.invoke_list[0].first_access("not_an_arg") - assert ("Failed to find any kernel argument with name" - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_inv_argtype(): - ''' Tests that we raise an error when LFRicInvoke.unique_declns_by_intent() - is called with at least one invalid argument type. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(InternalError) as excinfo: - psy.invokes.invoke_list[0].unique_declns_by_intent(["gh_invalid"]) - const = LFRicConstants() - assert (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 ['gh_invalid']." - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_invalid_intrinsic(): - ''' Tests that we raise an error when Invoke.unique_declns_by_intent() - is called for an invalid intrinsic type. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(InternalError) as excinfo: - psy.invokes.invoke_list[0].unique_declns_by_intent( - ["gh_scalar"], intrinsic_type="triple") - const = LFRicConstants() - assert (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 'triple'." - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_ops(tmpdir): - ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct - list of arguments for operator arguments. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "4.4_multikernel_invokes.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - args = psy.invokes.invoke_list[0].unique_declns_by_intent(["gh_operator"]) - assert args['inout'] == [] - args_out = [arg.declaration_name for arg in args['out']] - assert args_out == ['op'] - assert args['in'] == [] - - assert LFRicBuild(tmpdir).code_compiles(psy) - - -def test_lfricinvoke_uniq_declns_intent_cma_ops(tmpdir): - ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct - list of arguments for columnwise operator arguments. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "20.5_multi_cma_invoke.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - args = psy.invokes.invoke_list[0]\ - .unique_declns_by_intent(["gh_columnwise_operator"]) - args_out = [arg.declaration_name for arg in args['out']] - assert args_out == ['cma_op1'] - args_inout = [arg.declaration_name for arg in args['inout']] - assert args_inout == ['cma_opc'] - args_in = [arg.declaration_name for arg in args['in']] - assert args_in == ['cma_opb'] - - assert LFRicBuild(tmpdir).code_compiles(psy) - - -def test_lfricinvoke_arg_for_fs(): - ''' Tests that we raise an error when LFRicInvoke.arg_for_funcspace() is - called for an unused space. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(GenerationError) as excinfo: - psy.invokes.invoke_list[0].arg_for_funcspace(FunctionSpace("wtheta", - None)) - assert "No argument found on 'wtheta' space" in str(excinfo.value) - - def test_kernel_specific(tmpdir): ''' Test that a call to enforce boundary conditions is *not* added following a call to the matrix_vector_kernel_type kernel. Boundary @@ -2400,22 +2306,6 @@ def test_func_descriptor_str(): assert output in func_str -def test_lfrickern_arg_for_fs(): - ''' Test that LFRicInvoke.arg_for_funcspace() raises an error if - passed an invalid function space. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, "1_single_invoke.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - first_invoke = psy.invokes.invoke_list[0] - with pytest.raises(InternalError) as err: - _ = first_invoke.arg_for_funcspace(FunctionSpace("waah", "waah")) - const = LFRicConstants() - assert (f"Unrecognised function space 'waah'. The supported spaces are " - f"{const.VALID_FUNCTION_SPACE_NAMES}" in str(err.value)) - - def test_dist_memory_true(): ''' Test that the distributed memory flag is on by default. ''' Config._instance = None @@ -2954,71 +2844,6 @@ def test_haloexchange_correct_parent(): assert child.parent == schedule -def test_lfricglobalsum_unsupported_argument(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception for an unsupported argument type. ''' - # Get an instance of a non-scalar argument - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.6.1_single_invoke_1_int_scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[4] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(InternalError) as err: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " - "but found argument of type 'gh_field'." in str(err.value)) - - -def test_lfricglobalsum_unsupported_scalar(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception if an unsupported scalar type is provided when distributed - memory is enabled (dm=True). - - ''' - # Get an instance of an integer scalar - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.6.1_single_invoke_1_int_scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[4] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[1] - with pytest.raises(GenerationError) as err: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum currently only supports real scalars, but " - "argument 'iflag' in Kernel 'testkern_one_int_scalar_code' " - "has 'integer' intrinsic type." in str(err.value)) - - -def test_lfricglobalsum_nodm_error(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception if it is instantiated with no distributed memory enabled - (dm=False). - - ''' - # Get an instance of a real scalar - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.9_single_invoke_2_real_scalars.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=False).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[0] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(GenerationError) as err: - _ = LFRicGlobalSum(argument) - assert ("It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False)." - in str(err.value)) - - def test_no_updated_args(): ''' Check that we raise the expected exception when we encounter a kernel that does not write to any of its arguments ''' diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 8db4845f44..dbe410cc94 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -59,10 +59,11 @@ from psyclone.configuration import Config from psyclone.core.access_type import AccessType from psyclone.domain.common.psylayer import PSyLoop -from psyclone.domain.lfric import (lfric_builtins, LFRicInvokeSchedule, +from psyclone.domain.lfric import (lfric_builtins, + LFRicInvokeSchedule, LFRicKern, LFRicKernMetadata) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans -from psyclone.lfric import LFRicGlobalSum, LFRicKernelArguments +from psyclone.lfric import LFRicKernelArguments from psyclone.errors import FieldNotFoundError, GenerationError, InternalError from psyclone.generator import generate from psyclone.gocean1p0 import GOKern @@ -70,7 +71,7 @@ from psyclone.psyGen import (TransInfo, PSyFactory, InlinedKern, object_index, HaloExchange, Invoke, DataAccess, Kern, Arguments, CodedKern, Argument, - GlobalSum, InvokeSchedule) + InvokeSchedule) from psyclone.psyir.nodes import (Assignment, BinaryOperation, Container, Literal, Loop, Node, KernelSchedule, Call, colored, Schedule) @@ -1018,48 +1019,6 @@ def test_haloexchange_unknown_halo_depth(): assert halo_exchange._halo_depth is None -def test_globalsum_node_str(): - '''test the node_str method in the GlobalSum class. The simplest way - to do this is to use an LFRic builtin example which contains a - scalar and then call node_str() on that. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "15.9.1_X_innerproduct_Y_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - gsum = None - for child in psy.invokes.invoke_list[0].schedule.children: - if isinstance(child, LFRicGlobalSum): - gsum = child - break - assert gsum - output = gsum.node_str() - expected_output = (colored("GlobalSum", GlobalSum._colour) + - "[scalar='asum']") - assert expected_output in output - - -def test_globalsum_children_validation(): - '''Test that children added to GlobalSum are validated. A GlobalSum node - does not accept any children. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "15.9.1_X_innerproduct_Y_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - gsum = None - for child in psy.invokes.invoke_list[0].schedule.children: - if isinstance(child, LFRicGlobalSum): - gsum = child - break - with pytest.raises(GenerationError) as excinfo: - gsum.addchild(Literal("2", INTEGER_TYPE)) - assert ("Item 'Literal' can't be child 0 of 'GlobalSum'. GlobalSum is a" - " LeafNode and doesn't accept children.") in str(excinfo.value) - - def test_args_filter(): '''the args_filter() method is in both Loop() and Arguments() classes with the former method calling the latter. This example tests the @@ -1430,21 +1389,6 @@ def test_argument_find_read_arguments(): assert result[idx] == loop.loop_body[0].arguments.args[3] -def test_globalsum_arg(): - ''' Check that the globalsum argument is defined as gh_readwrite and - points to the GlobalSum node ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - glob_sum = schedule.children[2] - glob_sum_arg = glob_sum.scalar - assert glob_sum_arg.access == AccessType.READWRITE - assert glob_sum_arg.call == glob_sum - - def test_haloexchange_arg(): '''Check that the HaloExchange argument is defined as gh_readwrite and points to the HaloExchange node''' @@ -1700,20 +1644,6 @@ def test_haloexchange_args(): assert haloexchange.args[0] == haloexchange.field -def test_globalsum_args(): - '''Test that the globalsum class args method returns the appropriate - argument ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - global_sum = schedule.children[2] - assert len(global_sum.args) == 1 - assert global_sum.args[0] == global_sum.scalar - - def test_call_forward_dependence(): '''Test that the Call class forward_dependence method returns the closest dependent call after the current call in the schedule or diff --git a/src/psyclone/tests/psyir/nodes/node_test.py b/src/psyclone/tests/psyir/nodes/node_test.py index 2a2343152c..5f67cf78db 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -763,15 +763,13 @@ def test_dag_names(): idx = aref.children[0].detach() assert idx.dag_name == "Literal_0" - # GlobalSum and BuiltIn also have specialised dag_names + # BuiltIn also has specialised dag_names _, invoke_info = parse( os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), api="lfric") psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) invoke = psy.invokes.invoke_list[0] schedule = invoke.schedule - global_sum = schedule.children[2] - assert global_sum.dag_name == "globalsum(asum)_2" builtin = schedule.children[1].loop_body[0] assert builtin.dag_name == "builtin_sum_x_12" diff --git a/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 b/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 index e83f626a0a..26707bed7e 100644 --- a/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 +++ b/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 @@ -36,8 +36,8 @@ program single_invoke - ! Description: single point-wise operation (min/max of field elements) - ! specified in an invoke call. + ! Description: three point-wise operations (setval, min and max of field + ! elements) specified in an invoke call. use constants_mod, only: r_def use field_mod, only: field_type @@ -46,7 +46,8 @@ program single_invoke type(field_type) :: f1 real(r_def) :: amin, amax - call invoke( minval_X(amin, f1), & + call invoke( setval_C(f1, 1.0_r_def), & + minval_X(amin, f1), & maxval_X(amax, f1) ) end program single_invoke