From c871676fad925171460ecdfe7c8c0c33e8ce9868 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Oct 2025 21:56:02 +0100 Subject: [PATCH 01/41] #2381 add initial GlobalReduction class --- .../common/psylayer/global_reduction.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/psyclone/domain/common/psylayer/global_reduction.py 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..40170e1110 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -0,0 +1,67 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2022-2025, 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. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' This module contains the GlobalReduction node implementation.''' + +from __future__ import annotations +from enum import Enum + +from psyclone.psyir.nodes import Node, Statement + + +class GlobalReduction(Statement): + ''' + Generic global reduction operation. + + :param reduction: + ''' + # Textual description of the node. + _children_valid_format = "[DataNode]" + _text_name = "GlobalReduction" + _colour = "cyan" + + class Reduction(Enum): + ''' + ''' + MIN = 1 + MAX = 2 + MINVAL = 3 + MAXVAL = 4 + SUM = 5 + + def __init__(self, + reduction: Reduction + *kwargs): From b0c458cd70515012e14dd70e2a0a2b04455e3c64 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Oct 2025 21:59:25 +0100 Subject: [PATCH 02/41] #2381 linting --- .../domain/common/psylayer/global_reduction.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 40170e1110..feae4a18d3 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -39,7 +39,7 @@ from __future__ import annotations from enum import Enum -from psyclone.psyir.nodes import Node, Statement +from psyclone.psyir.nodes import Statement class GlobalReduction(Statement): @@ -47,6 +47,9 @@ class GlobalReduction(Statement): Generic global reduction operation. :param reduction: + + :raises TypeError: + ''' # Textual description of the node. _children_valid_format = "[DataNode]" @@ -63,5 +66,7 @@ class Reduction(Enum): SUM = 5 def __init__(self, - reduction: Reduction - *kwargs): + reduction: Reduction, + **kwargs): + if not isinstance(reduction, GlobalReduction.Reduction): + raise TypeError("huh") From 0d5dadf0282c6df9ec9b11b68baf180702804994 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 6 Oct 2025 16:58:37 +0100 Subject: [PATCH 03/41] #2381 mv LFRicGlobalSum out of lfric.py and rename --- .../common/psylayer/global_reduction.py | 1 + .../domain/lfric/lfric_global_reduction.py | 76 +++++++++++++++++++ src/psyclone/domain/lfric/lfric_invoke.py | 14 ++-- src/psyclone/lfric.py | 76 +------------------ .../lfric/lfric_global_reduction_test.py | 72 ++++++++++++++++++ src/psyclone/tests/lfric_test.py | 67 +--------------- 6 files changed, 160 insertions(+), 146 deletions(-) create mode 100644 src/psyclone/domain/lfric/lfric_global_reduction.py create mode 100644 src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index feae4a18d3..3258bcd3da 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -70,3 +70,4 @@ def __init__(self, **kwargs): if not isinstance(reduction, GlobalReduction.Reduction): raise TypeError("huh") + super.__init__(kwargs) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py new file mode 100644 index 0000000000..caa8c7bca8 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -0,0 +1,76 @@ +from psyclone.configuration import Config +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.errors import GenerationError, InternalError + + +class LFRicGlobalReduction(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. + :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, operation, 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__(operation, 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) diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index f5a73639fc..71bee31674 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -41,6 +41,7 @@ from psyclone.configuration import Config from psyclone.core import AccessType +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.domain.lfric.lfric_constants import LFRicConstants from psyclone.errors import GenerationError, FieldNotFoundError from psyclone.psyGen import Invoke @@ -84,16 +85,17 @@ def __init__(self, alg_invocation, idx, invokes): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel - from psyclone.lfric import (LFRicFunctionSpaces, LFRicGlobalSum, - LFRicLMAOperators, + from psyclone.lfric import (LFRicFunctionSpaces, LFRicLMAOperators, LFRicReferenceElement, LFRicCMAOperators, LFRicBasisFunctions, LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, - LFRicRunTimeChecks, - LFRicScalarArgs, LFRicFields, LFRicDofmaps, LFRicStencils) + LFRicRunTimeChecks, LFRicScalarArgs, LFRicFields, LFRicDofmaps, + LFRicStencils) + from psyclone.domain.lfric.lfric_global_reduction import ( + LFRicGlobalReduction) self.scalar_args = LFRicScalarArgs(self) @@ -185,7 +187,9 @@ def __init__(self, alg_invocation, idx, invokes): arg_types=const.VALID_SCALAR_NAMES, arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): - global_sum = LFRicGlobalSum(scalar, parent=loop.parent) + global_sum = LFRicGlobalReduction( + GlobalReduction.Reduction.SUM, + scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) # 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 8413191f58..d732638136 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, @@ -3895,79 +3895,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 @@ -6597,7 +6524,6 @@ def data_on_device(self, _): 'LFRicInterGrid', 'LFRicBasisFunctions', 'LFRicBoundaryConditions', - 'LFRicGlobalSum', 'LFRicHaloExchange', 'LFRicHaloExchangeStart', 'LFRicHaloExchangeEnd', diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py new file mode 100644 index 0000000000..dd2c095298 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path +import pytest + +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction +from psyclone.errors import GenerationError, InternalError +from psyclone.parse.algorithm import Arg, parse +from psyclone.tests.utilities import get_invoke + +BASE_PATH = Path(os.path.dirname(os.path.abspath(__file__))) +BASE_PATH = BASE_PATH / ".." / ".." / "test_files" / "lfric" + +TEST_API = "lfric" + + +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 = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", TEST_API, + 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: + _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, + 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 = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + 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: + _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, + 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 = get_invoke("1.9_single_invoke_2_real_scalars.f90", + TEST_API, 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: + _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, argument) + assert ("It makes no sense to create an LFRicGlobalSum object when " + "distributed memory is not enabled (dm=False)." + in str(err.value)) diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index 9f156a336f..71ec6a53b4 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -52,7 +52,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 @@ -2941,71 +2941,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 ''' From 6856abda4ee3aa97d703ea2143c452e69e08f5bd Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 6 Oct 2025 17:03:28 +0100 Subject: [PATCH 04/41] #2381 fix linting --- src/psyclone/domain/lfric/lfric_global_reduction.py | 6 ++++++ .../tests/domain/lfric/lfric_global_reduction_test.py | 1 - src/psyclone/tests/lfric_test.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py index caa8c7bca8..a19e818214 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -1,6 +1,12 @@ from psyclone.configuration import Config from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import (Assignment, Call, Reference, + StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) class LFRicGlobalReduction(GlobalReduction): diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py index dd2c095298..6a825bfb35 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -5,7 +5,6 @@ from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction from psyclone.errors import GenerationError, InternalError -from psyclone.parse.algorithm import Arg, parse from psyclone.tests.utilities import get_invoke BASE_PATH = Path(os.path.dirname(os.path.abspath(__file__))) diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index 71ec6a53b4..dc0132e6f0 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -52,7 +52,7 @@ LFRicKernMetadata, LFRicLoop) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.lfric import ( - LFRicACCEnterDataDirective, LFRicBoundaryConditions + LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloReadAccess, KernCallArgList) from psyclone.errors import FieldNotFoundError, GenerationError, InternalError From 279ec53f253d4c8715c1f96e18d4547484af365e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 09:23:10 +0100 Subject: [PATCH 05/41] #2381 ensure any pre-existing Config is wiped by test fixture --- src/psyclone/tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/psyclone/tests/conftest.py b/src/psyclone/tests/conftest.py index ee0a46a2f3..97586e72a8 100644 --- a/src/psyclone/tests/conftest.py +++ b/src/psyclone/tests/conftest.py @@ -98,6 +98,9 @@ def setup_psyclone_config(): independent of a potential psyclone config file installed by the user. ''' + # Ensure any Config object that has already been loaded is wiped. + Config._instance = None + config_file = Config.get_repository_config_file() # In case that PSyclone is installed and tested (e.g. GitHub Actions), From 4a2f3d42aa3226f11c622f1a20754b2b7ae81e2c Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 10:47:27 +0100 Subject: [PATCH 06/41] #2381 WIP fixing/moving tests [skip ci] --- .../common/psylayer/global_reduction.py | 27 +++++------ .../domain/lfric/lfric_global_reduction.py | 3 +- src/psyclone/domain/lfric/lfric_invoke.py | 4 +- .../psyir/transformations/extract_trans.py | 9 ++-- .../common/psylayer/global_reduction_test.py | 41 +++++++++++++++++ .../lfric_transformations_test.py | 5 +- src/psyclone/tests/psyGen_test.py | 46 +------------------ 7 files changed, 69 insertions(+), 66 deletions(-) create mode 100644 src/psyclone/tests/domain/common/psylayer/global_reduction_test.py diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 3258bcd3da..441a4d572e 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BSD 3-Clause License # -# Copyright (c) 2022-2025, Science and Technology Facilities Council. +# Copyright (c) 2025, Science and Technology Facilities Council. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -42,6 +42,16 @@ from psyclone.psyir.nodes import Statement +class ReductionOp(Enum): + ''' + ''' + MIN = 1 + MAX = 2 + MINVAL = 3 + MAXVAL = 4 + SUM = 5 + + class GlobalReduction(Statement): ''' Generic global reduction operation. @@ -56,18 +66,9 @@ class GlobalReduction(Statement): _text_name = "GlobalReduction" _colour = "cyan" - class Reduction(Enum): - ''' - ''' - MIN = 1 - MAX = 2 - MINVAL = 3 - MAXVAL = 4 - SUM = 5 - def __init__(self, - reduction: Reduction, + reduction: ReductionOp, **kwargs): - if not isinstance(reduction, GlobalReduction.Reduction): + if not isinstance(reduction, ReductionOp): raise TypeError("huh") - super.__init__(kwargs) + super().__init__(kwargs) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py index a19e818214..7491274244 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -42,8 +42,9 @@ def __init__(self, operation, scalar, parent=None): f"LFRicGlobalSum currently only supports real scalars, but " f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " f"'{scalar.intrinsic_type}' intrinsic type.") + self._scalar = scalar # Initialise the parent class - super().__init__(operation, scalar, parent=parent) + super().__init__(operation, parent=parent) def lower_to_language_level(self): ''' diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 71bee31674..64687220fe 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -41,7 +41,7 @@ from psyclone.configuration import Config from psyclone.core import AccessType -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.domain.common.psylayer.global_reduction import ReductionOp from psyclone.domain.lfric.lfric_constants import LFRicConstants from psyclone.errors import GenerationError, FieldNotFoundError from psyclone.psyGen import Invoke @@ -188,7 +188,7 @@ def __init__(self, alg_invocation, idx, invokes): arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): global_sum = LFRicGlobalReduction( - GlobalReduction.Reduction.SUM, + ReductionOp.SUM, scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) diff --git a/src/psyclone/psyir/transformations/extract_trans.py b/src/psyclone/psyir/transformations/extract_trans.py index 4d8232143a..23d971d177 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.global_reduction 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..4e88747931 --- /dev/null +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -0,0 +1,41 @@ +from psyclone.domain.common.psylayer.global_reduction import ( + GlobalReduction, ReductionOp) + +''' +Module containing pytest tests for the GlobalReduction class. +''' + +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. + + ''' + gred = GlobalReduction(ReductionOp.SUM, + scalar="a") + output = str(gred) + 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) + + 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 2c7142fe34..cd7977555b 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -46,13 +46,14 @@ from psyclone.configuration import Config from psyclone.core import AccessType, Signature +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.domain.lfric import 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, @@ -3753,7 +3754,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("GlobalReduction", GlobalReduction._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index cd206ba25b..a94d9f398c 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -62,7 +62,7 @@ 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 +70,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) @@ -1005,48 +1005,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 From 9eb6784b8a98905f175d0a8b4d135ed756530d58 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 13:59:26 +0100 Subject: [PATCH 07/41] #2381 fix all tests --- .../common/psylayer/global_reduction.py | 60 +++++++++++++- .../domain/lfric/lfric_global_reduction.py | 9 +- src/psyclone/psyGen.py | 83 ++++--------------- .../common/psylayer/global_reduction_test.py | 72 ++++++++++++---- .../lfric/lfric_global_reduction_test.py | 34 ++++++-- .../lfric_transformations_test.py | 4 +- src/psyclone/tests/psyGen_test.py | 35 +------- src/psyclone/tests/psyir/nodes/node_test.py | 2 +- 8 files changed, 169 insertions(+), 130 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 441a4d572e..c5dd5f6ea8 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -37,8 +37,12 @@ ''' This module contains the GlobalReduction node implementation.''' from __future__ import annotations +import copy from enum import Enum +from typing import Any +from psyclone.core import AccessType, VariablesAccessMap +from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes import Statement @@ -62,13 +66,67 @@ class GlobalReduction(Statement): ''' # Textual description of the node. - _children_valid_format = "[DataNode]" + _children_valid_format = "" _text_name = "GlobalReduction" _colour = "cyan" def __init__(self, reduction: ReductionOp, + operand: Any, **kwargs): if not isinstance(reduction, ReductionOp): raise TypeError("huh") + self._operation = reduction + # Ideally, 'operand' would be a child of this node but it's typically + # a KernelArgument, not a PSyIR Node. + # TODO Without this `copy`, the tests for the old-style DA fail. + self._operand = copy.copy(operand) + if isinstance(operand, KernelArgument): + # Add old-style dependency information + # Here "readwrite" denotes how the class GlobalSum + # accesses/updates a scalar + self._operand.access = AccessType.READWRITE + self._operand.call = self super().__init__(kwargs) + + @property + def operand(self): + ''' + :returns: the operand of this global reduction. + :rtype: Any + ''' + return self._operand + + @property + def dag_name(self): + ''' + :returns: the name to use in the DAG for this node. + :rtype: str + ''' + return f"globalreduction({self._operand.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._operand] + + 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)}[{self._operation.name}, " + f"operand='{self._operand.name}']") + + def ARPDBG_reference_accesses(self): + ''' + ''' + var_accesses = VariablesAccessMap() + var_accesses.update(self._operand.reference_accesses()) + return var_accesses diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py index 7491274244..5473cea7b4 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -42,9 +42,8 @@ def __init__(self, operation, scalar, parent=None): f"LFRicGlobalSum currently only supports real scalars, but " f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " f"'{scalar.intrinsic_type}' intrinsic type.") - self._scalar = scalar # Initialise the parent class - super().__init__(operation, parent=parent) + super().__init__(operation, scalar, parent=parent) def lower_to_language_level(self): ''' @@ -53,9 +52,9 @@ def lower_to_language_level(self): ''' # Get the name strings to use - name = self._scalar.name - type_name = self._scalar.data_type - mod_name = self._scalar.module_name + name = self._operand.name + type_name = self._operand.data_type + mod_name = self._operand.module_name # Get the symbols from the given names symtab = self.ancestor(InvokeSchedule).symbol_table diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index f04db7ef5c..02070ca2a3 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -725,65 +725,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 @@ -1939,12 +1880,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) + `GlobalReduction` (or a subclass thereof) :type arg: :py:class:`psyclone.psyGen.Argument` ''' @@ -2406,8 +2347,13 @@ def _find_argument(self, nodes): :rtype: :py:class:`psyclone.psyGen.Argument` ''' + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer.global_reduction 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): @@ -2432,8 +2378,12 @@ def _find_read_arguments(self, nodes): return [] # We only need consider nodes that have arguments + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer.global_reduction import ( + GlobalReduction) 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: @@ -2473,8 +2423,11 @@ def _find_write_arguments(self, nodes, ignore_halos=False): return [] # We only need consider nodes that have arguments + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer.global_reduction import ( + GlobalReduction) 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 = [] @@ -2936,6 +2889,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/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index 4e88747931..dbb179fc80 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -1,10 +1,54 @@ -from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction, ReductionOp) +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2025, 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 +# ----------------------------------------------------------------------------- ''' Module containing pytest tests for the GlobalReduction class. ''' +import pytest + +from psyclone.domain.common.psylayer.global_reduction import ( + GlobalReduction, ReductionOp) +from psyclone.errors import GenerationError +from psyclone.psyir.nodes import colored, Literal, Reference +from psyclone.psyir.symbols import INTEGER_TYPE, Symbol + + 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 @@ -12,10 +56,10 @@ def test_globalsum_node_str(): ''' gred = GlobalReduction(ReductionOp.SUM, - scalar="a") + operand=Reference(Symbol("a"))) output = str(gred) - expected_output = (colored("GlobalSum", GlobalSum._colour) + - "[scalar='asum']") + expected_output = (colored("GlobalReduction", GlobalReduction._colour) + + "[SUM, operand='a']") assert expected_output in output @@ -24,18 +68,10 @@ def test_globalsum_children_validation(): 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 + gsum = GlobalReduction(ReductionOp.SUM, + operand=Reference(Symbol("a"))) 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) - - + assert ("Item 'Literal' can't be child 0 of 'GlobalReduction'. " + "GlobalReduction is a LeafNode and doesn't accept children." + in str(excinfo.value)) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py index 6a825bfb35..a340058b7f 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -2,7 +2,8 @@ from pathlib import Path import pytest -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.core import AccessType +from psyclone.domain.common.psylayer.global_reduction import ReductionOp from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction from psyclone.errors import GenerationError, InternalError from psyclone.tests.utilities import get_invoke @@ -24,8 +25,7 @@ def test_lfricglobalsum_unsupported_argument(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(InternalError) as err: - _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, - argument) + _ = LFRicGlobalReduction(ReductionOp.SUM, argument) assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " "but found argument of type 'gh_field'." in str(err.value)) @@ -44,8 +44,7 @@ def test_lfricglobalsum_unsupported_scalar(): kernel = loop.loop_body[0] argument = kernel.arguments.args[1] with pytest.raises(GenerationError) as err: - _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, - argument) + _ = LFRicGlobalReduction(ReductionOp.SUM, 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)) @@ -65,7 +64,30 @@ def test_lfricglobalsum_nodm_error(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(GenerationError) as err: - _ = LFRicGlobalReduction(GlobalReduction.Reduction.SUM, argument) + _ = LFRicGlobalReduction(ReductionOp.SUM, 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_globalsum_arg(): + ''' Check that the globalsum argument is defined as gh_readwrite and + points to the GlobalSum node ''' + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + glob_sum = schedule.children[2] + glob_sum_arg = glob_sum.operand + assert glob_sum_arg.access == AccessType.READWRITE + assert glob_sum_arg.call == glob_sum + + +def test_globalsum_args(): + '''Test that the globalsum 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.operand 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 cd7977555b..c986c14678 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -3793,7 +3793,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "1: " + gsum + "[scalar='asum']\n" + + indent + "1: " + gsum + "[SUM, operand='asum']\n" + indent + "2: " + ompparallel + "[]\n" + 2*indent + sched + "[]\n" + 3*indent + "0: " + ompdo + "[omp_schedule=static]\n" + @@ -3824,7 +3824,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "4: " + gsum + "[scalar='bsum']\n") + indent + "4: " + gsum + "[SUM, operand='bsum']\n") if not annexed: expected = expected.replace("nannexed", "ndofs") else: # not dist_mem. annexed can be True or False diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index a94d9f398c..7eabd3fc40 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -1351,7 +1351,7 @@ def test_argument_find_argument(): schedule = invoke.schedule # a) globalsum arg depends on kern arg kern_asum_arg = schedule.children[3].loop_body[0].arguments.args[1] - glob_sum_arg = schedule.children[2].scalar + glob_sum_arg = schedule.children[2].operand result = kern_asum_arg._find_argument(schedule.children) assert result == glob_sum_arg # b) kern arg depends on globalsum arg @@ -1393,21 +1393,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''' @@ -1505,7 +1490,7 @@ def test_argument_forward_dependence(monkeypatch, annexed): schedule = invoke.schedule prev_arg = schedule.children[0].loop_body[0].arguments.args[1] sum_arg = schedule.children[1].loop_body[0].arguments.args[0] - global_sum_arg = schedule.children[2].scalar + global_sum_arg = schedule.children[2].operand next_arg = schedule.children[3].loop_body[0].arguments.args[1] # a) prev kern arg depends on sum result = prev_arg.forward_dependence() @@ -1572,7 +1557,7 @@ def test_argument_backward_dependence(monkeypatch, annexed): schedule = invoke.schedule prev_arg = schedule.children[0].loop_body[0].arguments.args[1] sum_arg = schedule.children[1].loop_body[0].arguments.args[0] - global_sum_arg = schedule.children[2].scalar + global_sum_arg = schedule.children[2].operand next_arg = schedule.children[3].loop_body[0].arguments.args[1] # a) next kern arg depends on global sum arg result = next_arg.backward_dependence() @@ -1663,20 +1648,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 21214ee68e..129a1d251a 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -772,7 +772,7 @@ def test_dag_names(): invoke = psy.invokes.invoke_list[0] schedule = invoke.schedule global_sum = schedule.children[2] - assert global_sum.dag_name == "globalsum(asum)_2" + assert global_sum.dag_name == "globalreduction(asum)_2" builtin = schedule.children[1].loop_body[0] assert builtin.dag_name == "builtin_sum_x_12" From 592195813c6bcc36472ae2ba12d352d5440c6d79 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 14:54:54 +0100 Subject: [PATCH 08/41] #2381 tidy and improve tests --- .../common/psylayer/global_reduction.py | 23 ++++++++++++----- .../common/psylayer/global_reduction_test.py | 25 ++++++++++++++----- .../lfric/lfric_global_reduction_test.py | 3 +++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index c5dd5f6ea8..a4577e961a 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -60,9 +60,11 @@ class GlobalReduction(Statement): ''' Generic global reduction operation. - :param reduction: + :param reduction: the type of reduction to perform. + :param operand: the operand of the reduction operation. - :raises TypeError: + :raises TypeError: if the supplied reduction is not an instance of + ReductionOp. ''' # Textual description of the node. @@ -75,7 +77,10 @@ def __init__(self, operand: Any, **kwargs): if not isinstance(reduction, ReductionOp): - raise TypeError("huh") + raise TypeError( + f"The 'reduction' argument to GlobalReduction must be an " + f"instance of ReductionOp but got " + f"'{type(reduction).__name__}'") self._operation = reduction # Ideally, 'operand' would be a child of this node but it's typically # a KernelArgument, not a PSyIR Node. @@ -90,13 +95,19 @@ def __init__(self, super().__init__(kwargs) @property - def operand(self): + def operand(self) -> Any: ''' :returns: the operand of this global reduction. - :rtype: Any ''' return self._operand + @property + def operation(self) -> ReductionOp: + ''' + :returns: the type of reduction that is performed. + ''' + return self._operation + @property def dag_name(self): ''' @@ -128,5 +139,5 @@ def ARPDBG_reference_accesses(self): ''' ''' var_accesses = VariablesAccessMap() - var_accesses.update(self._operand.reference_accesses()) + var_accesses.add_access() # TODO return var_accesses diff --git a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index dbb179fc80..a5e8f99170 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -49,11 +49,24 @@ from psyclone.psyir.symbols import INTEGER_TYPE, Symbol -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. +def test_globalreduction_init(): + ''' + Test the constructor of GlobalReduction. + ''' + gred = GlobalReduction(ReductionOp.SUM, + operand=Reference(Symbol("a"))) + assert gred.operand.name == "a" + assert gred.operation == ReductionOp.SUM + with pytest.raises(TypeError) as err: + _ = GlobalReduction("SUM", operand=Reference(Symbol("a"))) + assert ("The 'reduction' argument to GlobalReduction must be an instance " + "of ReductionOp but got 'str'" in str(err.value)) + + +def test_globalreduction_node_str(): + ''' + Test the node_str method in the GlobalReduction class. ''' gred = GlobalReduction(ReductionOp.SUM, operand=Reference(Symbol("a"))) @@ -64,8 +77,8 @@ def test_globalsum_node_str(): def test_globalsum_children_validation(): - '''Test that children added to GlobalSum are validated. A GlobalSum node - does not accept any children. + '''Test that children added to GlobalReduction are validated. A + GlobalReduction node does not accept any children. ''' gsum = GlobalReduction(ReductionOp.SUM, diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py index a340058b7f..ed3afebe39 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -1,3 +1,6 @@ + +'''Module containing pytest tests for the LFRicGlobalReduction class.''' + import os from pathlib import Path import pytest From 49b856b7edf93e46646f415a57676d27e3774dd7 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 22:25:22 +0100 Subject: [PATCH 09/41] #2381 get full unit coverage for global_reduction --- .../common/psylayer/global_reduction.py | 24 ++++++++---------- .../common/psylayer/global_reduction_test.py | 25 +++++++++++++++++++ src/psyclone/tests/psyir/nodes/node_test.py | 4 +-- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index a4577e961a..a41d112e66 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -41,13 +41,14 @@ from enum import Enum from typing import Any -from psyclone.core import AccessType, VariablesAccessMap +from psyclone.core import AccessType from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes import Statement class ReductionOp(Enum): ''' + Enumeration of the supported global reduction operations. ''' MIN = 1 MAX = 2 @@ -109,17 +110,19 @@ def operation(self) -> ReductionOp: return self._operation @property - def dag_name(self): + def dag_name(self) -> str: ''' :returns: the name to use in the DAG for this node. - :rtype: str ''' - return f"globalreduction({self._operand.name})_{self.position}" + return (f"globalreduction({self._operation.name},{self._operand.name})" + f"_{self.position}") @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' + def args(self) -> list[Any]: + ''' + :returns: the arguments associated with this node. Override + the base method and simply return the operand. + ''' return [self._operand] def node_str(self, colour: bool = True) -> str: @@ -134,10 +137,3 @@ def node_str(self, colour: bool = True) -> str: ''' return (f"{self.coloured_name(colour)}[{self._operation.name}, " f"operand='{self._operand.name}']") - - def ARPDBG_reference_accesses(self): - ''' - ''' - var_accesses = VariablesAccessMap() - var_accesses.add_access() # TODO - return var_accesses diff --git a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index a5e8f99170..f4352a0fc0 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -42,11 +42,14 @@ import pytest +from psyclone.core import AccessType from psyclone.domain.common.psylayer.global_reduction import ( GlobalReduction, ReductionOp) from psyclone.errors import GenerationError +from psyclone.psyGen import Kern from psyclone.psyir.nodes import colored, Literal, Reference from psyclone.psyir.symbols import INTEGER_TYPE, Symbol +from psyclone.tests.utilities import get_invoke def test_globalreduction_init(): @@ -57,12 +60,34 @@ def test_globalreduction_init(): operand=Reference(Symbol("a"))) assert gred.operand.name == "a" assert gred.operation == ReductionOp.SUM + assert gred.args == [gred.operand] with pytest.raises(TypeError) as err: _ = GlobalReduction("SUM", operand=Reference(Symbol("a"))) assert ("The 'reduction' argument to GlobalReduction must be an instance " "of ReductionOp but got 'str'" in str(err.value)) + # Construct with a KernelArgument. These are not easy to make so we create + # PSyIR for a PSyKAl invoke first. + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + kernels = schedule.walk(Kern) + # Get hold of a KernelArgument object + karg = kernels[1].args[0] + gred = GlobalReduction(ReductionOp.SUM, operand=karg) + assert gred.operand.access == AccessType.READWRITE + assert gred.operand.call == gred + + +def test_globalreduction_dag_name(): + ''' + Test the dag_name property. + ''' + gred = GlobalReduction(ReductionOp.SUM, + operand=Reference(Symbol("a"))) + assert gred.dag_name == "globalreduction(SUM,a)_0" + def test_globalreduction_node_str(): ''' diff --git a/src/psyclone/tests/psyir/nodes/node_test.py b/src/psyclone/tests/psyir/nodes/node_test.py index 129a1d251a..17ae9b3ab8 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -764,15 +764,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 has a specialised dag_name _, 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 == "globalreduction(asum)_2" builtin = schedule.children[1].loop_body[0] assert builtin.dag_name == "builtin_sum_x_12" From 355a900ce398935b0f035d0ff4b59968316aa9f0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 23:03:40 +0100 Subject: [PATCH 10/41] #2381 update from global sum to global reduction --- .../domain/lfric/lfric_global_reduction.py | 75 +++++++++++++++---- .../lfric/lfric_global_reduction_test.py | 67 +++++++++++++---- 2 files changed, 113 insertions(+), 29 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py index 5473cea7b4..feef1bbd82 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -1,3 +1,44 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2025, 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 and S. Siso, STFC Daresbury Lab +# Modified I. Kavcic, A. Coughtrie, L. Turner and O. Brunt, Met Office +# Modified J. Henrichs, Bureau of Meteorology +# Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''This module contains the LFRicGlobalReduction implementation.''' + from psyclone.configuration import Config from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.errors import GenerationError, InternalError @@ -11,39 +52,43 @@ class LFRicGlobalReduction(GlobalReduction): ''' - LFRic specific global sum class which can be added to and + LFRic specific global-reduction 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 operation: the type of global reduction to perform. + :param operand: the kernel argument for which to perform a global + reduction. + :type operand: :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. + :raises GenerationError: if the operand is not of "real" intrinsic type. ''' - def __init__(self, operation, scalar, parent=None): + def __init__(self, operation, operand, 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).") + "It makes no sense to create an LFRicGlobalReduction 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: + if not operand.is_scalar: raise InternalError( - f"LFRicGlobalSum.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") + f"LFRicGlobalReduction.init(): A global reduction argument " + f"should be a scalar but found argument of type " + f"'{operand.argument_type}'.") # Check scalar intrinsic types that this class supports (only # "real" for now) - if scalar.intrinsic_type != "real": + if operand.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.") + f"LFRicGlobalReduction currently only supports real scalars, " + f"but argument '{operand.name}' in Kernel " + f"'{operand.call.name}' has '{operand.intrinsic_type}' " + f"intrinsic type.") # Initialise the parent class - super().__init__(operation, scalar, parent=parent) + super().__init__(operation, operand, parent=parent) def lower_to_language_level(self): ''' diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py index ed3afebe39..9e3430d5c1 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -1,3 +1,41 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2025, 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 and S. Siso, STFC Daresbury Lab +# Modified I. Kavcic, A. Coughtrie, L. Turner and O. Brunt, Met Office +# Modified J. Henrichs, Bureau of Meteorology +# Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab +# ----------------------------------------------------------------------------- '''Module containing pytest tests for the LFRicGlobalReduction class.''' @@ -17,8 +55,8 @@ TEST_API = "lfric" -def test_lfricglobalsum_unsupported_argument(): - ''' Check that an instance of the LFRicGlobalSum class raises an +def test_lfricglobalreduction_unsupported_argument(): + ''' Check that an instance of the LFRicGlobalReduction 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", TEST_API, @@ -29,12 +67,13 @@ def test_lfricglobalsum_unsupported_argument(): argument = kernel.arguments.args[0] with pytest.raises(InternalError) as err: _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " - "but found argument of type 'gh_field'." in str(err.value)) + assert ("LFRicGlobalReduction.init(): A global reduction 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 +def test_lfricglobalreduction_unsupported_scalar(): + ''' Check that an instance of the LFRicGlobalReduction class raises an exception if an unsupported scalar type is provided when distributed memory is enabled (dm=True). @@ -48,13 +87,13 @@ def test_lfricglobalsum_unsupported_scalar(): argument = kernel.arguments.args[1] with pytest.raises(GenerationError) as err: _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("LFRicGlobalSum currently only supports real scalars, but " + assert ("LFRicGlobalReduction 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 +def test_lfricglobalreduction_nodm_error(): + ''' Check that an instance of the LFRicGlobalReduction class raises an exception if it is instantiated with no distributed memory enabled (dm=False). @@ -68,13 +107,13 @@ def test_lfricglobalsum_nodm_error(): argument = kernel.arguments.args[0] with pytest.raises(GenerationError) as err: _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("It makes no sense to create an LFRicGlobalSum object when " + assert ("It makes no sense to create an LFRicGlobalReduction object when " "distributed memory is not enabled (dm=False)." in str(err.value)) -def test_globalsum_arg(): - ''' Check that the globalsum argument is defined as gh_readwrite and +def test_globalreduction_arg(): + ''' Check that the globalreduction operand is defined as gh_readwrite and points to the GlobalSum node ''' _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", api="lfric", dist_mem=True, idx=0) @@ -85,8 +124,8 @@ def test_globalsum_arg(): assert glob_sum_arg.call == glob_sum -def test_globalsum_args(): - '''Test that the globalsum class args method returns the appropriate +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) From 88956f86f29932b45840779ed611bbcf0b191237 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 7 Oct 2025 23:04:37 +0100 Subject: [PATCH 11/41] #2381 fix linting --- src/psyclone/domain/lfric/lfric_global_reduction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py index feef1bbd82..2547cdd4bb 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -72,7 +72,7 @@ def __init__(self, operation, operand, parent=None): if not Config.get().distributed_memory: raise GenerationError( "It makes no sense to create an LFRicGlobalReduction object " - f"when distributed memory is not enabled (dm=False).") + "when distributed memory is not enabled (dm=False).") # Check that the global sum argument is indeed a scalar if not operand.is_scalar: raise InternalError( From 488d0a5a337f2320e1c6a76d18f7625ffbf237e3 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 9 Oct 2025 08:12:05 +0100 Subject: [PATCH 12/41] #2381 tidy test file --- .../tests/domain/lfric/lfric_global_reduction_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py index 9e3430d5c1..5cbd481155 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py @@ -39,8 +39,6 @@ '''Module containing pytest tests for the LFRicGlobalReduction class.''' -import os -from pathlib import Path import pytest from psyclone.core import AccessType @@ -49,9 +47,6 @@ from psyclone.errors import GenerationError, InternalError from psyclone.tests.utilities import get_invoke -BASE_PATH = Path(os.path.dirname(os.path.abspath(__file__))) -BASE_PATH = BASE_PATH / ".." / ".." / "test_files" / "lfric" - TEST_API = "lfric" From d26c56c3d45281e7a4077a6d497eca3169228511 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 15 Oct 2025 13:07:53 +0100 Subject: [PATCH 13/41] #2381 update docs --- doc/developer_guide/APIs.rst | 18 +++--------------- doc/developer_guide/dependency.rst | 4 ++-- doc/developer_guide/psyir.rst | 2 +- doc/developer_guide/psykal.rst | 2 +- doc/user_guide/transformations.rst | 6 +++--- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/doc/developer_guide/APIs.rst b/doc/developer_guide/APIs.rst index 16a575a8c1..be7c6e25c7 100644 --- a/doc/developer_guide/APIs.rst +++ b/doc/developer_guide/APIs.rst @@ -1044,21 +1044,9 @@ PSyIR for the arithmetic operations required by the particular BuiltIn. This PSyIR forms the new body of the dof loop containing the original BuiltIn node. -In constructing this PSyIR, suitable Symbols for the loop -variable and the various kernel arguments must be looked up. Since the -migration to the use of language-level PSyIR for the LFRic PSy layer -is at an early stage, in practise this often requires that suitable -Symbols be constructed and inserted into the symbol table of the PSy -layer routine. A lot of this work is currently performed in the -``LFRicKernelArgument.infer_datatype()`` method but ultimately (see -https://github.com/stfc/PSyclone/issues/1258) much of this will be -removed. - -The sum and inner product BuiltIns require extending PSyIR to handle -reductions in the ``GlobalSum`` class in ``psyGen.py``. Conversions from -``real`` to ``int`` and vice-versa require the target precisions be -available as symbols, which is being implemented as a part of the mixed -precision support. +The sum and inner product BuiltIns require PSyIR support for +reductions. This is provided by the ``domain.common.psyir.GlobalReduction`` +class. Kernel Metadata --------------- diff --git a/doc/developer_guide/dependency.rst b/doc/developer_guide/dependency.rst index f724fc8c05..8fc22ed79b 100644 --- a/doc/developer_guide/dependency.rst +++ b/doc/developer_guide/dependency.rst @@ -144,7 +144,7 @@ instances of the `Argument` class within a PSyIR tree. The `Argument` class is used to specify the data being passed into and out of instances of the `Kern` class, `HaloExchange` class and -`GlobalSum` class (and their subclasses). +`GlobalReduction` class (and their subclasses). As an illustration consider the following invoke:: @@ -217,7 +217,7 @@ exist. If there is a field vector associated with an instance of an `Argument` class then all of the data in its vector indices are assumed to be accessed when the argument is part of a `Kern` or a -`GlobalSum`. However, in contrast, a `HaloExchange` only acts on a +`GlobalReduction`. However, in contrast, a `HaloExchange` only acts on a single index of a field vector. Therefore there is one halo exchange per field vector index. For example:: diff --git a/doc/developer_guide/psyir.rst b/doc/developer_guide/psyir.rst index b9c9892990..f67f6d9769 100644 --- a/doc/developer_guide/psyir.rst +++ b/doc/developer_guide/psyir.rst @@ -953,7 +953,7 @@ PSy-layer concepts are the singular units of computation that can be found inside a `PSyLoop`. * The `HaloExchange` is a distributed-memory concept in the PSy-layer. -* The `GlobalSum` is a distributed-memory concept in the PSy-layer. +* The `GlobalReduction` is a distributed-memory concept in the PSy-layer. Other specializations diff --git a/doc/developer_guide/psykal.rst b/doc/developer_guide/psykal.rst index 38281edf02..b1aa88f65b 100644 --- a/doc/developer_guide/psykal.rst +++ b/doc/developer_guide/psykal.rst @@ -387,7 +387,7 @@ is shown below using the LFRic API as an illustration. The InvokeSchedule can currently contain nodes of type: **Loop**, **Kernel**, **Built-in** (see the :ref:`psykal-built-ins` section), **Directive** (of various types), **HaloExchange**, or -**GlobalSum** (the latter two are only used if distributed memory is +**GlobalReduction** (the latter two are only used if distributed memory is supported and is switched on; see the :ref:`psykal_usage` section). The order of the tree (depth first) indicates the order of the associated Fortran code. diff --git a/doc/user_guide/transformations.rst b/doc/user_guide/transformations.rst index 4b0cc279cc..441192ee17 100644 --- a/doc/user_guide/transformations.rst +++ b/doc/user_guide/transformations.rst @@ -710,9 +710,9 @@ PSyclone supports parallel scalar reductions. If a scalar reduction is specified in the Kernel metadata (see the API-specific sections for details) then PSyclone ensures the appropriate reduction is performed. -In the case of distributed memory, PSyclone will add **GlobalSum's** -at the appropriate locations. As can be inferred by the name, only -"summation" reductions are currently supported for distributed memory. +In the case of distributed memory, PSyclone will add **GlobalReduction's** +at the appropriate locations. Currently, only +"summation" reductions are supported for distributed memory (TODO #2381). In the case of an OpenMP parallel loop the standard reduction support will be used by default. For example From 226cfeb718efb5749634b42a9f5e18ade3fa0671 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 5 Nov 2025 15:03:06 +0000 Subject: [PATCH 14/41] #2381 rejig class hierarchy --- .../common/psylayer/global_reduction.py | 42 +++--------- .../domain/common/psylayer/global_sum.py | 6 ++ src/psyclone/domain/lfric/lfric_global_sum.py | 67 +++++++++++++++++++ src/psyclone/domain/lfric/lfric_invoke.py | 7 +- .../common/psylayer/global_reduction_test.py | 26 +++---- ...ction_test.py => lfric_global_sum_test.py} | 17 +++-- .../lfric_transformations_test.py | 4 +- 7 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 src/psyclone/domain/common/psylayer/global_sum.py create mode 100644 src/psyclone/domain/lfric/lfric_global_sum.py rename src/psyclone/tests/domain/lfric/{lfric_global_reduction_test.py => lfric_global_sum_test.py} (89%) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index a41d112e66..490c3c3392 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -41,32 +41,19 @@ from enum import Enum from typing import Any +from psyclone.configuration import Config from psyclone.core import AccessType +from psyclone.errors import GenerationError from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes import Statement -class ReductionOp(Enum): - ''' - Enumeration of the supported global reduction operations. - ''' - MIN = 1 - MAX = 2 - MINVAL = 3 - MAXVAL = 4 - SUM = 5 - - class GlobalReduction(Statement): ''' Generic global reduction operation. - :param reduction: the type of reduction to perform. :param operand: the operand of the reduction operation. - :raises TypeError: if the supplied reduction is not an instance of - ReductionOp. - ''' # Textual description of the node. _children_valid_format = "" @@ -74,15 +61,14 @@ class GlobalReduction(Statement): _colour = "cyan" def __init__(self, - reduction: ReductionOp, operand: Any, **kwargs): - if not isinstance(reduction, ReductionOp): - raise TypeError( - f"The 'reduction' argument to GlobalReduction must be an " - f"instance of ReductionOp but got " - f"'{type(reduction).__name__}'") - self._operation = reduction + # Check that distributed memory is enabled + if not Config.get().distributed_memory: + raise GenerationError( + "It makes no sense to create a GlobalReduction object " + "when distributed memory is not enabled (dm=False).") + # Ideally, 'operand' would be a child of this node but it's typically # a KernelArgument, not a PSyIR Node. # TODO Without this `copy`, the tests for the old-style DA fail. @@ -102,19 +88,12 @@ def operand(self) -> Any: ''' return self._operand - @property - def operation(self) -> ReductionOp: - ''' - :returns: the type of reduction that is performed. - ''' - return self._operation - @property def dag_name(self) -> str: ''' :returns: the name to use in the DAG for this node. ''' - return (f"globalreduction({self._operation.name},{self._operand.name})" + return (f"globalreduction({self._operand.name})" f"_{self.position}") @property @@ -135,5 +114,6 @@ def node_str(self, colour: bool = True) -> str: :returns: description of this node, possibly coloured. ''' - return (f"{self.coloured_name(colour)}[{self._operation.name}, " + # TODO move this to sub-classes (e.g. GlobalSum) + return (f"{self.coloured_name(colour)}[" f"operand='{self._operand.name}']") 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..d02b06ff46 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -0,0 +1,6 @@ +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction + + +class GlobalSum(GlobalReduction): + ''' + ''' 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..534235800d --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -0,0 +1,67 @@ +from psyclone.configuration import Config +from psyclone.domain.common.psylayer.global_sum import GlobalSum +from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import (Assignment, Call, Reference, + StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalSum(GlobalSum): + ''' + ''' + def __init__(self, operand, parent=None): + # Check that the global sum argument is indeed a scalar + if not operand.is_scalar: + raise InternalError( + f"LFRicGlobalSum.init(): A global reduction argument " + f"should be a scalar but found argument of type " + f"'{operand.argument_type}'.") + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if operand.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalSum currently only supports real scalars, " + f"but argument '{operand.name}' in Kernel " + f"'{operand.call.name}' has '{operand.intrinsic_type}' " + f"intrinsic type.") + # Initialise the parent class + super().__init__(operand, 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._operand.name + type_name = self._operand.data_type + mod_name = self._operand.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 64687220fe..2227b9cc55 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -41,7 +41,6 @@ from psyclone.configuration import Config from psyclone.core import AccessType -from psyclone.domain.common.psylayer.global_reduction import ReductionOp from psyclone.domain.lfric.lfric_constants import LFRicConstants from psyclone.errors import GenerationError, FieldNotFoundError from psyclone.psyGen import Invoke @@ -94,8 +93,7 @@ def __init__(self, alg_invocation, idx, invokes): LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicFields, LFRicDofmaps, LFRicStencils) - from psyclone.domain.lfric.lfric_global_reduction import ( - LFRicGlobalReduction) + from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum self.scalar_args = LFRicScalarArgs(self) @@ -187,8 +185,7 @@ def __init__(self, alg_invocation, idx, invokes): arg_types=const.VALID_SCALAR_NAMES, arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): - global_sum = LFRicGlobalReduction( - ReductionOp.SUM, + global_sum = LFRicGlobalSum( scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) diff --git a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index f4352a0fc0..66dcd5904d 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -44,7 +44,7 @@ from psyclone.core import AccessType from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction, ReductionOp) + GlobalReduction) from psyclone.errors import GenerationError from psyclone.psyGen import Kern from psyclone.psyir.nodes import colored, Literal, Reference @@ -56,17 +56,10 @@ def test_globalreduction_init(): ''' Test the constructor of GlobalReduction. ''' - gred = GlobalReduction(ReductionOp.SUM, - operand=Reference(Symbol("a"))) + gred = GlobalReduction(operand=Reference(Symbol("a"))) assert gred.operand.name == "a" - assert gred.operation == ReductionOp.SUM assert gred.args == [gred.operand] - with pytest.raises(TypeError) as err: - _ = GlobalReduction("SUM", operand=Reference(Symbol("a"))) - assert ("The 'reduction' argument to GlobalReduction must be an instance " - "of ReductionOp but got 'str'" in str(err.value)) - # Construct with a KernelArgument. These are not easy to make so we create # PSyIR for a PSyKAl invoke first. _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", @@ -75,7 +68,7 @@ def test_globalreduction_init(): kernels = schedule.walk(Kern) # Get hold of a KernelArgument object karg = kernels[1].args[0] - gred = GlobalReduction(ReductionOp.SUM, operand=karg) + gred = GlobalReduction(operand=karg) assert gred.operand.access == AccessType.READWRITE assert gred.operand.call == gred @@ -84,20 +77,18 @@ def test_globalreduction_dag_name(): ''' Test the dag_name property. ''' - gred = GlobalReduction(ReductionOp.SUM, - operand=Reference(Symbol("a"))) - assert gred.dag_name == "globalreduction(SUM,a)_0" + gred = GlobalReduction(operand=Reference(Symbol("a"))) + assert gred.dag_name == "globalreduction(a)_0" def test_globalreduction_node_str(): ''' Test the node_str method in the GlobalReduction class. ''' - gred = GlobalReduction(ReductionOp.SUM, - operand=Reference(Symbol("a"))) + gred = GlobalReduction(operand=Reference(Symbol("a"))) output = str(gred) expected_output = (colored("GlobalReduction", GlobalReduction._colour) + - "[SUM, operand='a']") + "[operand='a']") assert expected_output in output @@ -106,8 +97,7 @@ def test_globalsum_children_validation(): GlobalReduction node does not accept any children. ''' - gsum = GlobalReduction(ReductionOp.SUM, - operand=Reference(Symbol("a"))) + gsum = GlobalReduction(operand=Reference(Symbol("a"))) with pytest.raises(GenerationError) as excinfo: gsum.addchild(Literal("2", INTEGER_TYPE)) assert ("Item 'Literal' can't be child 0 of 'GlobalReduction'. " diff --git a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py similarity index 89% rename from src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py rename to src/psyclone/tests/domain/lfric/lfric_global_sum_test.py index 5cbd481155..a258acf37d 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_reduction_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py @@ -37,13 +37,12 @@ # Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab # ----------------------------------------------------------------------------- -'''Module containing pytest tests for the LFRicGlobalReduction class.''' +'''Module containing pytest tests for the LFRicGlobalSum class.''' import pytest from psyclone.core import AccessType -from psyclone.domain.common.psylayer.global_reduction import ReductionOp -from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction +from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum from psyclone.errors import GenerationError, InternalError from psyclone.tests.utilities import get_invoke @@ -61,8 +60,8 @@ def test_lfricglobalreduction_unsupported_argument(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(InternalError) as err: - _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("LFRicGlobalReduction.init(): A global reduction argument should " + _ = LFRicGlobalSum(argument) + assert ("LFRicGlobalSum.init(): A global reduction argument should " "be a scalar but found argument of type 'gh_field'." in str(err.value)) @@ -81,8 +80,8 @@ def test_lfricglobalreduction_unsupported_scalar(): kernel = loop.loop_body[0] argument = kernel.arguments.args[1] with pytest.raises(GenerationError) as err: - _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("LFRicGlobalReduction currently only supports real scalars, but " + _ = 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)) @@ -101,8 +100,8 @@ def test_lfricglobalreduction_nodm_error(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(GenerationError) as err: - _ = LFRicGlobalReduction(ReductionOp.SUM, argument) - assert ("It makes no sense to create an LFRicGlobalReduction object when " + _ = LFRicGlobalSum(argument) + assert ("It makes no sense to create a GlobalReduction object when " "distributed memory is not enabled (dm=False)." in str(err.value)) 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 c2997abb96..eae8beb495 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -3790,7 +3790,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "1: " + gsum + "[SUM, operand='asum']\n" + + indent + "1: " + gsum + "[operand='asum']\n" + indent + "2: " + ompparallel + "[]\n" + 2*indent + sched + "[]\n" + 3*indent + "0: " + ompdo + "[omp_schedule=static]\n" + @@ -3821,7 +3821,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "4: " + gsum + "[SUM, operand='bsum']\n") + indent + "4: " + gsum + "[operand='bsum']\n") if not annexed: expected = expected.replace("nannexed", "ndofs") else: # not dist_mem. annexed can be True or False From 134954d383c2ef98300cf3b345ba5f7394e8b1ec Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 5 Nov 2025 15:24:27 +0000 Subject: [PATCH 15/41] #2381 start adding an LFRicGlobalMin to test new structure --- .../domain/common/psylayer/global_min.py | 6 ++ .../common/psylayer/global_reduction.py | 5 ++ src/psyclone/domain/lfric/lfric_global_min.py | 71 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/psyclone/domain/common/psylayer/global_min.py create mode 100644 src/psyclone/domain/lfric/lfric_global_min.py diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py new file mode 100644 index 0000000000..80d4ad54dc --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -0,0 +1,6 @@ +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction + + +class GlobalMin(GlobalReduction): + ''' + ''' diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 490c3c3392..30bd070d2b 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -48,6 +48,7 @@ from psyclone.psyir.nodes import Statement +# TODO make this virtual class GlobalReduction(Statement): ''' Generic global reduction operation. @@ -81,6 +82,10 @@ def __init__(self, self._operand.call = self super().__init__(kwargs) + def initialise_reduction_variable(self, parent): + ''' + ''' + @property def operand(self) -> Any: ''' 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..80b5c328d5 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -0,0 +1,71 @@ +''' +''' + +from psyclone.domain.common.psylayer.global_min import GlobalMin +from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import (Assignment, Call, Reference, + StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalMin(GlobalMin): + ''' + ''' + def __init__(self, operand, parent=None): + # Check that the global sum argument is indeed a scalar + # TODO can this be moved up to GlobalReduction or are there + # operations that take non-scalar args? + if not operand.is_scalar: + raise InternalError( + f"LFRicGlobalMin.init(): A global reduction argument " + f"should be a scalar but found argument of type " + f"'{operand.argument_type}'.") + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if operand.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalMin currently only supports real scalars, " + f"but argument '{operand.name}' in Kernel " + f"'{operand.call.name}' has '{operand.intrinsic_type}' " + f"intrinsic type.") + # Initialise the parent class + super().__init__(operand, 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._operand.name + type_name = self._operand.data_type + mod_name = self._operand.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) From 72dd101f9eff100b644c115c97e60421150ca40a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 5 Nov 2025 15:26:01 +0000 Subject: [PATCH 16/41] #2381 tidy for lint --- src/psyclone/domain/common/psylayer/global_reduction.py | 1 - src/psyclone/domain/lfric/lfric_global_sum.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 30bd070d2b..8e2b8b1788 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -38,7 +38,6 @@ from __future__ import annotations import copy -from enum import Enum from typing import Any from psyclone.configuration import Config diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 534235800d..7f56948174 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,4 +1,3 @@ -from psyclone.configuration import Config from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.errors import GenerationError, InternalError from psyclone.psyGen import InvokeSchedule From 5a0654a06b901d760432bc535baa418a884f6abc Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Nov 2025 16:33:40 +0000 Subject: [PATCH 17/41] #2381 WIP updating lower_to_language_level [skip-ci] --- .../nodes/data_sharing_attribute_mixin.py | 89 +++++++++++++++++++ src/psyclone/psyir/nodes/omp_directives.py | 64 +++++++------ .../psyir/tools/reduction_inference.py | 15 ++-- .../lfric_transformations_test.py | 44 ++++----- 4 files changed, 150 insertions(+), 62 deletions(-) diff --git a/src/psyclone/psyir/nodes/data_sharing_attribute_mixin.py b/src/psyclone/psyir/nodes/data_sharing_attribute_mixin.py index b294cb5bf0..3c9ee5c64b 100644 --- a/src/psyclone/psyir/nodes/data_sharing_attribute_mixin.py +++ b/src/psyclone/psyir/nodes/data_sharing_attribute_mixin.py @@ -274,3 +274,92 @@ def _should_it_be_fprivate( # If not, it can be just 'private' return False + + def lower_to_language_level(self): + ''' + ''' + # first check whether we have more than one reduction with the same + # name in this Schedule. If so, raise an error as this is not + # supported for a parallel region. + red_names_and_loops = [] + reduction_kernels = self.reductions() + reprod_red_call_list = self.reductions(reprod=True) + import pdb; pdb.set_trace() + for call in reduction_kernels: + name = call.reduction_arg.name + if name in [item[0] for item in red_names_and_loops]: + raise GenerationError( + f"Reduction variables can only be used once in an invoke. " + f"'{name}' is used multiple times, please use a different " + f"reduction variable") + red_names_and_loops.append((name, call.ancestor(Loop))) + + if reduction_kernels: + first_type = type(self.dir_body[0]) + for child in self.dir_body.children: + if first_type != type(child): + raise GenerationError( + "Cannot correctly generate code for an OpenMP parallel" + " region with reductions and containing children of " + "different types.") + + # Lower the first two children + for child in self.children[:2]: + child.lower_to_language_level() + # Create data sharing clauses (order alphabetically to make generation + # reproducible) + private, fprivate, need_sync = self.infer_sharing_attributes() + #if reprod_red_call_list: + # private.add(thread_idx) + + from psyclone.psyir.nodes import ( + OMPDependClause, OMPPrivateClause, OMPFirstprivateClause, + OMPReductionClause) + private_clause = OMPPrivateClause.create( + sorted(private, key=lambda x: x.name)) + fprivate_clause = OMPFirstprivateClause.create( + sorted(fprivate, key=lambda x: x.name)) + # Check all of the need_sync nodes are synchronized in children. + # unless it has reduction_kernels which are handled separately + sync_clauses = self.walk(OMPDependClause) + if not reduction_kernels and need_sync: + for sym in need_sync: + for clause in sync_clauses: + # Needs to be an out depend clause to synchronize + if clause.operator == "in": + continue + # Check if the symbol is in this depend clause. + if sym.name in [child.symbol.name for child in + clause.children]: + break + else: + logger = logging.getLogger(__name__) + logger.warning( + "Lowering '%s' detected a possible race condition for " + "symbol '%s'. Make sure this is a false WaW dependency" + " or the code includes the necessary " + "synchronisations.", type(self).__name__, sym.name) + + self.children[2].replace_with(private_clause) + self.children[3].replace_with(fprivate_clause) + + if reduction_kernels and not reprod_red_call_list: + vam = self.reference_accesses() + from psyclone.psyir.tools.reduction_inference import ( + ReductionInferenceTool) + from psyclone.psyir.transformations.omp_loop_trans import ( + MAP_REDUCTION_OP_TO_OMP) + red_tool = ReductionInferenceTool( + [BinaryOperation.Operator.ADD]) + #import pdb; pdb.set_trace() + for name, _ in red_names_and_loops: + sig = Signature(name) + acc_seq = vam[sig] + clause = red_tool.attempt_reduction(sig, acc_seq) + if clause: + self.children.append( + OMPReductionClause(MAP_REDUCTION_OP_TO_OMP[clause[0]], + children=[clause[1].copy()])) + + return self + diff --git a/src/psyclone/psyir/nodes/omp_directives.py b/src/psyclone/psyir/nodes/omp_directives.py index 3e75b73849..05f33d1537 100644 --- a/src/psyclone/psyir/nodes/omp_directives.py +++ b/src/psyclone/psyir/nodes/omp_directives.py @@ -52,7 +52,7 @@ from typing import List from psyclone.configuration import Config -from psyclone.core import AccessType +from psyclone.core import AccessSequence, AccessType, Signature from psyclone.errors import (GenerationError, UnresolvedDependencyError) from psyclone.psyir.nodes.array_mixin import ArrayMixin @@ -1312,16 +1312,16 @@ def lower_to_language_level(self): # first check whether we have more than one reduction with the same # name in this Schedule. If so, raise an error as this is not # supported for a parallel region. - names = [] + red_names_and_loops = [] reduction_kernels = self.reductions() for call in reduction_kernels: name = call.reduction_arg.name - if name in names: + if name in [item[0] for item in red_names_and_loops]: raise GenerationError( f"Reduction variables can only be used once in an invoke. " f"'{name}' is used multiple times, please use a different " f"reduction variable") - names.append(name) + red_names_and_loops.append((name, call.ancestor(Loop))) if reduction_kernels: first_type = type(self.dir_body[0]) @@ -1400,21 +1400,35 @@ def lower_to_language_level(self): self.children[2].replace_with(private_clause) self.children[3].replace_with(fprivate_clause) + if reduction_kernels and not reprod_red_call_list: + vam = self.reference_accesses() + from psyclone.psyir.tools.reduction_inference import ( + ReductionInferenceTool) + from psyclone.psyir.transformations.omp_loop_trans import ( + MAP_REDUCTION_OP_TO_OMP) + red_tool = ReductionInferenceTool( + [BinaryOperation.Operator.ADD]) + #import pdb; pdb.set_trace() + for name, _ in red_names_and_loops: + sig = Signature(name) + acc_seq = vam[sig] + clause = red_tool.attempt_reduction(sig, acc_seq) + if clause: + self.children.append( + OMPReductionClause(MAP_REDUCTION_OP_TO_OMP[clause[0]], + children=[clause[1].copy()])) + return self - def begin_string(self): + def begin_string(self) -> str: '''Returns the beginning statement of this directive, i.e. "omp parallel". The visitor is responsible for adding the correct directive beginning (e.g. "!$"). :returns: the opening statement of this directive. - :rtype: str ''' result = "omp parallel" - # TODO #514: not yet working with NEMO, so commented out for now - # if not self._reprod: - # result += self._reduction_string() return result @@ -1681,10 +1695,6 @@ def __init__(self, omp_schedule: str = "none", self._omp_schedule = omp_schedule self._collapse = None self.collapse = collapse # Use setter with error checking - # TODO #514 - reductions are only implemented in LFRic, for now we - # store the needed clause when lowering, but this needs a better - # solution - self._lowered_reduction_string = "" self.nowait = nowait @staticmethod @@ -1801,20 +1811,6 @@ def node_str(self, colour=True): parts.append(f"collapse={self._collapse}") return f"{self.coloured_name(colour)}[{','.join(parts)}]" - def _reduction_string(self): - ''' - :returns: the OMP reduction information. - :rtype: str - ''' - for reduction_type in AccessType.get_valid_reduction_modes(): - reductions = self._get_reductions_list(reduction_type) - parts = [] - for reduction in reductions: - parts.append(f"reduction(" - f"{OMP_OPERATOR_MAPPING[reduction_type]}:" - f"{reduction})") - return ", ".join(parts) - @property def omp_schedule(self): ''' @@ -1933,8 +1929,8 @@ def lower_to_language_level(self): :rtype: :py:class:`psyclone.psyir.node.Node` ''' - self._lowered_reduction_string = self._reduction_string() - return super().lower_to_language_level() + DataSharingAttributeMixin.lower_to_language_level(self) + super().lower_to_language_level() def begin_string(self): '''Returns the beginning statement of this directive, i.e. @@ -1950,8 +1946,8 @@ def begin_string(self): string += f" schedule({self.omp_schedule})" if self._collapse: string += f" collapse({self._collapse})" - if self._lowered_reduction_string: - string += f" {self._lowered_reduction_string}" + #if self._lowered_reduction_string: + # string += f" {self._lowered_reduction_string}" return string def end_string(self): @@ -2045,7 +2041,7 @@ def lower_to_language_level(self): ''' # Calling the super() explicitly to avoid confusion # with the multiple-inheritance - self._lowered_reduction_string = self._reduction_string() + #self._lowered_reduction_string = self._reduction_string() OMPParallelDirective.lower_to_language_level(self) self.children[4].replace_with(OMPScheduleClause(self._omp_schedule)) @@ -2063,8 +2059,8 @@ def begin_string(self): string = f"omp {self._directive_string}" if self._collapse: string += f" collapse({self._collapse})" - if self._lowered_reduction_string: - string += f" {self._lowered_reduction_string}" + #if self._lowered_reduction_string: + # string += f" {self._lowered_reduction_string}" return string def end_string(self): diff --git a/src/psyclone/psyir/tools/reduction_inference.py b/src/psyclone/psyir/tools/reduction_inference.py index da7f743d25..4dee2ed6bf 100644 --- a/src/psyclone/psyir/tools/reduction_inference.py +++ b/src/psyclone/psyir/tools/reduction_inference.py @@ -61,17 +61,18 @@ def _get_reduction_operator(self, node: Node) -> \ Union[BinaryOperation.Operator, IntrinsicCall.Intrinsic]: ''' :param node: the node to match against. + :returns: the reduction operator at the root of the given - DataNode or None if there isn't one. + DataNode or None if there isn't one. ''' if isinstance(node, BinaryOperation): - for op in self.red_ops: - if node.operator == op: - return node.operator + if node.operator in self.red_ops: + return node.operator + if isinstance(node, IntrinsicCall): - for op in self.red_ops: - if node.intrinsic == op: - return node.intrinsic + if node.intrinsic in self.red_ops: + return node.intrinsic + return None @staticmethod 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 eae8beb495..beb2ac5342 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -1905,8 +1905,8 @@ def test_reduction_real_pdo(tmpdir, dist_mem): if dist_mem: assert "loop0_stop = f1_proxy%vspace%get_last_dof_owned()" in code assert ( - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -1920,8 +1920,8 @@ def test_reduction_real_pdo(tmpdir, dist_mem): else: assert "loop0_stop = undf_aspc1_f1" in code assert ( - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -1950,8 +1950,9 @@ def test_reduction_real_do(tmpdir, dist_mem): if dist_mem: assert "loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n" in code assert ( - " !$omp parallel default(shared) private(df)\n" - " !$omp do schedule(static) reduction(+:asum)\n" + " !$omp parallel default(shared) private(df) " + "reduction(+: asum)\n" + " !$omp do schedule(static)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -1965,8 +1966,9 @@ def test_reduction_real_do(tmpdir, dist_mem): else: assert "loop0_stop = undf_aspc1_f1\n" in code assert ( - " !$omp parallel default(shared) private(df)\n" - " !$omp do schedule(static) reduction(+:asum)\n" + " !$omp parallel default(shared) private(df) " + "reduction(+: asum)\n" + " !$omp do schedule(static)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -2000,8 +2002,8 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem): " asum = 0.0_r_def\n" "\n" " ! Call kernels and communication routines\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -2014,8 +2016,8 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem): "\n" " ! Zero summation variables\n" " asum = 0.0_r_def\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop1_start, loop1_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -2032,8 +2034,8 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem): " asum = 0.0_r_def\n" "\n" " ! Call kernels\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop0_start, loop0_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -2042,8 +2044,8 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem): "\n" " ! Zero summation variables\n" " asum = 0.0_r_def\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) private(df) " + "schedule(static) reduction(+: asum)\n" " do df = loop1_start, loop1_stop, 1\n" " ! Built-in: X_innerproduct_Y (real-valued fields)\n" " asum = asum + f1_data(df) * f2_data(df)\n" @@ -2131,7 +2133,7 @@ def test_reduction_after_normal_real_do(tmpdir, monkeypatch, annexed, " f1_data(df) = bvalue * f1_data(df)\n" " enddo\n" " !$omp end do\n" - " !$omp do schedule(static) reduction(+:asum)\n" + " !$omp do schedule(static) reduction(+: asum)\n" " do df = loop1_start, loop1_stop, 1\n" " ! Built-in: sum_X (sum a real-valued field)\n" " asum = asum + f1_data(df)\n" @@ -2969,8 +2971,8 @@ def test_multi_builtins_usual_then_red_pdo(tmpdir, monkeypatch, annexed, "\n" " ! Zero summation variables\n" " asum = 0.0_r_def\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop1_start, loop1_stop, 1\n" " ! Built-in: sum_X (sum a real-valued field)\n" " asum = asum + f1_data(df)\n" @@ -2995,8 +2997,8 @@ def test_multi_builtins_usual_then_red_pdo(tmpdir, monkeypatch, annexed, "\n" " ! Zero summation variables\n" " asum = 0.0_r_def\n" - " !$omp parallel do reduction(+:asum) default(shared) " - "private(df) schedule(static)\n" + " !$omp parallel do default(shared) " + "private(df) schedule(static) reduction(+: asum)\n" " do df = loop1_start, loop1_stop, 1\n" " ! Built-in: sum_X (sum a real-valued field)\n" " asum = asum + f1_data(df)\n" From 9fc397b5764ace895598005bab2f1eddbd428bda Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 12 Dec 2025 12:53:03 +0000 Subject: [PATCH 18/41] #2381 flesh out LFRicGlobalMin --- .../common/psylayer/global_reduction.py | 16 +++- src/psyclone/domain/lfric/lfric_global_min.py | 81 +++++++------------ .../domain/lfric/lfric_global_min_test.py | 77 ++++++++++++++++++ 3 files changed, 122 insertions(+), 52 deletions(-) create mode 100644 src/psyclone/tests/domain/lfric/lfric_global_min_test.py diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 8e2b8b1788..e44d7e804f 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -42,7 +42,7 @@ from psyclone.configuration import Config from psyclone.core import AccessType -from psyclone.errors import GenerationError +from psyclone.errors import GenerationError, InternalError from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes import Statement @@ -79,6 +79,20 @@ def __init__(self, # accesses/updates a scalar self._operand.access = AccessType.READWRITE self._operand.call = self + # Check that the global reduction argument is indeed a scalar + if not operand.is_scalar: + raise InternalError( + f"GlobalReduction.init(): A global reduction argument " + f"should be a scalar but found argument of type " + f"'{operand.argument_type}'.") + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if operand.intrinsic_type != "real": + raise GenerationError( + f"GlobalReduction currently only supports real scalars, " + f"but argument '{operand.name}' in Kernel " + f"'{operand.call.name}' has '{operand.intrinsic_type}' " + f"intrinsic type.") super().__init__(kwargs) def initialise_reduction_variable(self, parent): diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 80b5c328d5..98a7a12317 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -2,70 +2,49 @@ ''' from psyclone.domain.common.psylayer.global_min import GlobalMin -from psyclone.errors import GenerationError, InternalError from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Reference, +from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, StructureReference) from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - UnresolvedType) + ContainerSymbol, DataSymbol, ImportInterface, + REAL_TYPE, UnresolvedType) class LFRicGlobalMin(GlobalMin): ''' ''' - def __init__(self, operand, parent=None): - # Check that the global sum argument is indeed a scalar - # TODO can this be moved up to GlobalReduction or are there - # operations that take non-scalar args? - if not operand.is_scalar: - raise InternalError( - f"LFRicGlobalMin.init(): A global reduction argument " - f"should be a scalar but found argument of type " - f"'{operand.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if operand.intrinsic_type != "real": - raise GenerationError( - f"LFRicGlobalMin currently only supports real scalars, " - f"but argument '{operand.name}' in Kernel " - f"'{operand.call.name}' has '{operand.intrinsic_type}' " - f"intrinsic type.") - # Initialise the parent class - super().__init__(operand, parent=parent) - - def lower_to_language_level(self): + def lower_to_language_level(self) -> Node: ''' :returns: this node lowered to language-level PSyIR. - :rtype: :py:class:`psyclone.psyir.nodes.Node` - ''' + ''' # Get the name strings to use name = self._operand.name - type_name = self._operand.data_type - mod_name = self._operand.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) + # We'll need the global LFRic mpi object. + mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", + symbol_type=ContainerSymbol) + mpi_type = symtab.find_or_create_tag( + "global_mpi", + symbol_type=DataSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(mpi_mod)) + + # Symbol holding the local minimum value. + loc_min = symtab.lookup(name) + + # Symbol holding the global minimum value. + result = symtab.new_symbol("glob_min", symbol_type=DataSymbol, + # TODO - get correct type. + datatype=REAL_TYPE) + sref = StructureReference.create(mpi_type, ["global_min"]) + + # Call the method to compute the global min. + call = Call.create(sref, [Reference(loc_min), Reference(result)]) + call.preceding_comment = "Perform global min" + self.parent.addchild(call, self.position) + assign = Assignment.create(lhs=Reference(loc_min), + rhs=Reference(result)) + return self.replace_with(assign) 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..b4f80a9922 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -0,0 +1,77 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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. +# ----------------------------------------------------------------------------- +# Author A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''Module containing pytest tests for the LFRicGlobalMin class.''' + +from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin +from psyclone.psyGen import Kern +from psyclone.tests.utilities import get_invoke + +TEST_API = "lfric" + + +def test_lgm_in_invoke(): + ''' + Test the construction of an LFRicGlobalMin object. + + This is complicated by the need to supply it with an LFRicKernelArgument + and therefore we use a full example to get hold of a suitable argument. + ''' + psy, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", + TEST_API, dist_mem=True, + idx=0) + sched = invoke.schedule + + # Find a suitable kernel argument (real scalar). + kernel = sched.walk(Kern)[0] + for arg in kernel.args: + if arg.is_scalar and arg.intrinsic_type == "real": + break + + lgm = LFRicGlobalMin(operand=arg) + assert isinstance(lgm, LFRicGlobalMin) + assert lgm.operand is not arg + assert lgm.operand.name == arg.name + + sched.addchild(lgm) + output = psy.gen + assert "use lfric_mpi_mod, only : global_mpi" in output + # TODO correct type/precision + assert "real :: glob_min" in output + assert '''\ + ! Perform global min + call global_mpi%global_min(a, glob_min) + a = glob_min''' in output From 052334626ca85861cc573612b318506b2ca2ac30 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 12 Dec 2025 13:32:33 +0000 Subject: [PATCH 19/41] #2381 make dag_name more general --- src/psyclone/domain/common/psylayer/global_reduction.py | 2 +- .../tests/domain/common/psylayer/global_reduction_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index e44d7e804f..1d4ce619bd 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -111,7 +111,7 @@ def dag_name(self) -> str: ''' :returns: the name to use in the DAG for this node. ''' - return (f"globalreduction({self._operand.name})" + return (f"{type(self).__name__}({self._operand.name})" f"_{self.position}") @property diff --git a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index 66dcd5904d..fabf2b99f8 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -78,7 +78,7 @@ def test_globalreduction_dag_name(): Test the dag_name property. ''' gred = GlobalReduction(operand=Reference(Symbol("a"))) - assert gred.dag_name == "globalreduction(a)_0" + assert gred.dag_name == "GlobalReduction(a)_0" def test_globalreduction_node_str(): From 4bceba7cbbc18f274e00ffc6a1cc8dfe1f0a7e00 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 15 Dec 2025 09:48:17 +0000 Subject: [PATCH 20/41] #2381 mv common init functionality to parent class --- .../common/psylayer/global_reduction.py | 9 +- src/psyclone/domain/lfric/lfric_global_min.py | 23 +++- .../domain/lfric/lfric_global_reduction.py | 127 ------------------ src/psyclone/domain/lfric/lfric_global_sum.py | 19 --- .../domain/lfric/lfric_global_min_test.py | 6 +- 5 files changed, 26 insertions(+), 158 deletions(-) delete mode 100644 src/psyclone/domain/lfric/lfric_global_reduction.py diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 1d4ce619bd..f358607a46 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -73,6 +73,7 @@ def __init__(self, # a KernelArgument, not a PSyIR Node. # TODO Without this `copy`, the tests for the old-style DA fail. self._operand = copy.copy(operand) + if isinstance(operand, KernelArgument): # Add old-style dependency information # Here "readwrite" denotes how the class GlobalSum @@ -82,15 +83,15 @@ def __init__(self, # Check that the global reduction argument is indeed a scalar if not operand.is_scalar: raise InternalError( - f"GlobalReduction.init(): A global reduction argument " - f"should be a scalar but found argument of type " + f"{type(self).__name__}.init(): A global reduction " + f"argument should be a scalar but found argument of type " f"'{operand.argument_type}'.") # Check scalar intrinsic types that this class supports (only # "real" for now) if operand.intrinsic_type != "real": raise GenerationError( - f"GlobalReduction currently only supports real scalars, " - f"but argument '{operand.name}' in Kernel " + f"{type(self).__name__} currently only supports real " + f"scalars, but argument '{operand.name}' in Kernel " f"'{operand.call.name}' has '{operand.intrinsic_type}' " f"intrinsic type.") super().__init__(kwargs) diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 98a7a12317..566ffea759 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -6,7 +6,7 @@ from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, StructureReference) from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, ImportInterface, + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, REAL_TYPE, UnresolvedType) @@ -23,15 +23,16 @@ def lower_to_language_level(self) -> Node: symtab = self.ancestor(InvokeSchedule).symbol_table - # We'll need the global LFRic mpi object. + # We'll need the LFRic mpi_type. mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", symbol_type=ContainerSymbol) mpi_type = symtab.find_or_create_tag( - "global_mpi", - symbol_type=DataSymbol, + "lfric_mpi_type", + symbol_type=DataTypeSymbol, datatype=UnresolvedType(), interface=ImportInterface(mpi_mod)) - + mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, + datatype=mpi_type) # Symbol holding the local minimum value. loc_min = symtab.lookup(name) @@ -39,9 +40,19 @@ def lower_to_language_level(self) -> Node: result = symtab.new_symbol("glob_min", symbol_type=DataSymbol, # TODO - get correct type. datatype=REAL_TYPE) - sref = StructureReference.create(mpi_type, ["global_min"]) + + # Obtain a suitable mpi object from one of the field arguments. + for sym in symtab.datasymbols: + if (isinstance(sym.datatype, DataTypeSymbol) and + sym.datatype.name == "field_type"): + break + get_mpi = StructureReference.create(sym, ["get_mpi"]) + self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), + rhs=Call.create(get_mpi)), + index=0) # Call the method to compute the global min. + sref = StructureReference.create(mpi_obj, ["global_min"]) call = Call.create(sref, [Reference(loc_min), Reference(result)]) call.preceding_comment = "Perform global min" self.parent.addchild(call, self.position) diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py deleted file mode 100644 index 2547cdd4bb..0000000000 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ /dev/null @@ -1,127 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2017-2025, 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 and S. Siso, STFC Daresbury Lab -# Modified I. Kavcic, A. Coughtrie, L. Turner and O. Brunt, Met Office -# Modified J. Henrichs, Bureau of Meteorology -# Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''This module contains the LFRicGlobalReduction implementation.''' - -from psyclone.configuration import Config -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.errors import GenerationError, InternalError -from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Reference, - StructureReference) -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - UnresolvedType) - - -class LFRicGlobalReduction(GlobalReduction): - ''' - LFRic specific global-reduction class which can be added to and - manipulated in a schedule. - - :param operation: the type of global reduction to perform. - :param operand: the kernel argument for which to perform a global - reduction. - :type operand: :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 operand is not of "real" intrinsic type. - - ''' - def __init__(self, operation, operand, parent=None): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - "It makes no sense to create an LFRicGlobalReduction object " - "when distributed memory is not enabled (dm=False).") - # Check that the global sum argument is indeed a scalar - if not operand.is_scalar: - raise InternalError( - f"LFRicGlobalReduction.init(): A global reduction argument " - f"should be a scalar but found argument of type " - f"'{operand.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if operand.intrinsic_type != "real": - raise GenerationError( - f"LFRicGlobalReduction currently only supports real scalars, " - f"but argument '{operand.name}' in Kernel " - f"'{operand.call.name}' has '{operand.intrinsic_type}' " - f"intrinsic type.") - # Initialise the parent class - super().__init__(operation, operand, 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._operand.name - type_name = self._operand.data_type - mod_name = self._operand.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_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 7f56948174..5fadfbadb0 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,5 +1,4 @@ from psyclone.domain.common.psylayer.global_sum import GlobalSum -from psyclone.errors import GenerationError, InternalError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import (Assignment, Call, Reference, StructureReference) @@ -11,24 +10,6 @@ class LFRicGlobalSum(GlobalSum): ''' ''' - def __init__(self, operand, parent=None): - # Check that the global sum argument is indeed a scalar - if not operand.is_scalar: - raise InternalError( - f"LFRicGlobalSum.init(): A global reduction argument " - f"should be a scalar but found argument of type " - f"'{operand.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if operand.intrinsic_type != "real": - raise GenerationError( - f"LFRicGlobalSum currently only supports real scalars, " - f"but argument '{operand.name}' in Kernel " - f"'{operand.call.name}' has '{operand.intrinsic_type}' " - f"intrinsic type.") - # Initialise the parent class - super().__init__(operand, parent=parent) - def lower_to_language_level(self): ''' :returns: this node lowered to language-level PSyIR. diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py index b4f80a9922..4e372b9b21 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -68,10 +68,12 @@ def test_lgm_in_invoke(): sched.addchild(lgm) output = psy.gen - assert "use lfric_mpi_mod, only : global_mpi" in output + assert "use lfric_mpi_mod, only : lfric_mpi_type" in output + assert "type(lfric_mpi_type) :: mpi" in output # TODO correct type/precision assert "real :: glob_min" in output + assert "mpi = f1%get_mpi()" in output assert '''\ ! Perform global min - call global_mpi%global_min(a, glob_min) + call mpi%global_min(a, glob_min) a = glob_min''' in output From 438e40530220bfb395235098da6ded1487cb4fc7 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Dec 2025 10:42:48 +0000 Subject: [PATCH 21/41] #2381 add MIN and MAX AccessTypes and appropriate init in Kern --- src/psyclone/core/access_type.py | 4 ++++ src/psyclone/psyGen.py | 24 ++++++++++++++++++---- src/psyclone/psyir/nodes/omp_directives.py | 6 +++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/psyclone/core/access_type.py b/src/psyclone/core/access_type.py index 4839523e60..5d47d90bbb 100644 --- a/src/psyclone/core/access_type.py +++ b/src/psyclone/core/access_type.py @@ -75,6 +75,10 @@ class AccessType(Enum): #: is available at compile-time and can be used for type properties such #: as kinds or dimensions. CONSTANT = 10 + #: Is the output of a MIN reduction (i.e. global minimum value). + MIN = 11 + #: Is the output of a MAX reduction (i.e. global maximum value). + MAX = 12 def __str__(self) -> str: '''Convert to a string representation, returning just the diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 8381ab48e3..a00af152b2 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -63,7 +63,7 @@ from psyclone.psyir.nodes import ( ArrayReference, Call, Container, Literal, Loop, Node, OMPDoDirective, Reference, Directive, Routine, Schedule, Statement, Assignment, - IntrinsicCall, BinaryOperation, FileContainer) + IntrinsicCall, BinaryOperation, FileContainer, UnaryOperation) from psyclone.psyir.symbols import ( ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, ScalarType, UnresolvedType, ImportInterface, INTEGER_TYPE, RoutineSymbol) @@ -1011,6 +1011,24 @@ def initialise_reduction_variable(self) -> None: f"'real' or an 'integer' scalar but found scalar of type " f"'{var_arg.intrinsic_type}'.") + # Create the initialisation expression - this depends upon the type + # of reduction being performed. + if var_arg.access == AccessType.SUM: + init_val = Literal("0", variable.datatype.copy()) + elif var_arg.access == AccessType.MIN: + init_val = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, + Reference(variable)) + elif var_arg.access == AccessType.MAX: + huge = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, + Reference(variable)) + init_val = UnaryOperation.create(UnaryOperation.Operator.MINUS, + huge) + else: + raise GenerationError( + f"Kernel '{self.name}' performs a reduction of type " + f"'{var_arg.access}' but this is not supported by Kern." + f"initialise_reduction_variable()") + # Find a safe location to initialise it. insert_loc = self.ancestor((Loop, Directive)) while insert_loc: @@ -1020,9 +1038,7 @@ def initialise_reduction_variable(self) -> None: 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 = Assignment.create(lhs=Reference(variable), rhs=init_val) new_node.append_preceding_comment("Initialise reduction variable") insert_loc.addchild(new_node, cursor) cursor += 1 diff --git a/src/psyclone/psyir/nodes/omp_directives.py b/src/psyclone/psyir/nodes/omp_directives.py index c177318cc0..536ffb4382 100644 --- a/src/psyclone/psyir/nodes/omp_directives.py +++ b/src/psyclone/psyir/nodes/omp_directives.py @@ -1307,16 +1307,16 @@ def lower_to_language_level(self) -> Node: # first check whether we have more than one reduction with the same # name in this Schedule. If so, raise an error as this is not # supported for a parallel region. - red_names_and_loops = [] + names = [] reduction_kernels = self.reductions() for call in reduction_kernels: name = call.reduction_arg.name - if name in [item[0] for item in red_names_and_loops]: + if name in names: raise GenerationError( f"Reduction variables can only be used once in an invoke. " f"'{name}' is used multiple times, please use a different " f"reduction variable") - red_names_and_loops.append((name, call.ancestor(Loop))) + names.append(name) if reduction_kernels: first_type = type(self.dir_body[0]) From 3d53e98d2b79d839aa3228fc865f7c3ae03ef646 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Dec 2025 10:44:07 +0000 Subject: [PATCH 22/41] #2381 tidy-up the reduction classes --- .../domain/common/psylayer/global_min.py | 3 +++ .../domain/common/psylayer/global_reduction.py | 16 +++++++++------- .../domain/common/psylayer/global_sum.py | 1 + src/psyclone/domain/lfric/lfric_global_sum.py | 9 +++++---- .../lfric_transformations_test.py | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py index 80d4ad54dc..4830d22538 100644 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -3,4 +3,7 @@ class GlobalMin(GlobalReduction): ''' + Represents a reduction to compute the global minimum value of a scalar. + ''' + _text_name = "GlobalMin" diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index f358607a46..7f77502f7b 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -37,6 +37,7 @@ ''' This module contains the GlobalReduction node implementation.''' from __future__ import annotations +from abc import abstractmethod import copy from typing import Any @@ -44,10 +45,9 @@ 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 import Node, Statement -# TODO make this virtual class GlobalReduction(Statement): ''' Generic global reduction operation. @@ -96,10 +96,6 @@ def __init__(self, f"intrinsic type.") super().__init__(kwargs) - def initialise_reduction_variable(self, parent): - ''' - ''' - @property def operand(self) -> Any: ''' @@ -133,6 +129,12 @@ def node_str(self, colour: bool = True) -> str: :returns: description of this node, possibly coloured. ''' - # TODO move this to sub-classes (e.g. GlobalSum) return (f"{self.coloured_name(colour)}[" f"operand='{self._operand.name}']") + + @abstractmethod + def lower_to_language_level(self) -> Node: + ''' + Creates language-level PSyIR for this node and returns it. + + ''' diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py index d02b06ff46..baef4b6e1a 100644 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -4,3 +4,4 @@ class GlobalSum(GlobalReduction): ''' ''' + _text_name = "GlobalSum" diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 5fadfbadb0..0a6847d3ff 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,6 +1,6 @@ from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Reference, +from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, StructureReference) from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, @@ -9,13 +9,14 @@ class LFRicGlobalSum(GlobalSum): ''' + Represents a global sum in the LFRic DSL. + ''' - def lower_to_language_level(self): + def lower_to_language_level(self) -> Node: ''' :returns: this node lowered to language-level PSyIR. - :rtype: :py:class:`psyclone.psyir.nodes.Node` - ''' + ''' # Get the name strings to use name = self._operand.name type_name = self._operand.data_type 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 8646330c24..3e5ae68083 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -3839,7 +3839,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("GlobalReduction", GlobalReduction._colour) + gsum = colored("GlobalSum", GlobalReduction._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) From b460b3c6d99d3e82dbc252c54b03bf1d221b3f58 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Dec 2025 14:31:40 +0000 Subject: [PATCH 23/41] #2381 fix failing test --- .../domain/common/psylayer/global_min.py | 40 +++++++++++++++++++ .../domain/common/psylayer/global_sum.py | 39 ++++++++++++++++++ src/psyclone/psyGen.py | 7 +++- src/psyclone/tests/psyGen_test.py | 4 ++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py index 4830d22538..4c3f595087 100644 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -1,3 +1,43 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''Python module definining the GlobalMin subclass of GlobalReduction. + +''' + from psyclone.domain.common.psylayer.global_reduction import GlobalReduction diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py index baef4b6e1a..3796964fee 100644 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -1,3 +1,42 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''Python module definining the GlobalSum subclass of GlobalReduction. + +''' from psyclone.domain.common.psylayer.global_reduction import GlobalReduction diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index a00af152b2..2d0d0d95f5 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1014,13 +1014,16 @@ def initialise_reduction_variable(self) -> None: # Create the initialisation expression - this depends upon the type # of reduction being performed. if var_arg.access == AccessType.SUM: + # Sum - initialise to zero. init_val = Literal("0", variable.datatype.copy()) elif var_arg.access == AccessType.MIN: + # Minimum - initialise to HUGE. init_val = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, - Reference(variable)) + [Reference(variable)]) elif var_arg.access == AccessType.MAX: + # Maximum - initialise to -HUGE. huge = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, - Reference(variable)) + [Reference(variable)]) init_val = UnaryOperation.create(UnaryOperation.Operator.MINUS, huge) else: diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 8080cf913f..b33e694c24 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -1103,9 +1103,13 @@ def test_reduction_var_invalid_scalar_error(dist_mem): # REALs and INTEGERs are fine assert call.arguments.args[0].intrinsic_type == 'real' call._reduction_arg = call.arguments.args[0] + # Have to pretend this arg has a reduction access. + call._reduction_arg._access = AccessType.MIN call.initialise_reduction_variable() assert call.arguments.args[6].intrinsic_type == 'integer' call._reduction_arg = call.arguments.args[6] + # Have to pretend this arg has a reduction access. + call._reduction_arg._access = AccessType.MAX call.initialise_reduction_variable() From c475f0c3a43970b3b3f9404fff436ffea36c4a27 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Dec 2025 16:25:58 +0000 Subject: [PATCH 24/41] #2381 WIP fixing tests after extending access-modes [skip ci] --- config/psyclone.cfg | 3 +- src/psyclone/configuration.py | 2 +- src/psyclone/core/access_type.py | 6 +- .../domain/common/psylayer/global_max.py | 49 +++++++++++++++ .../domain/lfric/kernel/common_metadata.py | 2 +- .../domain/lfric/lfric_arg_descriptor.py | 2 +- src/psyclone/domain/lfric/lfric_constants.py | 8 ++- src/psyclone/domain/lfric/lfric_global_max.py | 61 +++++++++++++++++++ src/psyclone/domain/lfric/lfric_invoke.py | 13 ++-- src/psyclone/tests/core/access_type_test.py | 12 ++-- .../kernel/evaluator_targets_metadata_test.py | 9 +-- .../lfric/kernel/field_arg_metadata_test.py | 25 +++----- .../kernel/field_vector_arg_metadata_test.py | 2 +- .../kernel/lfric_kernel_metadata_test.py | 14 ++--- .../lfric/kernel/scalar_arg_metadata_test.py | 4 +- .../kernel/scalar_array_arg_metadata_test.py | 4 +- .../domain/lfric/lfric_scalar_mdata_test.py | 12 ++-- 17 files changed, 171 insertions(+), 57 deletions(-) create mode 100644 src/psyclone/domain/common/psylayer/global_max.py create mode 100644 src/psyclone/domain/lfric/lfric_global_max.py diff --git a/config/psyclone.cfg b/config/psyclone.cfg index 8b7355c864..1f11818c56 100644 --- a/config/psyclone.cfg +++ b/config/psyclone.cfg @@ -64,7 +64,8 @@ FORTRAN_STANDARD = f2008 # ================================== [lfric] access_mapping = gh_read: read, gh_write: write, gh_readwrite: readwrite, - gh_inc: inc, gh_readinc: readinc, gh_sum: sum + gh_inc: inc, gh_readinc: readinc, gh_sum: sum, + gh_min: min, gh_max: max # Specify whether we compute annexed dofs when a kernel is written so # that it iterates over dofs. This is currently only the case for diff --git a/src/psyclone/configuration.py b/src/psyclone/configuration.py index 0647a84dbd..9902281543 100644 --- a/src/psyclone/configuration.py +++ b/src/psyclone/configuration.py @@ -815,7 +815,7 @@ def __init__(self, section): # having to specify those mappings. self._access_mapping = {"read": "read", "write": "write", "readwrite": "readwrite", "inc": "inc", - "sum": "sum"} + "sum": "sum", "min": "min", "max": "max"} # Get the mapping if one exists and convert it into a # dictionary. The input is in the format: key1:value1, # key2=value2, ... diff --git a/src/psyclone/core/access_type.py b/src/psyclone/core/access_type.py index 5d47d90bbb..0194cc2b73 100644 --- a/src/psyclone/core/access_type.py +++ b/src/psyclone/core/access_type.py @@ -37,6 +37,7 @@ '''This module implements the AccessType used throughout PSyclone.''' +from __future__ import annotations from enum import Enum from psyclone.configuration import Config @@ -136,12 +137,11 @@ def all_read_accesses(): AccessType.READINC] @staticmethod - def get_valid_reduction_modes(): + def get_valid_reduction_modes() -> list[AccessType]: ''' :returns: A list of valid reduction access modes. - :rtype: List of py:class:`psyclone.core.access_type.AccessType`. ''' - return [AccessType.SUM] + return [AccessType.SUM, AccessType.MIN, AccessType.MAX] @staticmethod def get_valid_reduction_names(): diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py new file mode 100644 index 0000000000..3051d791a2 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_max.py @@ -0,0 +1,49 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''Python module definining the GlobalMax subclass of GlobalReduction. + +''' + +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction + + +class GlobalMax(GlobalReduction): + ''' + Represents a reduction to compute the global minimum value of a scalar. + + ''' + _text_name = "GlobalMax" diff --git a/src/psyclone/domain/lfric/kernel/common_metadata.py b/src/psyclone/domain/lfric/kernel/common_metadata.py index b74c1424bc..1c6471295e 100644 --- a/src/psyclone/domain/lfric/kernel/common_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_metadata.py @@ -95,7 +95,7 @@ def validate_scalar_value(value, valid_values, name): if value.lower() not in valid_values: raise ValueError( f"The '{name}' metadata should be a recognised " - f"value (one of {valid_values}) " + f"value (one of {sorted(valid_values)}) " f"but found '{value}'.") @staticmethod diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index 3597695e09..44eef75f3a 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -628,7 +628,7 @@ def _init_scalar(self, arg_type): rev_access_mapping = api_config.get_reverse_access_mapping() if self._access_type not in scalar_accesses: api_specific_name = rev_access_mapping[self._access_type] - valid_reductions = AccessType.get_valid_reduction_names() + valid_reductions = sorted(AccessType.get_valid_reduction_names()) raise ParseError( f"In the LFRic API scalar arguments must have read-only " f"('gh_read') or a reduction {valid_reductions} access but " diff --git a/src/psyclone/domain/lfric/lfric_constants.py b/src/psyclone/domain/lfric/lfric_constants.py index ccdb6a636a..79a8e11269 100644 --- a/src/psyclone/domain/lfric/lfric_constants.py +++ b/src/psyclone/domain/lfric/lfric_constants.py @@ -114,8 +114,9 @@ def __init__(self) -> None: # pylint: disable=too-many-instance-attributes # Supported access types - # gh_sum for scalars is restricted to iterates_over == 'dof' - LFRicConstants.VALID_SCALAR_ACCESS_TYPES = ["gh_read", "gh_sum"] + # gh_sum/min/max for scalars is restricted to iterates_over == 'dof' + LFRicConstants.VALID_SCALAR_ACCESS_TYPES = ["gh_read", "gh_sum", + "gh_min", "gh_max"] LFRicConstants.VALID_ARRAY_ACCESS_TYPES = ["gh_read"] LFRicConstants.VALID_FIELD_ACCESS_TYPES = [ "gh_read", "gh_write", "gh_readwrite", "gh_inc", "gh_readinc"] @@ -125,7 +126,8 @@ def __init__(self) -> None: "gh_read", "gh_write", "gh_readwrite", "gh_inc", "gh_readinc"] LFRicConstants.WRITE_ACCESSES = [ - "gh_write", "gh_readwrite", "gh_inc", "gh_readinc", "gh_sum"] + "gh_write", "gh_readwrite", "gh_inc", "gh_readinc", "gh_sum", + "gh_min", "gh_max"] # Supported LFRic API stencil types and directions LFRicConstants.VALID_STENCIL_TYPES = ["x1d", "y1d", "xory1d", "cross", 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..68a5f42d62 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -0,0 +1,61 @@ +''' +''' + +from psyclone.domain.common.psylayer.global_max import GlobalMax +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, + StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + REAL_TYPE, UnresolvedType) + + +class LFRicGlobalMax(GlobalMax): + ''' + ''' + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + + ''' + # Get the name strings to use + name = self._operand.name + + symtab = self.ancestor(InvokeSchedule).symbol_table + + # We'll need the LFRic mpi_type. + mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", + symbol_type=ContainerSymbol) + mpi_type = symtab.find_or_create_tag( + "lfric_mpi_type", + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(mpi_mod)) + mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, + datatype=mpi_type) + # Symbol holding the local maximum value. + loc_min = symtab.lookup(name) + + # Symbol holding the global maximum value. + result = symtab.new_symbol("glob_min", symbol_type=DataSymbol, + # TODO - get correct type. + datatype=REAL_TYPE) + + # Obtain a suitable mpi object from one of the field arguments. + for sym in symtab.datasymbols: + if (isinstance(sym.datatype, DataTypeSymbol) and + sym.datatype.name == "field_type"): + break + get_mpi = StructureReference.create(sym, ["get_mpi"]) + self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), + rhs=Call.create(get_mpi)), + index=0) + + # Call the method to compute the global min. + sref = StructureReference.create(mpi_obj, ["global_max"]) + call = Call.create(sref, [Reference(loc_min), Reference(result)]) + call.preceding_comment = "Perform global max" + self.parent.addchild(call, self.position) + assign = Assignment.create(lhs=Reference(loc_min), + rhs=Reference(result)) + return self.replace_with(assign) diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 2227b9cc55..3e924fd8d7 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -94,6 +94,8 @@ def __init__(self, alg_invocation, idx, invokes): LFRicRunTimeChecks, LFRicScalarArgs, LFRicFields, LFRicDofmaps, LFRicStencils) from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum + from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin + from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax self.scalar_args = LFRicScalarArgs(self) @@ -172,20 +174,23 @@ def __init__(self, alg_invocation, idx, invokes): # Lastly, add in halo exchange calls and global sums if # required. We only need to add halo exchange calls for fields # since operators are assembled in place and scalars don't - # have halos. We only need to add global sum calls for scalars - # which have a 'gh_sum' access. + # have halos. We only need to add global reductions for scalars + # which have a 'gh_sum/max/min' access. + rmap = {AccessType.SUM: LFRicGlobalSum, + AccessType.MAX: LFRicGlobalMax, + AccessType.MIN: LFRicGlobalMin} if Config.get().distributed_memory: # halo exchange calls const = LFRicConstants() for loop in self.schedule.loops(): loop.create_halo_exchanges() - # global sum calls + # global reduction calls for loop in self.schedule.loops(): for scalar in loop.args_filter( arg_types=const.VALID_SCALAR_NAMES, arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): - global_sum = LFRicGlobalSum( + global_sum = rmap[scalar.access]( scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) diff --git a/src/psyclone/tests/core/access_type_test.py b/src/psyclone/tests/core/access_type_test.py index f1cb1ca2a6..afcde623ae 100644 --- a/src/psyclone/tests/core/access_type_test.py +++ b/src/psyclone/tests/core/access_type_test.py @@ -72,14 +72,18 @@ def test_api_specific_name(): assert AccessType.INQUIRY.api_specific_name() == "inquiry" assert AccessType.CONSTANT.api_specific_name() == "constant" assert AccessType.UNKNOWN.api_specific_name() == "unknown" - assert AccessType.get_valid_reduction_modes() == [AccessType.SUM] - assert AccessType.get_valid_reduction_names() == ["gh_sum"] + assert (set(AccessType.get_valid_reduction_modes()) == + set([AccessType.SUM, AccessType.MIN, AccessType.MAX])) + assert (set(AccessType.get_valid_reduction_names()) == + set(["gh_sum", "gh_min", "gh_max"])) # Use set to make this independent of the order: assert set(AccessType.all_write_accesses()) == set([AccessType.WRITE, AccessType.READWRITE, AccessType.INC, AccessType.READINC, - AccessType.SUM]) + AccessType.SUM, + AccessType.MAX, + AccessType.MIN]) assert set(AccessType.all_read_accesses()) == set([AccessType.READ, AccessType.READWRITE, AccessType.READINC, @@ -112,7 +116,7 @@ def test_all_write_accesses(): all_write_accesses = AccessType.all_write_accesses() assert isinstance(all_write_accesses, list) - assert len(all_write_accesses) == 5 + assert len(all_write_accesses) == 7 assert (len(all_write_accesses) == len(set(all_write_accesses))) assert all(isinstance(write_access, AccessType) diff --git a/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py index 19ace025fa..57eb7724c7 100644 --- a/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py @@ -40,6 +40,7 @@ from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import EvaluatorTargetsMetadata @@ -145,7 +146,7 @@ def test_setter_errors(): with pytest.raises(ValueError) as info: metadata.evaluator_targets = ["invalid"] - assert ("The 'evaluator_targets' metadata should be a recognised value " - "(one of ['w3', 'wtheta', 'w2v', 'w2vtrace', 'w2broken', 'w0', " - "'w1', 'w2', 'w2trace', 'w2h', 'w2htrace', 'any_w2', 'wchi']) " - "but found 'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'evaluator_targets' metadata should be a recognised value " + f"(one of {sorted(const.VALID_FUNCTION_SPACES)}) but found " + f"'invalid'." in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py index c23b04966d..37fbf1c073 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py @@ -40,6 +40,7 @@ from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import FieldArgMetadata @@ -166,7 +167,7 @@ def test_check_datatype(): with pytest.raises(ValueError) as info: FieldArgMetadata.check_datatype("invalid") assert ("The 'datatype descriptor' metadata should be a recognised value " - "(one of ['gh_real', 'gh_integer']) but found 'invalid'." + "(one of ['gh_integer', 'gh_real']) but found 'invalid'." in str(info.value)) @@ -176,8 +177,8 @@ def test_check_access(): with pytest.raises(ValueError) as info: FieldArgMetadata.check_access("invalid") assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_read', 'gh_write', 'gh_readwrite', 'gh_inc', " - "'gh_readinc']) but found 'invalid'." in str(info.value)) + "(one of ['gh_inc', 'gh_read', 'gh_readinc', 'gh_readwrite', " + "'gh_write']) but found 'invalid'." in str(info.value)) def test_function_space_setter_getter(): @@ -188,18 +189,10 @@ def test_function_space_setter_getter(): field_arg = FieldArgMetadata("GH_REAL", "GH_READ", "W0") with pytest.raises(ValueError) as info: field_arg.function_space = "invalid" - assert ("The 'function space' metadata should be a recognised value (one " - "of ['w3', 'wtheta', 'w2v', 'w2vtrace', 'w2broken', 'w0', 'w1', " - "'w2', 'w2trace', 'w2h', 'w2htrace', 'any_w2', 'wchi', " - "'any_space_1', 'any_space_2', 'any_space_3', 'any_space_4', " - "'any_space_5', 'any_space_6', 'any_space_7', 'any_space_8', " - "'any_space_9', 'any_space_10', 'any_discontinuous_space_1', " - "'any_discontinuous_space_2', 'any_discontinuous_space_3', " - "'any_discontinuous_space_4', 'any_discontinuous_space_5', " - "'any_discontinuous_space_6', 'any_discontinuous_space_7', " - "'any_discontinuous_space_8', 'any_discontinuous_space_9', " - "'any_discontinuous_space_10']) but found 'invalid'." - in str(info.value)) + const = LFRicConstants() + assert (f"The 'function space' metadata should be a recognised value (one " + f"of {sorted(const.VALID_FUNCTION_SPACE_NAMES)}) but found " + f"'invalid'." in str(info.value)) field_arg.function_space = "w3" assert field_arg.function_space == "w3" field_arg.function_space = "W3" @@ -216,7 +209,7 @@ def test_stencil_getter_setter(): with pytest.raises(ValueError) as info: field_arg.stencil = "invalid" assert ("The 'stencil' metadata should be a recognised value (one of " - "['x1d', 'y1d', 'xory1d', 'cross', 'region', 'cross2d']) but " + "['cross', 'cross2d', 'region', 'x1d', 'xory1d', 'y1d']) but " "found 'invalid'." in str(info.value)) field_arg.stencil = "x1d" assert field_arg.stencil == "x1d" diff --git a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py index cada154869..e6366e3aeb 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py @@ -97,7 +97,7 @@ def test_init_invalid_stencil(): _ = FieldVectorArgMetadata( "GH_REAL", "GH_READ", "W0", "2", stencil="invalid") assert ("The 'stencil' metadata should be a recognised value (one of " - "['x1d', 'y1d', 'xory1d', 'cross', 'region', 'cross2d']) but " + "['cross', 'cross2d', 'region', 'x1d', 'xory1d', 'y1d']) but " "found 'invalid'." in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py index 010c5dc7b9..be54150c49 100644 --- a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py @@ -115,9 +115,8 @@ def test_init_args_error(): with pytest.raises(ValueError) as info: _ = LFRicKernelMetadata(operates_on="invalid") assert ("The 'OPERATES_ON' metadata should be a recognised value " - "(one of ['domain', 'dof', 'owned_dof', 'cell_column', " - "'owned_cell_column', 'halo_cell_column', " - "'owned_and_halo_cell_column']) " + "(one of ['cell_column', 'dof', 'domain', 'halo_cell_column', " + "'owned_and_halo_cell_column', 'owned_cell_column', 'owned_dof']) " "but found 'invalid'." in str(info.value)) with pytest.raises(TypeError) as info: @@ -783,8 +782,8 @@ def test_validate_cma_matrix_kernel(): with pytest.raises(ValueError) as info: ScalarArgMetadata("gh_real", "gh_write") assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_read', 'gh_sum']) but found 'gh_write'." - in str(info.value)) + "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " + "'gh_write'." in str(info.value)) # OK. meta_args = [ @@ -1298,9 +1297,8 @@ def test_setter_getter_operates_on(): with pytest.raises(ValueError) as info: metadata.operates_on = "invalid" assert ("The 'OPERATES_ON' metadata should be a recognised value " - "(one of ['domain', 'dof', 'owned_dof', 'cell_column', " - "'owned_cell_column', 'halo_cell_column', " - "'owned_and_halo_cell_column']) " + "(one of ['cell_column', 'dof', 'domain', 'halo_cell_column', " + "'owned_and_halo_cell_column', 'owned_cell_column', 'owned_dof']) " "but found 'invalid'." in str(info.value)) metadata.operates_on = "DOMAIN" assert metadata.operates_on == "domain" diff --git a/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py index b1697dce2a..8d9d1c63fd 100644 --- a/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py @@ -130,5 +130,5 @@ def test_check_access(): with pytest.raises(ValueError) as info: ScalarArgMetadata.check_access("invalid") assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_read', 'gh_sum']) but found 'invalid'." - in str(info.value)) + "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " + "'invalid'." in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py index 6bd3a841d8..e3a9fec1e1 100644 --- a/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py @@ -99,8 +99,8 @@ def test_check_access(): with pytest.raises(ValueError) as info: ScalarArrayArgMetadata.check_access("invalid") assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_read', 'gh_sum']) but found 'invalid'." - in str(info.value)) + "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " + "'invalid'." in str(info.value)) def test_get_array_ndims(): diff --git a/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py b/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py index 1d01b65040..69e107cadc 100644 --- a/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py @@ -198,8 +198,8 @@ def test_ad_scalar_type_no_write(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_sum'] access but found 'gh_write'" in - str(excinfo.value)) + "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "'gh_write'" in str(excinfo.value)) def test_ad_scalar_type_no_inc(): @@ -215,8 +215,8 @@ def test_ad_scalar_type_no_inc(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_sum'] access but found 'gh_inc'" in - str(excinfo.value)) + "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "'gh_inc'" in str(excinfo.value)) def test_ad_scalar_type_no_readwrite(): @@ -233,8 +233,8 @@ def test_ad_scalar_type_no_readwrite(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_sum'] access but found 'gh_readwrite'" in - str(excinfo.value)) + "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "'gh_readwrite'" in str(excinfo.value)) @pytest.mark.parametrize("scalar_type", ["gh_integer", "gh_logical"]) From 64874cdbded6dd1101f7a863314f5daa2bd8dbaa Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 09:37:17 +0000 Subject: [PATCH 25/41] #2381 fix all LFRic tests --- .../domain/lfric/kernel/common_metadata.py | 2 +- .../domain/lfric/lfric_arg_descriptor.py | 2 +- .../kernel/evaluator_targets_metadata_test.py | 2 +- .../lfric/kernel/field_arg_metadata_test.py | 18 ++++++++++-------- .../kernel/field_vector_arg_metadata_test.py | 8 +++++--- .../lfric/kernel/lfric_kernel_metadata_test.py | 18 +++++++++++------- .../lfric/kernel/scalar_arg_metadata_test.py | 8 +++++--- .../kernel/scalar_array_arg_metadata_test.py | 8 +++++--- .../domain/lfric/lfric_scalar_mdata_test.py | 6 +++--- 9 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_metadata.py b/src/psyclone/domain/lfric/kernel/common_metadata.py index 1c6471295e..b74c1424bc 100644 --- a/src/psyclone/domain/lfric/kernel/common_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_metadata.py @@ -95,7 +95,7 @@ def validate_scalar_value(value, valid_values, name): if value.lower() not in valid_values: raise ValueError( f"The '{name}' metadata should be a recognised " - f"value (one of {sorted(valid_values)}) " + f"value (one of {valid_values}) " f"but found '{value}'.") @staticmethod diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index 44eef75f3a..3597695e09 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -628,7 +628,7 @@ def _init_scalar(self, arg_type): rev_access_mapping = api_config.get_reverse_access_mapping() if self._access_type not in scalar_accesses: api_specific_name = rev_access_mapping[self._access_type] - valid_reductions = sorted(AccessType.get_valid_reduction_names()) + valid_reductions = AccessType.get_valid_reduction_names() raise ParseError( f"In the LFRic API scalar arguments must have read-only " f"('gh_read') or a reduction {valid_reductions} access but " diff --git a/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py index 57eb7724c7..35f6349699 100644 --- a/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/evaluator_targets_metadata_test.py @@ -148,5 +148,5 @@ def test_setter_errors(): metadata.evaluator_targets = ["invalid"] const = LFRicConstants() assert (f"The 'evaluator_targets' metadata should be a recognised value " - f"(one of {sorted(const.VALID_FUNCTION_SPACES)}) but found " + f"(one of {const.VALID_FUNCTION_SPACES}) but found " f"'invalid'." in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py index 37fbf1c073..6783c5fb93 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py @@ -167,7 +167,7 @@ def test_check_datatype(): with pytest.raises(ValueError) as info: FieldArgMetadata.check_datatype("invalid") assert ("The 'datatype descriptor' metadata should be a recognised value " - "(one of ['gh_integer', 'gh_real']) but found 'invalid'." + "(one of ['gh_real', 'gh_integer']) but found 'invalid'." in str(info.value)) @@ -176,9 +176,10 @@ def test_check_access(): FieldArgMetadata.check_access("GH_READ") with pytest.raises(ValueError) as info: FieldArgMetadata.check_access("invalid") - assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_inc', 'gh_read', 'gh_readinc', 'gh_readwrite', " - "'gh_write']) but found 'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'access descriptor' metadata should be a recognised value " + f"(one of {const.VALID_FIELD_ACCESS_TYPES}) but found 'invalid'." + in str(info.value)) def test_function_space_setter_getter(): @@ -191,7 +192,7 @@ def test_function_space_setter_getter(): field_arg.function_space = "invalid" const = LFRicConstants() assert (f"The 'function space' metadata should be a recognised value (one " - f"of {sorted(const.VALID_FUNCTION_SPACE_NAMES)}) but found " + f"of {const.VALID_FUNCTION_SPACE_NAMES}) but found " f"'invalid'." in str(info.value)) field_arg.function_space = "w3" assert field_arg.function_space == "w3" @@ -208,9 +209,10 @@ def test_stencil_getter_setter(): field_arg = FieldArgMetadata("GH_REAL", "GH_READ", "W0") with pytest.raises(ValueError) as info: field_arg.stencil = "invalid" - assert ("The 'stencil' metadata should be a recognised value (one of " - "['cross', 'cross2d', 'region', 'x1d', 'xory1d', 'y1d']) but " - "found 'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'stencil' metadata should be a recognised value (one of " + f"{const.VALID_STENCIL_TYPES}) but " + f"found 'invalid'." in str(info.value)) field_arg.stencil = "x1d" assert field_arg.stencil == "x1d" field_arg.stencil = "X1D" diff --git a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py index e6366e3aeb..32aad5f2f5 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py @@ -40,6 +40,7 @@ from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import FieldVectorArgMetadata @@ -96,9 +97,10 @@ def test_init_invalid_stencil(): with pytest.raises(ValueError) as info: _ = FieldVectorArgMetadata( "GH_REAL", "GH_READ", "W0", "2", stencil="invalid") - assert ("The 'stencil' metadata should be a recognised value (one of " - "['cross', 'cross2d', 'region', 'x1d', 'xory1d', 'y1d']) but " - "found 'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'stencil' metadata should be a recognised value (one of " + f"{const.VALID_STENCIL_TYPES}) but " + f"found 'invalid'." in str(info.value)) def test_get_metadata(): diff --git a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py index be54150c49..1d1860012d 100644 --- a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py @@ -41,6 +41,7 @@ from fparser.common.readfortran import FortranStringReader from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import ( ColumnwiseOperatorArgMetadata, EvaluatorTargetsMetadata, FieldArgMetadata, FieldVectorArgMetadata, InterGridArgMetadata, InterGridVectorArgMetadata, @@ -115,8 +116,9 @@ def test_init_args_error(): with pytest.raises(ValueError) as info: _ = LFRicKernelMetadata(operates_on="invalid") assert ("The 'OPERATES_ON' metadata should be a recognised value " - "(one of ['cell_column', 'dof', 'domain', 'halo_cell_column', " - "'owned_and_halo_cell_column', 'owned_cell_column', 'owned_dof']) " + "(one of ['domain', 'dof', 'owned_dof', 'cell_column', " + "'owned_cell_column', 'halo_cell_column', " + "'owned_and_halo_cell_column']) " "but found 'invalid'." in str(info.value)) with pytest.raises(TypeError) as info: @@ -781,9 +783,10 @@ def test_validate_cma_matrix_kernel(): # check that a scalar must be read only. with pytest.raises(ValueError) as info: ScalarArgMetadata("gh_real", "gh_write") - assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " - "'gh_write'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'access descriptor' metadata should be a recognised value " + f"(one of {const.VALID_SCALAR_ACCESS_TYPES}) but found 'gh_write'." + in str(info.value)) # OK. meta_args = [ @@ -1297,8 +1300,9 @@ def test_setter_getter_operates_on(): with pytest.raises(ValueError) as info: metadata.operates_on = "invalid" assert ("The 'OPERATES_ON' metadata should be a recognised value " - "(one of ['cell_column', 'dof', 'domain', 'halo_cell_column', " - "'owned_and_halo_cell_column', 'owned_cell_column', 'owned_dof']) " + "(one of ['domain', 'dof', 'owned_dof', 'cell_column', " + "'owned_cell_column', 'halo_cell_column', " + "'owned_and_halo_cell_column']) " "but found 'invalid'." in str(info.value)) metadata.operates_on = "DOMAIN" assert metadata.operates_on == "domain" diff --git a/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py index 8d9d1c63fd..58b4018c5d 100644 --- a/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/scalar_arg_metadata_test.py @@ -40,6 +40,7 @@ from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import ScalarArgMetadata @@ -129,6 +130,7 @@ def test_check_access(): ScalarArgMetadata.check_access("gh_sum") with pytest.raises(ValueError) as info: ScalarArgMetadata.check_access("invalid") - assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " - "'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'access descriptor' metadata should be a recognised value " + f"(one of {const.VALID_SCALAR_ACCESS_TYPES}) but found " + f"'invalid'." in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py index e3a9fec1e1..1aac4102e3 100644 --- a/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py @@ -41,6 +41,7 @@ from fparser.two import Fortran2003 +from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import ScalarArrayArgMetadata @@ -98,9 +99,10 @@ def test_check_access(): ScalarArrayArgMetadata.check_access("GH_READ") with pytest.raises(ValueError) as info: ScalarArrayArgMetadata.check_access("invalid") - assert ("The 'access descriptor' metadata should be a recognised value " - "(one of ['gh_max', 'gh_min', 'gh_read', 'gh_sum']) but found " - "'invalid'." in str(info.value)) + const = LFRicConstants() + assert (f"The 'access descriptor' metadata should be a recognised value " + f"(one of {const.VALID_SCALAR_ACCESS_TYPES}) but found " + f"'invalid'." in str(info.value)) def test_get_array_ndims(): diff --git a/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py b/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py index 69e107cadc..32c5af35b8 100644 --- a/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_scalar_mdata_test.py @@ -198,7 +198,7 @@ def test_ad_scalar_type_no_write(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "reduction ['gh_sum', 'gh_min', 'gh_max'] access but found " "'gh_write'" in str(excinfo.value)) @@ -215,7 +215,7 @@ def test_ad_scalar_type_no_inc(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "reduction ['gh_sum', 'gh_min', 'gh_max'] access but found " "'gh_inc'" in str(excinfo.value)) @@ -233,7 +233,7 @@ def test_ad_scalar_type_no_readwrite(): with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) assert ("scalar arguments must have read-only ('gh_read') or a " - "reduction ['gh_max', 'gh_min', 'gh_sum'] access but found " + "reduction ['gh_sum', 'gh_min', 'gh_max'] access but found " "'gh_readwrite'" in str(excinfo.value)) From 650f58442c7f5c319d6a2309249539c2c4603ae0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 13:28:32 +0000 Subject: [PATCH 26/41] #2381 use template for max/min reduction --- src/psyclone/domain/lfric/lfric_global_max.py | 89 +++++++++---------- src/psyclone/domain/lfric/lfric_global_min.py | 89 +++++++++---------- .../domain/lfric/lfric_global_reduction.py | 61 +++++++++++++ src/psyclone/domain/lfric/lfric_global_sum.py | 44 +++++++++ .../domain/lfric/lfric_global_max_test.py | 79 ++++++++++++++++ .../domain/lfric/lfric_global_min_test.py | 6 +- 6 files changed, 269 insertions(+), 99 deletions(-) create mode 100644 src/psyclone/domain/lfric/lfric_global_reduction.py create mode 100644 src/psyclone/tests/domain/lfric/lfric_global_max_test.py diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py index 68a5f42d62..ce0dddb7af 100644 --- a/src/psyclone/domain/lfric/lfric_global_max.py +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -1,7 +1,44 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab + ''' +This module provides the LFRicGlobalMax class. + ''' -from psyclone.domain.common.psylayer.global_max import GlobalMax +from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, StructureReference) @@ -10,52 +47,8 @@ REAL_TYPE, UnresolvedType) -class LFRicGlobalMax(GlobalMax): +class LFRicGlobalMax(LFRicGlobalReduction): ''' ''' - def lower_to_language_level(self) -> Node: - ''' - :returns: this node lowered to language-level PSyIR. - - ''' - # Get the name strings to use - name = self._operand.name - - symtab = self.ancestor(InvokeSchedule).symbol_table - - # We'll need the LFRic mpi_type. - mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", - symbol_type=ContainerSymbol) - mpi_type = symtab.find_or_create_tag( - "lfric_mpi_type", - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(mpi_mod)) - mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, - datatype=mpi_type) - # Symbol holding the local maximum value. - loc_min = symtab.lookup(name) - - # Symbol holding the global maximum value. - result = symtab.new_symbol("glob_min", symbol_type=DataSymbol, - # TODO - get correct type. - datatype=REAL_TYPE) - - # Obtain a suitable mpi object from one of the field arguments. - for sym in symtab.datasymbols: - if (isinstance(sym.datatype, DataTypeSymbol) and - sym.datatype.name == "field_type"): - break - get_mpi = StructureReference.create(sym, ["get_mpi"]) - self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), - rhs=Call.create(get_mpi)), - index=0) - - # Call the method to compute the global min. - sref = StructureReference.create(mpi_obj, ["global_max"]) - call = Call.create(sref, [Reference(loc_min), Reference(result)]) - call.preceding_comment = "Perform global max" - self.parent.addchild(call, self.position) - assign = Assignment.create(lhs=Reference(loc_min), - rhs=Reference(result)) - return self.replace_with(assign) + _reduction_name = "max" + _method_name = "global_max" diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 566ffea759..b2482499f9 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -1,7 +1,44 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab + ''' +This module provides the LFRicGlobalMin class. + ''' -from psyclone.domain.common.psylayer.global_min import GlobalMin +from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, StructureReference) @@ -10,52 +47,8 @@ REAL_TYPE, UnresolvedType) -class LFRicGlobalMin(GlobalMin): +class LFRicGlobalMin(LFRicGlobalReduction): ''' ''' - def lower_to_language_level(self) -> Node: - ''' - :returns: this node lowered to language-level PSyIR. - - ''' - # Get the name strings to use - name = self._operand.name - - symtab = self.ancestor(InvokeSchedule).symbol_table - - # We'll need the LFRic mpi_type. - mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", - symbol_type=ContainerSymbol) - mpi_type = symtab.find_or_create_tag( - "lfric_mpi_type", - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(mpi_mod)) - mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, - datatype=mpi_type) - # Symbol holding the local minimum value. - loc_min = symtab.lookup(name) - - # Symbol holding the global minimum value. - result = symtab.new_symbol("glob_min", symbol_type=DataSymbol, - # TODO - get correct type. - datatype=REAL_TYPE) - - # Obtain a suitable mpi object from one of the field arguments. - for sym in symtab.datasymbols: - if (isinstance(sym.datatype, DataTypeSymbol) and - sym.datatype.name == "field_type"): - break - get_mpi = StructureReference.create(sym, ["get_mpi"]) - self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), - rhs=Call.create(get_mpi)), - index=0) - - # Call the method to compute the global min. - sref = StructureReference.create(mpi_obj, ["global_min"]) - call = Call.create(sref, [Reference(loc_min), Reference(result)]) - call.preceding_comment = "Perform global min" - self.parent.addchild(call, self.position) - assign = Assignment.create(lhs=Reference(loc_min), - rhs=Reference(result)) - return self.replace_with(assign) + _reduction_name = "min" + _method_name = "global_min" diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reduction.py new file mode 100644 index 0000000000..f265cf5869 --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_reduction.py @@ -0,0 +1,61 @@ +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, + StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + REAL_TYPE, UnresolvedType) + + +class LFRicGlobalReduction(GlobalReduction): + ''' + ''' + _reduction_name = "" + _method_name = "" + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + + ''' + # Get the name strings to use + name = self._operand.name + + symtab = self.ancestor(InvokeSchedule).symbol_table + + # We'll need the LFRic mpi_type. + mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", + symbol_type=ContainerSymbol) + mpi_type = symtab.find_or_create_tag( + "lfric_mpi_type", + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(mpi_mod)) + mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, + datatype=mpi_type) + # Symbol holding the local value. + loc_min = symtab.lookup(name) + + # Symbol holding the global value. + result = symtab.new_symbol(f"glob_{name}", symbol_type=DataSymbol, + # TODO - get correct type. + datatype=REAL_TYPE) + + # Obtain a suitable mpi object from one of the field arguments. + for sym in symtab.datasymbols: + if (isinstance(sym.datatype, DataTypeSymbol) and + sym.datatype.name == "field_type"): + break + get_mpi = StructureReference.create(sym, ["get_mpi"]) + self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), + rhs=Call.create(get_mpi)), + index=0) + + # Call the method to compute the global min. + sref = StructureReference.create(mpi_obj, [self._method_name]) + call = Call.create(sref, [Reference(loc_min), Reference(result)]) + call.preceding_comment = f"Perform global {self._reduction_name}" + self.parent.addchild(call, self.position) + assign = Assignment.create(lhs=Reference(loc_min), + rhs=Reference(result)) + return self.replace_with(assign) diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 0a6847d3ff..f6a0be3631 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,3 +1,47 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2025, 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 the LFRicGlobalSum class. + +''' + from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py new file mode 100644 index 0000000000..d4cb1ad36d --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py @@ -0,0 +1,79 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2025, 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. +# ----------------------------------------------------------------------------- +# Author A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +'''Module containing pytest tests for the LFRicGlobalMax class.''' + +from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax +from psyclone.psyGen import Kern +from psyclone.tests.utilities import get_invoke + +TEST_API = "lfric" + + +def test_lgmax_in_invoke(): + ''' + Test the construction of an LFRicGlobalMax object. + + This is complicated by the need to supply it with an LFRicKernelArgument + and therefore we use a full example to get hold of a suitable argument. + ''' + psy, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", + TEST_API, dist_mem=True, + idx=0) + sched = invoke.schedule + + # Find a suitable kernel argument (real scalar). + kernel = sched.walk(Kern)[0] + for arg in kernel.args: + if arg.is_scalar and arg.intrinsic_type == "real": + break + + lgm = LFRicGlobalMax(operand=arg) + assert isinstance(lgm, LFRicGlobalMax) + assert lgm.operand is not arg + assert lgm.operand.name == arg.name + + sched.addchild(lgm) + output = psy.gen + assert "use lfric_mpi_mod, only : lfric_mpi_type" in output + assert "type(lfric_mpi_type) :: mpi" in output + # TODO correct type/precision + assert "real :: glob_a" in output + assert "mpi = f1%get_mpi()" in output + assert '''\ + ! Perform global max + call mpi%global_max(a, glob_a) + a = glob_a''' in output diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py index 4e372b9b21..c9360b5792 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -71,9 +71,9 @@ def test_lgm_in_invoke(): assert "use lfric_mpi_mod, only : lfric_mpi_type" in output assert "type(lfric_mpi_type) :: mpi" in output # TODO correct type/precision - assert "real :: glob_min" in output + assert "real :: glob_a" in output assert "mpi = f1%get_mpi()" in output assert '''\ ! Perform global min - call mpi%global_min(a, glob_min) - a = glob_min''' in output + call mpi%global_min(a, glob_a) + a = glob_a''' in output From 8aeffda323bdc4c02781062b5bc1ec605311912d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 13:30:01 +0000 Subject: [PATCH 27/41] #2381 fix linting --- src/psyclone/domain/lfric/lfric_global_max.py | 6 ------ src/psyclone/domain/lfric/lfric_global_min.py | 6 ------ 2 files changed, 12 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py index ce0dddb7af..ca7396ecd0 100644 --- a/src/psyclone/domain/lfric/lfric_global_max.py +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -39,12 +39,6 @@ ''' from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction -from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, - StructureReference) -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - REAL_TYPE, UnresolvedType) class LFRicGlobalMax(LFRicGlobalReduction): diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index b2482499f9..46ff740e31 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -39,12 +39,6 @@ ''' from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction -from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, - StructureReference) -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - REAL_TYPE, UnresolvedType) class LFRicGlobalMin(LFRicGlobalReduction): From e5e9af555f726ad46c57a4beaf2f391dd77c6876 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 14:08:09 +0000 Subject: [PATCH 28/41] #2381 fix cov of psyGen --- src/psyclone/psyGen.py | 1 + src/psyclone/tests/psyGen_test.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 2d0d0d95f5..440cda5fbd 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -992,6 +992,7 @@ def initialise_reduction_variable(self) -> None: configuration file) is less than 1. :raises GenerationError: for a reduction into a scalar that is neither 'real' nor 'integer'. + :raises GenerationError: for an unsupported type of reduction. ''' var_arg = self._reduction_arg diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index b33e694c24..524c046cb3 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -1111,6 +1111,13 @@ def test_reduction_var_invalid_scalar_error(dist_mem): # Have to pretend this arg has a reduction access. call._reduction_arg._access = AccessType.MAX call.initialise_reduction_variable() + # An invalid reduction access. + call._reduction_arg._access = AccessType.INC + with pytest.raises(GenerationError) as err: + call.initialise_reduction_variable() + assert ("Kernel 'testkern_three_scalars_code' performs a reduction of " + "type 'INC' but this is not supported by Kern.initialise_" + "reduction_variable()" in str(err.value)) def test_reduction_sum_error(dist_mem): From bd0c70a17d6462f076d258aaeab05bce1581eef2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 14:45:32 +0000 Subject: [PATCH 29/41] #2381 rationalise class structure --- .../domain/common/psylayer/global_max.py | 49 ---------- .../domain/common/psylayer/global_min.py | 49 ---------- .../domain/common/psylayer/global_sum.py | 46 ---------- src/psyclone/domain/lfric/lfric_global_max.py | 48 ---------- src/psyclone/domain/lfric/lfric_global_min.py | 48 ---------- ...eduction.py => lfric_global_reductions.py} | 68 ++++++++++++++ src/psyclone/domain/lfric/lfric_global_sum.py | 92 ------------------- src/psyclone/domain/lfric/lfric_invoke.py | 5 +- .../domain/lfric/lfric_global_max_test.py | 2 +- .../domain/lfric/lfric_global_min_test.py | 2 +- .../domain/lfric/lfric_global_sum_test.py | 2 +- 11 files changed, 73 insertions(+), 338 deletions(-) delete mode 100644 src/psyclone/domain/common/psylayer/global_max.py delete mode 100644 src/psyclone/domain/common/psylayer/global_min.py delete mode 100644 src/psyclone/domain/common/psylayer/global_sum.py delete mode 100644 src/psyclone/domain/lfric/lfric_global_max.py delete mode 100644 src/psyclone/domain/lfric/lfric_global_min.py rename src/psyclone/domain/lfric/{lfric_global_reduction.py => lfric_global_reductions.py} (53%) delete mode 100644 src/psyclone/domain/lfric/lfric_global_sum.py diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py deleted file mode 100644 index 3051d791a2..0000000000 --- a/src/psyclone/domain/common/psylayer/global_max.py +++ /dev/null @@ -1,49 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Python module definining the GlobalMax subclass of GlobalReduction. - -''' - -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction - - -class GlobalMax(GlobalReduction): - ''' - Represents a reduction to compute the global minimum value of a scalar. - - ''' - _text_name = "GlobalMax" diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py deleted file mode 100644 index 4c3f595087..0000000000 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ /dev/null @@ -1,49 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Python module definining the GlobalMin subclass of GlobalReduction. - -''' - -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction - - -class GlobalMin(GlobalReduction): - ''' - Represents a reduction to compute the global minimum value of a scalar. - - ''' - _text_name = "GlobalMin" diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py deleted file mode 100644 index 3796964fee..0000000000 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ /dev/null @@ -1,46 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Python module definining the GlobalSum subclass of GlobalReduction. - -''' -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction - - -class GlobalSum(GlobalReduction): - ''' - ''' - _text_name = "GlobalSum" diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py deleted file mode 100644 index ca7396ecd0..0000000000 --- a/src/psyclone/domain/lfric/lfric_global_max.py +++ /dev/null @@ -1,48 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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. -# ----------------------------------------------------------------------------- -# Author: A. R. Porter, STFC Daresbury Lab - -''' -This module provides the LFRicGlobalMax class. - -''' - -from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction - - -class LFRicGlobalMax(LFRicGlobalReduction): - ''' - ''' - _reduction_name = "max" - _method_name = "global_max" diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py deleted file mode 100644 index 46ff740e31..0000000000 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ /dev/null @@ -1,48 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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. -# ----------------------------------------------------------------------------- -# Author: A. R. Porter, STFC Daresbury Lab - -''' -This module provides the LFRicGlobalMin class. - -''' - -from psyclone.domain.lfric.lfric_global_reduction import LFRicGlobalReduction - - -class LFRicGlobalMin(LFRicGlobalReduction): - ''' - ''' - _reduction_name = "min" - _method_name = "global_min" diff --git a/src/psyclone/domain/lfric/lfric_global_reduction.py b/src/psyclone/domain/lfric/lfric_global_reductions.py similarity index 53% rename from src/psyclone/domain/lfric/lfric_global_reduction.py rename to src/psyclone/domain/lfric/lfric_global_reductions.py index f265cf5869..25d373c70e 100644 --- a/src/psyclone/domain/lfric/lfric_global_reduction.py +++ b/src/psyclone/domain/lfric/lfric_global_reductions.py @@ -59,3 +59,71 @@ def lower_to_language_level(self) -> Node: assign = Assignment.create(lhs=Reference(loc_min), rhs=Reference(result)) return self.replace_with(assign) + + +class LFRicGlobalMax(LFRicGlobalReduction): + ''' + ''' + _reduction_name = "max" + _method_name = "global_max" + _text_name = "GlobalMax" + + +class LFRicGlobalMin(LFRicGlobalReduction): + ''' + ''' + _reduction_name = "min" + _method_name = "global_min" + _text_name = "GlobalMin" + + +class LFRicGlobalSum(LFRicGlobalReduction): + ''' + Represents a global sum in the LFRic DSL. + + ''' + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + + ''' + # Get the name strings to use + name = self._operand.name + type_name = self._operand.data_type + mod_name = self._operand.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) + + +# ============================================================================= +# Documentation utils: The list of module members that we wish AutoAPI to +# generate documentation for. +__all__ = [ + 'LFRicGlobalReduction', + 'LFRicGlobalMax', + 'LFRicGlobalMin', + 'LFRicGlobalSum' +] diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py deleted file mode 100644 index f6a0be3631..0000000000 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ /dev/null @@ -1,92 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2017-2025, 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 the LFRicGlobalSum class. - -''' - -from psyclone.domain.common.psylayer.global_sum import GlobalSum -from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, - StructureReference) -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - UnresolvedType) - - -class LFRicGlobalSum(GlobalSum): - ''' - Represents a global sum in the LFRic DSL. - - ''' - def lower_to_language_level(self) -> Node: - ''' - :returns: this node lowered to language-level PSyIR. - - ''' - # Get the name strings to use - name = self._operand.name - type_name = self._operand.data_type - mod_name = self._operand.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 3e924fd8d7..847ed465ea 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -93,9 +93,8 @@ def __init__(self, alg_invocation, idx, invokes): LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicFields, LFRicDofmaps, LFRicStencils) - from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum - from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin - from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax + from psyclone.domain.lfric.lfric_global_reductions import ( + LFRicGlobalSum, LFRicGlobalMin, LFRicGlobalMax) self.scalar_args = LFRicScalarArgs(self) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py index d4cb1ad36d..365c5ee652 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py @@ -36,7 +36,7 @@ '''Module containing pytest tests for the LFRicGlobalMax class.''' -from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax +from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMax from psyclone.psyGen import Kern from psyclone.tests.utilities import get_invoke diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py index c9360b5792..78f436adf5 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -36,7 +36,7 @@ '''Module containing pytest tests for the LFRicGlobalMin class.''' -from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin +from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMin from psyclone.psyGen import Kern from psyclone.tests.utilities import get_invoke diff --git a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py index a258acf37d..9a5fe75d27 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py @@ -42,7 +42,7 @@ import pytest from psyclone.core import AccessType -from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum +from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalSum from psyclone.errors import GenerationError, InternalError from psyclone.tests.utilities import get_invoke From 0eea98f2b71167e66a6cab280c7ea88029f20d08 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 15:14:22 +0000 Subject: [PATCH 30/41] #2381 update docstrings and copyright --- .../domain/lfric/lfric_global_reductions.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/psyclone/domain/lfric/lfric_global_reductions.py b/src/psyclone/domain/lfric/lfric_global_reductions.py index 25d373c70e..3f783daab6 100644 --- a/src/psyclone/domain/lfric/lfric_global_reductions.py +++ b/src/psyclone/domain/lfric/lfric_global_reductions.py @@ -1,3 +1,45 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2025, 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 implementations of the various global reduction + nodes supported in the LFRic DSL. ''' + from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, @@ -9,6 +51,8 @@ class LFRicGlobalReduction(GlobalReduction): ''' + LFRic-specific base class for all global-reduction operations. + ''' _reduction_name = "" _method_name = "" @@ -63,6 +107,8 @@ def lower_to_language_level(self) -> Node: class LFRicGlobalMax(LFRicGlobalReduction): ''' + Represents the operation to find the global maximum value of a scalar. + ''' _reduction_name = "max" _method_name = "global_max" @@ -71,6 +117,8 @@ class LFRicGlobalMax(LFRicGlobalReduction): class LFRicGlobalMin(LFRicGlobalReduction): ''' + Represents the operation to find the global minimum value of a scalar. + ''' _reduction_name = "min" _method_name = "global_min" @@ -82,6 +130,8 @@ class LFRicGlobalSum(LFRicGlobalReduction): Represents a global sum in the LFRic DSL. ''' + _text_name = "GlobalSum" + def lower_to_language_level(self) -> Node: ''' :returns: this node lowered to language-level PSyIR. From 19e1982a5ea8dacad686a7da8588e0c077a57d17 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 17:28:08 +0000 Subject: [PATCH 31/41] #2381 mv mpi-obj init into LFRicProxies --- .../domain/lfric/lfric_global_reductions.py | 37 +++++++------------ src/psyclone/lfric.py | 32 ++++++++++++++++ .../domain/lfric/lfric_global_max_test.py | 14 ++++--- .../domain/lfric/lfric_global_min_test.py | 5 +-- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_global_reductions.py b/src/psyclone/domain/lfric/lfric_global_reductions.py index 3f783daab6..cf954d3567 100644 --- a/src/psyclone/domain/lfric/lfric_global_reductions.py +++ b/src/psyclone/domain/lfric/lfric_global_reductions.py @@ -46,7 +46,7 @@ StructureReference) from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - REAL_TYPE, UnresolvedType) + UnresolvedType) class LFRicGlobalReduction(GlobalReduction): @@ -54,11 +54,19 @@ class LFRicGlobalReduction(GlobalReduction): LFRic-specific base class for all global-reduction operations. ''' + # The name of the reduction being performed. _reduction_name = "" + # The method of the lfric_mpi_type object that must be called to + # perform the reduction. _method_name = "" def lower_to_language_level(self) -> Node: ''' + Replaces this node with its language-level representation. + + This method is parameterised using self._reduction_name and + self._method_name so that it can be used for both max and min. + :returns: this node lowered to language-level PSyIR. ''' @@ -67,33 +75,16 @@ def lower_to_language_level(self) -> Node: symtab = self.ancestor(InvokeSchedule).symbol_table - # We'll need the LFRic mpi_type. - mpi_mod = symtab.find_or_create_tag("lfric_mpi_mod", - symbol_type=ContainerSymbol) - mpi_type = symtab.find_or_create_tag( - "lfric_mpi_type", - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(mpi_mod)) - mpi_obj = symtab.new_symbol("mpi", symbol_type=DataSymbol, - datatype=mpi_type) # Symbol holding the local value. loc_min = symtab.lookup(name) # Symbol holding the global value. result = symtab.new_symbol(f"glob_{name}", symbol_type=DataSymbol, - # TODO - get correct type. - datatype=REAL_TYPE) - - # Obtain a suitable mpi object from one of the field arguments. - for sym in symtab.datasymbols: - if (isinstance(sym.datatype, DataTypeSymbol) and - sym.datatype.name == "field_type"): - break - get_mpi = StructureReference.create(sym, ["get_mpi"]) - self.parent.addchild(Assignment.create(lhs=Reference(mpi_obj), - rhs=Call.create(get_mpi)), - index=0) + datatype=loc_min.datatype.copy()) + + # The code to get the MPI object is generated by lfric.LFRicProxies + # since it requires a proxy. + mpi_obj = symtab.lookup_with_tag("mpi") # Call the method to compute the global min. sref = StructureReference.create(mpi_obj, [self._method_name]) diff --git a/src/psyclone/lfric.py b/src/psyclone/lfric.py index 9c6268690f..36fe5c961b 100644 --- a/src/psyclone/lfric.py +++ b/src/psyclone/lfric.py @@ -1313,6 +1313,21 @@ def __init__(self, node): rank = 1 if arg not in op_args else 3 self._add_symbol(new_name, tag, intrinsic_type, arg, rank) + if self._invoke.schedule.reductions(): + # We have one or more reductions so we need to obtain the MPI + # object from one of the field arguments. + # We'll need the LFRic mpi_type. + mpi_mod = self.symtab.find_or_create_tag( + "lfric_mpi_mod", symbol_type=ContainerSymbol) + mpi_type = self.symtab.find_or_create_tag( + "lfric_mpi_type", + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(mpi_mod)) + self.symtab.find_or_create_tag("mpi", + symbol_type=DataSymbol, + datatype=mpi_type) + def _add_symbol(self, name, tag, intrinsic_type, arg, rank): ''' Creates a new DataSymbol representing either an LFRic field or @@ -1565,6 +1580,23 @@ def initialise(self, cursor: int) -> int: self._invoke.schedule[init_cursor].preceding_comment = ( "Initialise field and/or operator proxies") + if self._invoke.schedule.reductions(): + # Now that we've initialised the field proxies, we can get the + # MPI object. + for sym in self.symtab.datasymbols: + if (isinstance(sym.datatype, DataTypeSymbol) and + sym.datatype.name == "field_proxy_type"): + break + else: + raise InternalError("huh") + get_mpi = StructureReference.create(sym, ["get_mpi"]) + mpi_obj = self.symtab.lookup_with_tag("mpi") + self._invoke.schedule.addchild( + Assignment.create(lhs=Reference(mpi_obj), + rhs=Call.create(get_mpi)), + index=cursor) + cursor += 1 + return cursor diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py index 365c5ee652..eeffdf13ba 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py @@ -68,12 +68,14 @@ def test_lgmax_in_invoke(): sched.addchild(lgm) output = psy.gen - assert "use lfric_mpi_mod, only : lfric_mpi_type" in output - assert "type(lfric_mpi_type) :: mpi" in output - # TODO correct type/precision - assert "real :: glob_a" in output - assert "mpi = f1%get_mpi()" in output + assert "use lfric_mpi_mod, only : lfric_mpi_type" in output, output + assert "type(lfric_mpi_type) :: mpi" in output, output + assert "real(kind=r_def) :: glob_a" in output, output + assert "mpi = f1_proxy%get_mpi()" in output, output assert '''\ ! Perform global max call mpi%global_max(a, glob_a) - a = glob_a''' in output + a = glob_a''' in output, output + + # Can't compile this because we're assigning to a read-only scalar + # argument. diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py index 78f436adf5..16cb973955 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -70,9 +70,8 @@ def test_lgm_in_invoke(): output = psy.gen assert "use lfric_mpi_mod, only : lfric_mpi_type" in output assert "type(lfric_mpi_type) :: mpi" in output - # TODO correct type/precision - assert "real :: glob_a" in output - assert "mpi = f1%get_mpi()" in output + assert "real(kind=r_def) :: glob_a" in output + assert "mpi = f1_proxy%get_mpi()" in output assert '''\ ! Perform global min call mpi%global_min(a, glob_a) From e672b5a1b5e7f07dd4f823489ac53ea2e84fb435 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 21:52:41 +0000 Subject: [PATCH 32/41] #2381 WIP moving mpi-related setup to LFRicProxies class [skip ci] --- src/psyclone/lfric.py | 11 ++++++++--- .../tests/domain/lfric/lfric_global_max_test.py | 13 ++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/psyclone/lfric.py b/src/psyclone/lfric.py index 36fe5c961b..747454167f 100644 --- a/src/psyclone/lfric.py +++ b/src/psyclone/lfric.py @@ -1419,7 +1419,7 @@ def invoke_declarations(self): # field-type proxies for (fld_type, fld_mod), args in field_datatype_map.items(): fld_mod_symbol = table.node.parent.symbol_table.lookup(fld_mod) - fld_type_sym = table.node.parent.symbol_table.new_symbol( + fld_type_sym = table.node.parent.symbol_table.find_or_create_tag( fld_type, symbol_type=DataTypeSymbol, datatype=UnresolvedType(), @@ -1483,13 +1483,15 @@ def initialise(self, cursor: int) -> int: ''' Insert code into the PSy layer to initialise all necessary proxies. - :param cursor: position where to add the next initialisation + :param cursor: position at which to add the next initialisation statements. :returns: Updated cursor value. :raises InternalError: if a kernel argument of an unrecognised type is encountered. + :raises InternalError: if the invoke contains one or more reductions + but no fields. ''' init_cursor = cursor @@ -1588,7 +1590,10 @@ def initialise(self, cursor: int) -> int: sym.datatype.name == "field_proxy_type"): break else: - raise InternalError("huh") + raise InternalError( + f"Invoke '{self._invoke.name}' contains one or more " + f"reductions ({self._invoke.schedule.reductions()}) but " + f"does not access any fields.") get_mpi = StructureReference.create(sym, ["get_mpi"]) mpi_obj = self.symtab.lookup_with_tag("mpi") self._invoke.schedule.addchild( diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py index eeffdf13ba..14f8a0ff32 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py @@ -38,6 +38,7 @@ from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMax from psyclone.psyGen import Kern +from psyclone.psyir.symbols import ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, UnresolvedType from psyclone.tests.utilities import get_invoke TEST_API = "lfric" @@ -65,13 +66,19 @@ def test_lgmax_in_invoke(): assert isinstance(lgm, LFRicGlobalMax) assert lgm.operand is not arg assert lgm.operand.name == arg.name + csym = sched.symbol_table.new_symbol("lfric_mpi_mod", + symbol_type=ContainerSymbol) + mtype = sched.symbol_table.new_symbol("mpi_type", + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(csym)) + sched.symbol_table.find_or_create_tag("mpi", + symbol_type=DataSymbol, + datatype=mtype) sched.addchild(lgm) output = psy.gen - assert "use lfric_mpi_mod, only : lfric_mpi_type" in output, output - assert "type(lfric_mpi_type) :: mpi" in output, output assert "real(kind=r_def) :: glob_a" in output, output - assert "mpi = f1_proxy%get_mpi()" in output, output assert '''\ ! Perform global max call mpi%global_max(a, glob_a) From dfde2c6d37302a4894ce4b1f8e25f66b68a3543f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Dec 2025 21:53:54 +0000 Subject: [PATCH 33/41] #2381 fix lint [skip ci] --- src/psyclone/tests/domain/lfric/lfric_global_max_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py index 14f8a0ff32..55f9579839 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py @@ -38,7 +38,9 @@ from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMax from psyclone.psyGen import Kern -from psyclone.psyir.symbols import ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, UnresolvedType +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) from psyclone.tests.utilities import get_invoke TEST_API = "lfric" From a7cbe5e589cd0be0f0080952ff389a270fdef3fe Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 12:18:15 +0000 Subject: [PATCH 34/41] #2381 begin adding min/max builtins [skip ci] --- src/psyclone/domain/lfric/lfric_builtins.py | 136 +++++++++++++++++- src/psyclone/parse/lfric_builtins_mod.f90 | 48 +++++++ .../tests/domain/lfric/lfric_builtins_test.py | 15 ++ .../lfric/15.10.9_min_max_X_builtin.f90 | 52 +++++++ 4 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py index e194c6d93c..b11f11df33 100644 --- a/src/psyclone/domain/lfric/lfric_builtins.py +++ b/src/psyclone/domain/lfric/lfric_builtins.py @@ -311,6 +311,10 @@ def _validate(self): write_count = 0 # Only one argument must be written to field_count = 0 # We must have one or more fields as arguments spaces = set() # All field arguments must be on the same space + # Built-ins update fields DoF by DoF and therefore can have + # WRITE/READWRITE access + write_access_modes = AccessType.get_valid_reduction_modes() + [ + AccessType.WRITE, AccessType.READWRITE] # Field data types must be the same except for the conversion built-ins data_types = set() for arg in self.arg_descriptors: @@ -328,10 +332,8 @@ def _validate(self): f"must have one of {const.VALID_BUILTIN_DATA_TYPES} as " f"a data type but kernel '{self.name}' has an argument " f"of data type '{arg.data_type}'.") - # Built-ins update fields DoF by DoF and therefore can have - # WRITE/READWRITE access - if arg.access in [AccessType.WRITE, AccessType.SUM, - AccessType.READWRITE]: + # Check for write accesses + if arg.access in write_access_modes: write_count += 1 if arg.argument_type in const.VALID_FIELD_NAMES: field_count += 1 @@ -2743,6 +2745,128 @@ def lower_to_language_level(self) -> Node: # Create assignment and replace node return self._replace_with_assignment(lhs, rhs) +# ------------------------------------------------------------------- # +# ============ Minimum, maximum value of real field elements) ======= # +# ------------------------------------------------------------------- # + + +class LFRicMinvalXKern(LFRicBuiltIn): + ''' + Computes the (global) minimum scalar value held in + the supplied field. + ''' + _case_name = "minval_X" + _datatype = "real" + + @classmethod + def metadata(cls) -> LFRicKernelMetadata: + """ + :returns: kernel metadata describing this built-in. + """ + return cls._builtin_metadata([ + ScalarArgMetadata("gh_real", "gh_min"), + FieldArgMetadata("gh_real", "gh_read", "any_space_1")]) + + def __str__(self): + return (f"Built-in: {self._case_name} (compute the global minimum " + f"value contained in a field)") + + def lower_to_language_level(self) -> Node: + ''' + Lowers this LFRic-specific built-in kernel to language-level PSyIR. + This BuiltIn node is replaced by an Assignment node. + + :returns: the lowered version of this node. + + ''' + super().lower_to_language_level() + # Get indexed references for the field (proxy) argument. + arg_refs = self.get_indexed_field_argument_references() + # Get a reference for the kernel scalar reduction argument. + lhs = self._reduction_reference() + minval = IntrinsicCall(IntrinsicCall.Intrinsic.MINVAL, + [arg_refs[0]]) + return self._replace_with_assignment(lhs, minval) + + +class LFRicMaxvalXKern(LFRicBuiltIn): + ''' + Computes the (global) maximum scalar value held in + the supplied field. + ''' + _case_name = "maxval_X" + _datatype = "real" + + @classmethod + def metadata(cls) -> LFRicKernelMetadata: + """ + :returns: kernel metadata describing this built-in. + """ + return cls._builtin_metadata([ + ScalarArgMetadata("gh_real", "gh_max"), + FieldArgMetadata("gh_real", "gh_read", "any_space_1")]) + + def __str__(self): + return (f"Built-in: {self._case_name} (compute the global maximimum " + f"value contained in a field)") + + def lower_to_language_level(self) -> Node: + ''' + Lowers this LFRic-specific built-in kernel to language-level PSyIR. + This BuiltIn node is replaced by an Assignment node. + + :returns: the lowered version of this node. + + ''' + super().lower_to_language_level() + # Get indexed references for the field (proxy) argument. + arg_refs = self.get_indexed_field_argument_references() + # Get a reference for the kernel scalar reduction argument. + lhs = self._reduction_reference() + minval = IntrinsicCall(IntrinsicCall.Intrinsic.MAXVAL, + [arg_refs[0]]) + return self._replace_with_assignment(lhs, minval) + + +class LFRicMinMaxXKern(LFRicBuiltIn): + ''' + Computes the (global) minimum and maximum scalar values held in + the supplied field. + ''' + _case_name = "min_max_X" + _datatype = "real" + + @classmethod + def metadata(cls) -> LFRicKernelMetadata: + """ + :returns: kernel metadata describing this built-in. + """ + return cls._builtin_metadata([ + ScalarArgMetadata("gh_real", "gh_min"), + ScalarArgMetadata("gh_real", "gh_max"), + FieldArgMetadata("gh_real", "gh_read", "any_space_1")]) + + def __str__(self): + return (f"Built-in: {self._case_name} (compute the global minimum and " + f"maximum values contained in a field)") + + def lower_to_language_level(self) -> Node: + ''' + Lowers this LFRic-specific built-in kernel to language-level PSyIR. + This BuiltIn node is replaced by an Assignment node. + + :returns: the lowered version of this node. + + ''' + super().lower_to_language_level() + # Get indexed references for the field (proxy) argument. + arg_refs = self.get_indexed_field_argument_references() + # Get a reference for the kernel scalar reduction argument. + lhs = self._reduction_reference() + minval = IntrinsicCall(IntrinsicCall.Intrinsic.MINVAL, + [arg_refs[0]]) + return self._replace_with_assignment(lhs, minval) + # ------------------------------------------------------------------- # # ============== Converting real to integer field elements ========== # # ------------------------------------------------------------------- # @@ -3270,6 +3394,10 @@ def lower_to_language_level(self) -> Node: # Minimum of a real scalar value and real field elements "min_aX": LFRicMinAXKern, "inc_min_aX": LFRicIncMinAXKern, + # Minimum and maximum values contained in a field + "minval_X": LFRicMinvalXKern, + "maxval_X": LFRicMaxvalXKern, + "min_max_X": LFRicMinMaxXKern, # Converting real to integer field elements "real_to_int_X": LFRicRealToIntXKern, # Converting real to real field elements diff --git a/src/psyclone/parse/lfric_builtins_mod.f90 b/src/psyclone/parse/lfric_builtins_mod.f90 index 46bdaa1f06..33fe1e51fd 100644 --- a/src/psyclone/parse/lfric_builtins_mod.f90 +++ b/src/psyclone/parse/lfric_builtins_mod.f90 @@ -672,6 +672,44 @@ module lfric_builtins_mod procedure, nopass :: inc_min_aX_code end type inc_min_aX +! ------------------------------------------------------------------- ! +! ============ Minimum, maximum value of real field elements) ======= ! +! ------------------------------------------------------------------- ! + + type, public, extends(kernel_type) :: minval_X + private + type(arg_type) :: meta_args(2) = (/ & + arg_type(GH_SCALAR, GH_REAL, GH_MIN ), & + arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) & + /) + integer :: operates_on = DOF + contains + procedure, nopass :: minval_X_code + end type minval_X + + type, public, extends(kernel_type) :: maxval_X + private + type(arg_type) :: meta_args(2) = (/ & + arg_type(GH_SCALAR, GH_REAL, GH_MAX ), & + arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) & + /) + integer :: operates_on = DOF + contains + procedure, nopass :: maxval_X_code + end type maxval_X + + type, public, extends(kernel_type) :: min_max_X + private + type(arg_type) :: meta_args(3) = (/ & + arg_type(GH_SCALAR, GH_REAL, GH_MIN ), & + arg_type(GH_SCALAR, GH_REAL, GH_MAX ), & + arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) & + /) + integer :: operates_on = DOF + contains + procedure, nopass :: min_max_X_code + end type min_max_X + ! ------------------------------------------------------------------- ! ! ============== Converting real to integer field elements ========== ! ! ------------------------------------------------------------------- ! @@ -1169,6 +1207,16 @@ end subroutine min_aX_code subroutine inc_min_aX_code() end subroutine inc_min_aX_code + ! Minimum and maximum values contained within a field + subroutine minval_X_code() + end subroutine minval_X_code + + subroutine maxval_X_code() + end subroutine maxval_X_code + + subroutine min_max_X_code() + end subroutine min_max_X_code + ! Converting real to integer field elements subroutine real_to_int_X_code() end subroutine real_to_int_X_code diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index a04a46a4da..0c32c64365 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1975,6 +1975,21 @@ def test_int_to_real_x_precision(tmpdir, kind_name): assert LFRicBuild(tmpdir).code_compiles(psy) +def test_min_max_x(fortran_writer): + ''' + ''' + metadata = lfric_builtins.LFRicMinMaxXKern.metadata() + assert isinstance(metadata, LFRicKernelMetadata) + assert len(metadata.meta_args) == 3 + assert metadata.meta_args[0].access == "gh_min" + assert metadata.meta_args[1].access == "gh_max" + assert metadata.meta_args[2].access == "gh_read" + assert metadata.meta_args[2].function_space == "any_space_1" + kern = builtin_from_file("15.10.9_min_max_X_builtin.f90") + assert str(kern) == ("Built-in: minval_X (compute the global minimum " + "value contained in a field)") + + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' metadata = lfric_builtins.LFRicRealToIntXKern.metadata() 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 new file mode 100644 index 0000000000..e83f626a0a --- /dev/null +++ b/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 @@ -0,0 +1,52 @@ +! ----------------------------------------------------------------------------- +! BSD 3-Clause License +! +! Copyright (c) 2021-2025, 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: I. Kavcic, Met Office +! A. R. Porter, STFC Daresbury Laboratory + +program single_invoke + + ! Description: single point-wise operation (min/max of field elements) + ! specified in an invoke call. + use constants_mod, only: r_def + use field_mod, only: field_type + + implicit none + + type(field_type) :: f1 + real(r_def) :: amin, amax + + call invoke( minval_X(amin, f1), & + maxval_X(amax, f1) ) + +end program single_invoke From 3c6187eda64b811d527cc44eae345882cc52075f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 13:38:32 +0000 Subject: [PATCH 35/41] #2381 WIP on new builtins --- src/psyclone/domain/lfric/lfric_builtins.py | 4 ++-- .../tests/domain/lfric/lfric_builtins_test.py | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py index b11f11df33..d600e2e6ea 100644 --- a/src/psyclone/domain/lfric/lfric_builtins.py +++ b/src/psyclone/domain/lfric/lfric_builtins.py @@ -2784,8 +2784,8 @@ def lower_to_language_level(self) -> Node: arg_refs = self.get_indexed_field_argument_references() # Get a reference for the kernel scalar reduction argument. lhs = self._reduction_reference() - minval = IntrinsicCall(IntrinsicCall.Intrinsic.MINVAL, - [arg_refs[0]]) + minval = IntrinsicCall.create(IntrinsicCall.Intrinsic.MIN, + [lhs.copy(), arg_refs[0]]) return self._replace_with_assignment(lhs, minval) diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 0c32c64365..78f42673c9 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -77,9 +77,9 @@ API = "lfric" -def builtin_from_file(filename): +def builtin_from_file(filename: str): ''' - :param str filename: the name of the file to check for the builtin. + :param filename: the name of the file to check for the builtin. :returns: the first builtin in the first invoke. ''' _, invoke_info = parse(os.path.join(BASE_PATH, filename), api=API) @@ -1985,10 +1985,28 @@ def test_min_max_x(fortran_writer): assert metadata.meta_args[1].access == "gh_max" assert metadata.meta_args[2].access == "gh_read" assert metadata.meta_args[2].function_space == "any_space_1" - kern = builtin_from_file("15.10.9_min_max_X_builtin.f90") + metadata = lfric_builtins.LFRicMinvalXKern.metadata() + assert isinstance(metadata, LFRicKernelMetadata) + assert len(metadata.meta_args) == 2 + assert metadata.meta_args[0].access == "gh_min" + assert metadata.meta_args[1].access == "gh_read" + assert metadata.meta_args[1].function_space == "any_space_1" + metadata = lfric_builtins.LFRicMaxvalXKern.metadata() + assert isinstance(metadata, LFRicKernelMetadata) + assert len(metadata.meta_args) == 2 + assert metadata.meta_args[0].access == "gh_max" + assert metadata.meta_args[1].access == "gh_read" + assert metadata.meta_args[1].function_space == "any_space_1" + + _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, + dist_mem=False) + kern = invoke.schedule.kernels()[0] assert str(kern) == ("Built-in: minval_X (compute the global minimum " "value contained in a field)") + code = fortran_writer(kern) + assert "hllow" in code, code + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' From 3af3abb8763ccc822538a935b571f741e49a85e4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 13:46:37 +0000 Subject: [PATCH 36/41] #2381 revert reduction-related changes --- doc/developer_guide/APIs.rst | 18 +- doc/developer_guide/dependency.rst | 4 +- doc/developer_guide/psyir.rst | 2 +- doc/developer_guide/psykal.rst | 2 +- doc/user_guide/transformations.rst | 6 +- .../common/psylayer/global_reduction.py | 140 --------------- .../domain/lfric/lfric_global_reductions.py | 170 ------------------ src/psyclone/domain/lfric/lfric_invoke.py | 21 +-- src/psyclone/lfric.py | 117 +++++++----- .../psyir/transformations/extract_trans.py | 9 +- .../common/psylayer/global_reduction_test.py | 105 ----------- .../domain/lfric/lfric_global_max_test.py | 90 ---------- .../domain/lfric/lfric_global_min_test.py | 78 -------- .../domain/lfric/lfric_global_sum_test.py | 129 ------------- src/psyclone/tests/lfric_test.py | 67 ++++++- src/psyclone/tests/psyGen_test.py | 92 ++++++++-- src/psyclone/tests/psyir/nodes/node_test.py | 4 +- 17 files changed, 256 insertions(+), 798 deletions(-) delete mode 100644 src/psyclone/domain/common/psylayer/global_reduction.py delete mode 100644 src/psyclone/domain/lfric/lfric_global_reductions.py delete mode 100644 src/psyclone/tests/domain/common/psylayer/global_reduction_test.py delete mode 100644 src/psyclone/tests/domain/lfric/lfric_global_max_test.py delete mode 100644 src/psyclone/tests/domain/lfric/lfric_global_min_test.py delete mode 100644 src/psyclone/tests/domain/lfric/lfric_global_sum_test.py diff --git a/doc/developer_guide/APIs.rst b/doc/developer_guide/APIs.rst index be7c6e25c7..16a575a8c1 100644 --- a/doc/developer_guide/APIs.rst +++ b/doc/developer_guide/APIs.rst @@ -1044,9 +1044,21 @@ PSyIR for the arithmetic operations required by the particular BuiltIn. This PSyIR forms the new body of the dof loop containing the original BuiltIn node. -The sum and inner product BuiltIns require PSyIR support for -reductions. This is provided by the ``domain.common.psyir.GlobalReduction`` -class. +In constructing this PSyIR, suitable Symbols for the loop +variable and the various kernel arguments must be looked up. Since the +migration to the use of language-level PSyIR for the LFRic PSy layer +is at an early stage, in practise this often requires that suitable +Symbols be constructed and inserted into the symbol table of the PSy +layer routine. A lot of this work is currently performed in the +``LFRicKernelArgument.infer_datatype()`` method but ultimately (see +https://github.com/stfc/PSyclone/issues/1258) much of this will be +removed. + +The sum and inner product BuiltIns require extending PSyIR to handle +reductions in the ``GlobalSum`` class in ``psyGen.py``. Conversions from +``real`` to ``int`` and vice-versa require the target precisions be +available as symbols, which is being implemented as a part of the mixed +precision support. Kernel Metadata --------------- diff --git a/doc/developer_guide/dependency.rst b/doc/developer_guide/dependency.rst index e7c8ada05c..f8b5801604 100644 --- a/doc/developer_guide/dependency.rst +++ b/doc/developer_guide/dependency.rst @@ -180,7 +180,7 @@ evaluate the ordering constraints between between kernels. The `Argument` class is used to specify the data being passed into and out of instances of the `Kern` class, `HaloExchange` class and -`GlobalReduction` class (and their subclasses). +`GlobalSum` class (and their subclasses). As an illustration consider the following invoke:: @@ -253,7 +253,7 @@ exist. If there is a field vector associated with an instance of an `Argument` class then all of the data in its vector indices are assumed to be accessed when the argument is part of a `Kern` or a -`GlobalReduction`. However, in contrast, a `HaloExchange` only acts on a +`GlobalSum`. However, in contrast, a `HaloExchange` only acts on a single index of a field vector. Therefore there is one halo exchange per field vector index. For example:: diff --git a/doc/developer_guide/psyir.rst b/doc/developer_guide/psyir.rst index e2ce96cea3..c0fe5ccc7b 100644 --- a/doc/developer_guide/psyir.rst +++ b/doc/developer_guide/psyir.rst @@ -965,7 +965,7 @@ PSy-layer concepts are the singular units of computation that can be found inside a `PSyLoop`. * The `HaloExchange` is a distributed-memory concept in the PSy-layer. -* The `GlobalReduction` is a distributed-memory concept in the PSy-layer. +* The `GlobalSum` is a distributed-memory concept in the PSy-layer. Other specializations diff --git a/doc/developer_guide/psykal.rst b/doc/developer_guide/psykal.rst index b1aa88f65b..38281edf02 100644 --- a/doc/developer_guide/psykal.rst +++ b/doc/developer_guide/psykal.rst @@ -387,7 +387,7 @@ is shown below using the LFRic API as an illustration. The InvokeSchedule can currently contain nodes of type: **Loop**, **Kernel**, **Built-in** (see the :ref:`psykal-built-ins` section), **Directive** (of various types), **HaloExchange**, or -**GlobalReduction** (the latter two are only used if distributed memory is +**GlobalSum** (the latter two are only used if distributed memory is supported and is switched on; see the :ref:`psykal_usage` section). The order of the tree (depth first) indicates the order of the associated Fortran code. diff --git a/doc/user_guide/transformations.rst b/doc/user_guide/transformations.rst index 06699982e8..ef87ffab3d 100644 --- a/doc/user_guide/transformations.rst +++ b/doc/user_guide/transformations.rst @@ -717,9 +717,9 @@ PSyclone supports parallel scalar reductions. If a scalar reduction is specified in the Kernel metadata (see the API-specific sections for details) then PSyclone ensures the appropriate reduction is performed. -In the case of distributed memory, PSyclone will add **GlobalReduction's** -at the appropriate locations. Currently, only -"summation" reductions are supported for distributed memory (TODO #2381). +In the case of distributed memory, PSyclone will add **GlobalSum's** +at the appropriate locations. As can be inferred by the name, only +"summation" reductions are currently supported for distributed memory. In the case of an OpenMP parallel loop the standard reduction support will be used by default. For example diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py deleted file mode 100644 index 7f77502f7b..0000000000 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ /dev/null @@ -1,140 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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. -# ----------------------------------------------------------------------------- -# Author: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -''' This module contains the GlobalReduction node implementation.''' - -from __future__ import annotations -from abc import abstractmethod -import copy -from typing import Any - -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 Node, Statement - - -class GlobalReduction(Statement): - ''' - Generic global reduction operation. - - :param operand: the operand of the reduction operation. - - ''' - # Textual description of the node. - _children_valid_format = "" - _text_name = "GlobalReduction" - _colour = "cyan" - - def __init__(self, - operand: Any, - **kwargs): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - "It makes no sense to create a GlobalReduction object " - "when distributed memory is not enabled (dm=False).") - - # Ideally, 'operand' would be a child of this node but it's typically - # a KernelArgument, not a PSyIR Node. - # TODO Without this `copy`, the tests for the old-style DA fail. - self._operand = copy.copy(operand) - - if isinstance(operand, KernelArgument): - # Add old-style dependency information - # Here "readwrite" denotes how the class GlobalSum - # accesses/updates a scalar - self._operand.access = AccessType.READWRITE - self._operand.call = self - # Check that the global reduction argument is indeed a scalar - if not operand.is_scalar: - raise InternalError( - f"{type(self).__name__}.init(): A global reduction " - f"argument should be a scalar but found argument of type " - f"'{operand.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if operand.intrinsic_type != "real": - raise GenerationError( - f"{type(self).__name__} currently only supports real " - f"scalars, but argument '{operand.name}' in Kernel " - f"'{operand.call.name}' has '{operand.intrinsic_type}' " - f"intrinsic type.") - super().__init__(kwargs) - - @property - def operand(self) -> Any: - ''' - :returns: the operand of this global reduction. - ''' - return self._operand - - @property - def dag_name(self) -> str: - ''' - :returns: the name to use in the DAG for this node. - ''' - return (f"{type(self).__name__}({self._operand.name})" - f"_{self.position}") - - @property - def args(self) -> list[Any]: - ''' - :returns: the arguments associated with this node. Override - the base method and simply return the operand. - ''' - return [self._operand] - - 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)}[" - f"operand='{self._operand.name}']") - - @abstractmethod - def lower_to_language_level(self) -> Node: - ''' - Creates language-level PSyIR for this node and returns it. - - ''' diff --git a/src/psyclone/domain/lfric/lfric_global_reductions.py b/src/psyclone/domain/lfric/lfric_global_reductions.py deleted file mode 100644 index cf954d3567..0000000000 --- a/src/psyclone/domain/lfric/lfric_global_reductions.py +++ /dev/null @@ -1,170 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2017-2025, 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 implementations of the various global reduction - nodes supported in the LFRic DSL. ''' - -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.nodes import (Assignment, Call, Node, Reference, - StructureReference) -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - UnresolvedType) - - -class LFRicGlobalReduction(GlobalReduction): - ''' - LFRic-specific base class for all global-reduction operations. - - ''' - # The name of the reduction being performed. - _reduction_name = "" - # The method of the lfric_mpi_type object that must be called to - # perform the reduction. - _method_name = "" - - def lower_to_language_level(self) -> Node: - ''' - Replaces this node with its language-level representation. - - This method is parameterised using self._reduction_name and - self._method_name so that it can be used for both max and min. - - :returns: this node lowered to language-level PSyIR. - - ''' - # Get the name strings to use - name = self._operand.name - - symtab = self.ancestor(InvokeSchedule).symbol_table - - # Symbol holding the local value. - loc_min = symtab.lookup(name) - - # Symbol holding the global value. - result = symtab.new_symbol(f"glob_{name}", symbol_type=DataSymbol, - datatype=loc_min.datatype.copy()) - - # The code to get the MPI object is generated by lfric.LFRicProxies - # since it requires a proxy. - mpi_obj = symtab.lookup_with_tag("mpi") - - # Call the method to compute the global min. - sref = StructureReference.create(mpi_obj, [self._method_name]) - call = Call.create(sref, [Reference(loc_min), Reference(result)]) - call.preceding_comment = f"Perform global {self._reduction_name}" - self.parent.addchild(call, self.position) - assign = Assignment.create(lhs=Reference(loc_min), - rhs=Reference(result)) - return self.replace_with(assign) - - -class LFRicGlobalMax(LFRicGlobalReduction): - ''' - Represents the operation to find the global maximum value of a scalar. - - ''' - _reduction_name = "max" - _method_name = "global_max" - _text_name = "GlobalMax" - - -class LFRicGlobalMin(LFRicGlobalReduction): - ''' - Represents the operation to find the global minimum value of a scalar. - - ''' - _reduction_name = "min" - _method_name = "global_min" - _text_name = "GlobalMin" - - -class LFRicGlobalSum(LFRicGlobalReduction): - ''' - Represents a global sum in the LFRic DSL. - - ''' - _text_name = "GlobalSum" - - def lower_to_language_level(self) -> Node: - ''' - :returns: this node lowered to language-level PSyIR. - - ''' - # Get the name strings to use - name = self._operand.name - type_name = self._operand.data_type - mod_name = self._operand.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) - - -# ============================================================================= -# Documentation utils: The list of module members that we wish AutoAPI to -# generate documentation for. -__all__ = [ - 'LFRicGlobalReduction', - 'LFRicGlobalMax', - 'LFRicGlobalMin', - 'LFRicGlobalSum' -] diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 847ed465ea..f5a73639fc 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -84,17 +84,16 @@ def __init__(self, alg_invocation, idx, invokes): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel - from psyclone.lfric import (LFRicFunctionSpaces, LFRicLMAOperators, + from psyclone.lfric import (LFRicFunctionSpaces, LFRicGlobalSum, + LFRicLMAOperators, LFRicReferenceElement, LFRicCMAOperators, LFRicBasisFunctions, LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, - LFRicRunTimeChecks, LFRicScalarArgs, LFRicFields, LFRicDofmaps, - LFRicStencils) - from psyclone.domain.lfric.lfric_global_reductions import ( - LFRicGlobalSum, LFRicGlobalMin, LFRicGlobalMax) + LFRicRunTimeChecks, + LFRicScalarArgs, LFRicFields, LFRicDofmaps, LFRicStencils) self.scalar_args = LFRicScalarArgs(self) @@ -173,24 +172,20 @@ def __init__(self, alg_invocation, idx, invokes): # Lastly, add in halo exchange calls and global sums if # required. We only need to add halo exchange calls for fields # since operators are assembled in place and scalars don't - # have halos. We only need to add global reductions for scalars - # which have a 'gh_sum/max/min' access. - rmap = {AccessType.SUM: LFRicGlobalSum, - AccessType.MAX: LFRicGlobalMax, - AccessType.MIN: LFRicGlobalMin} + # have halos. We only need to add global sum calls for scalars + # which have a 'gh_sum' access. if Config.get().distributed_memory: # halo exchange calls const = LFRicConstants() for loop in self.schedule.loops(): loop.create_halo_exchanges() - # global reduction calls + # global sum calls for loop in self.schedule.loops(): for scalar in loop.args_filter( arg_types=const.VALID_SCALAR_NAMES, arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): - global_sum = rmap[scalar.access]( - scalar, parent=loop.parent) + global_sum = LFRicGlobalSum(scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) # 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 747454167f..711d8390ba 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) + KernelArgument, HaloExchange, GlobalSum) from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import ( Reference, ACCEnterDataDirective, ArrayOfStructuresReference, @@ -1313,21 +1313,6 @@ def __init__(self, node): rank = 1 if arg not in op_args else 3 self._add_symbol(new_name, tag, intrinsic_type, arg, rank) - if self._invoke.schedule.reductions(): - # We have one or more reductions so we need to obtain the MPI - # object from one of the field arguments. - # We'll need the LFRic mpi_type. - mpi_mod = self.symtab.find_or_create_tag( - "lfric_mpi_mod", symbol_type=ContainerSymbol) - mpi_type = self.symtab.find_or_create_tag( - "lfric_mpi_type", - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(mpi_mod)) - self.symtab.find_or_create_tag("mpi", - symbol_type=DataSymbol, - datatype=mpi_type) - def _add_symbol(self, name, tag, intrinsic_type, arg, rank): ''' Creates a new DataSymbol representing either an LFRic field or @@ -1419,7 +1404,7 @@ def invoke_declarations(self): # field-type proxies for (fld_type, fld_mod), args in field_datatype_map.items(): fld_mod_symbol = table.node.parent.symbol_table.lookup(fld_mod) - fld_type_sym = table.node.parent.symbol_table.find_or_create_tag( + fld_type_sym = table.node.parent.symbol_table.new_symbol( fld_type, symbol_type=DataTypeSymbol, datatype=UnresolvedType(), @@ -1483,15 +1468,13 @@ def initialise(self, cursor: int) -> int: ''' Insert code into the PSy layer to initialise all necessary proxies. - :param cursor: position at which to add the next initialisation + :param cursor: position where to add the next initialisation statements. :returns: Updated cursor value. :raises InternalError: if a kernel argument of an unrecognised type is encountered. - :raises InternalError: if the invoke contains one or more reductions - but no fields. ''' init_cursor = cursor @@ -1582,26 +1565,6 @@ def initialise(self, cursor: int) -> int: self._invoke.schedule[init_cursor].preceding_comment = ( "Initialise field and/or operator proxies") - if self._invoke.schedule.reductions(): - # Now that we've initialised the field proxies, we can get the - # MPI object. - for sym in self.symtab.datasymbols: - if (isinstance(sym.datatype, DataTypeSymbol) and - sym.datatype.name == "field_proxy_type"): - break - else: - raise InternalError( - f"Invoke '{self._invoke.name}' contains one or more " - f"reductions ({self._invoke.schedule.reductions()}) but " - f"does not access any fields.") - get_mpi = StructureReference.create(sym, ["get_mpi"]) - mpi_obj = self.symtab.lookup_with_tag("mpi") - self._invoke.schedule.addchild( - Assignment.create(lhs=Reference(mpi_obj), - rhs=Call.create(get_mpi)), - index=cursor) - cursor += 1 - return cursor @@ -3928,6 +3891,79 @@ 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 @@ -6557,6 +6593,7 @@ def data_on_device(self, _): 'LFRicInterGrid', 'LFRicBasisFunctions', 'LFRicBoundaryConditions', + 'LFRicGlobalSum', 'LFRicHaloExchange', 'LFRicHaloExchangeStart', 'LFRicHaloExchangeEnd', diff --git a/src/psyclone/psyir/transformations/extract_trans.py b/src/psyclone/psyir/transformations/extract_trans.py index 23d971d177..4d8232143a 100644 --- a/src/psyclone/psyir/transformations/extract_trans.py +++ b/src/psyclone/psyir/transformations/extract_trans.py @@ -39,8 +39,7 @@ of an Invoke into a stand-alone application." ''' -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.psyGen import BuiltIn, Kern, HaloExchange +from psyclone.psyGen import BuiltIn, Kern, HaloExchange, GlobalSum from psyclone.psyir.nodes import (CodeBlock, ExtractNode, Loop, Schedule, Directive, OMPParallelDirective, ACCParallelDirective) @@ -65,15 +64,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, GlobalReduction) + HaloExchange, GlobalSum) 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 deleted file mode 100644 index fabf2b99f8..0000000000 --- a/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py +++ /dev/null @@ -1,105 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2017-2025, 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 -# ----------------------------------------------------------------------------- - -''' -Module containing pytest tests for the GlobalReduction class. -''' - -import pytest - -from psyclone.core import AccessType -from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction) -from psyclone.errors import GenerationError -from psyclone.psyGen import Kern -from psyclone.psyir.nodes import colored, Literal, Reference -from psyclone.psyir.symbols import INTEGER_TYPE, Symbol -from psyclone.tests.utilities import get_invoke - - -def test_globalreduction_init(): - ''' - Test the constructor of GlobalReduction. - ''' - gred = GlobalReduction(operand=Reference(Symbol("a"))) - assert gred.operand.name == "a" - assert gred.args == [gred.operand] - - # Construct with a KernelArgument. These are not easy to make so we create - # PSyIR for a PSyKAl invoke first. - _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", - api="lfric", dist_mem=True, idx=0) - schedule = invoke.schedule - kernels = schedule.walk(Kern) - # Get hold of a KernelArgument object - karg = kernels[1].args[0] - gred = GlobalReduction(operand=karg) - assert gred.operand.access == AccessType.READWRITE - assert gred.operand.call == gred - - -def test_globalreduction_dag_name(): - ''' - Test the dag_name property. - ''' - gred = GlobalReduction(operand=Reference(Symbol("a"))) - assert gred.dag_name == "GlobalReduction(a)_0" - - -def test_globalreduction_node_str(): - ''' - Test the node_str method in the GlobalReduction class. - ''' - gred = GlobalReduction(operand=Reference(Symbol("a"))) - output = str(gred) - expected_output = (colored("GlobalReduction", GlobalReduction._colour) + - "[operand='a']") - assert expected_output in output - - -def test_globalsum_children_validation(): - '''Test that children added to GlobalReduction are validated. A - GlobalReduction node does not accept any children. - - ''' - gsum = GlobalReduction(operand=Reference(Symbol("a"))) - with pytest.raises(GenerationError) as excinfo: - gsum.addchild(Literal("2", INTEGER_TYPE)) - assert ("Item 'Literal' can't be child 0 of 'GlobalReduction'. " - "GlobalReduction is a LeafNode and doesn't accept children." - in str(excinfo.value)) diff --git a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py b/src/psyclone/tests/domain/lfric/lfric_global_max_test.py deleted file mode 100644 index 55f9579839..0000000000 --- a/src/psyclone/tests/domain/lfric/lfric_global_max_test.py +++ /dev/null @@ -1,90 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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. -# ----------------------------------------------------------------------------- -# Author A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Module containing pytest tests for the LFRicGlobalMax class.''' - -from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMax -from psyclone.psyGen import Kern -from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, - UnresolvedType) -from psyclone.tests.utilities import get_invoke - -TEST_API = "lfric" - - -def test_lgmax_in_invoke(): - ''' - Test the construction of an LFRicGlobalMax object. - - This is complicated by the need to supply it with an LFRicKernelArgument - and therefore we use a full example to get hold of a suitable argument. - ''' - psy, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", - TEST_API, dist_mem=True, - idx=0) - sched = invoke.schedule - - # Find a suitable kernel argument (real scalar). - kernel = sched.walk(Kern)[0] - for arg in kernel.args: - if arg.is_scalar and arg.intrinsic_type == "real": - break - - lgm = LFRicGlobalMax(operand=arg) - assert isinstance(lgm, LFRicGlobalMax) - assert lgm.operand is not arg - assert lgm.operand.name == arg.name - csym = sched.symbol_table.new_symbol("lfric_mpi_mod", - symbol_type=ContainerSymbol) - mtype = sched.symbol_table.new_symbol("mpi_type", - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(csym)) - sched.symbol_table.find_or_create_tag("mpi", - symbol_type=DataSymbol, - datatype=mtype) - - sched.addchild(lgm) - output = psy.gen - assert "real(kind=r_def) :: glob_a" in output, output - assert '''\ - ! Perform global max - call mpi%global_max(a, glob_a) - a = glob_a''' in output, output - - # Can't compile this because we're assigning to a read-only scalar - # argument. diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py deleted file mode 100644 index 16cb973955..0000000000 --- a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py +++ /dev/null @@ -1,78 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2025, 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. -# ----------------------------------------------------------------------------- -# Author A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Module containing pytest tests for the LFRicGlobalMin class.''' - -from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalMin -from psyclone.psyGen import Kern -from psyclone.tests.utilities import get_invoke - -TEST_API = "lfric" - - -def test_lgm_in_invoke(): - ''' - Test the construction of an LFRicGlobalMin object. - - This is complicated by the need to supply it with an LFRicKernelArgument - and therefore we use a full example to get hold of a suitable argument. - ''' - psy, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", - TEST_API, dist_mem=True, - idx=0) - sched = invoke.schedule - - # Find a suitable kernel argument (real scalar). - kernel = sched.walk(Kern)[0] - for arg in kernel.args: - if arg.is_scalar and arg.intrinsic_type == "real": - break - - lgm = LFRicGlobalMin(operand=arg) - assert isinstance(lgm, LFRicGlobalMin) - assert lgm.operand is not arg - assert lgm.operand.name == arg.name - - sched.addchild(lgm) - output = psy.gen - assert "use lfric_mpi_mod, only : lfric_mpi_type" in output - assert "type(lfric_mpi_type) :: mpi" in output - assert "real(kind=r_def) :: glob_a" in output - assert "mpi = f1_proxy%get_mpi()" in output - assert '''\ - ! Perform global min - call mpi%global_min(a, glob_a) - a = glob_a''' in output diff --git a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py deleted file mode 100644 index 9a5fe75d27..0000000000 --- a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py +++ /dev/null @@ -1,129 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2017-2025, 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 and S. Siso, STFC Daresbury Lab -# Modified I. Kavcic, A. Coughtrie, L. Turner and O. Brunt, Met Office -# Modified J. Henrichs, Bureau of Meteorology -# Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -'''Module containing pytest tests for the LFRicGlobalSum class.''' - -import pytest - -from psyclone.core import AccessType -from psyclone.domain.lfric.lfric_global_reductions import LFRicGlobalSum -from psyclone.errors import GenerationError, InternalError -from psyclone.tests.utilities import get_invoke - -TEST_API = "lfric" - - -def test_lfricglobalreduction_unsupported_argument(): - ''' Check that an instance of the LFRicGlobalReduction 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", TEST_API, - 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: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum.init(): A global reduction argument should " - "be a scalar but found argument of type 'gh_field'." - in str(err.value)) - - -def test_lfricglobalreduction_unsupported_scalar(): - ''' Check that an instance of the LFRicGlobalReduction 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", - 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)) - - -def test_lfricglobalreduction_nodm_error(): - ''' Check that an instance of the LFRicGlobalReduction class raises an - exception if it is instantiated with no distributed memory enabled - (dm=False). - - ''' - # Get an instance of a real scalar - _, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", - TEST_API, 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: - _ = LFRicGlobalSum(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_arg(): - ''' Check that the globalreduction operand is defined as gh_readwrite and - points to the GlobalSum node ''' - _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", - api="lfric", dist_mem=True, idx=0) - schedule = invoke.schedule - glob_sum = schedule.children[2] - glob_sum_arg = glob_sum.operand - 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.operand diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index 0abb89661c..5562a03094 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -52,7 +52,7 @@ LFRicKernMetadata, LFRicLoop) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.lfric import ( - LFRicACCEnterDataDirective, LFRicBoundaryConditions, + LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicGlobalSum, LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloReadAccess, KernCallArgList) from psyclone.errors import FieldNotFoundError, GenerationError, InternalError @@ -2924,6 +2924,71 @@ 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 524c046cb3..93055f3605 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -62,7 +62,7 @@ from psyclone.domain.lfric import (lfric_builtins, LFRicInvokeSchedule, LFRicKern, LFRicKernMetadata) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans -from psyclone.lfric import LFRicKernelArguments +from psyclone.lfric import LFRicGlobalSum, LFRicKernelArguments from psyclone.errors import FieldNotFoundError, GenerationError, InternalError from psyclone.generator import generate from psyclone.gocean1p0 import GOKern @@ -70,7 +70,7 @@ from psyclone.psyGen import (TransInfo, PSyFactory, InlinedKern, object_index, HaloExchange, Invoke, DataAccess, Kern, Arguments, CodedKern, Argument, - InvokeSchedule) + GlobalSum, InvokeSchedule) from psyclone.psyir.nodes import (Assignment, BinaryOperation, Container, Literal, Loop, Node, KernelSchedule, Call, colored, Schedule) @@ -1005,6 +1005,48 @@ 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 @@ -1103,21 +1145,10 @@ def test_reduction_var_invalid_scalar_error(dist_mem): # REALs and INTEGERs are fine assert call.arguments.args[0].intrinsic_type == 'real' call._reduction_arg = call.arguments.args[0] - # Have to pretend this arg has a reduction access. - call._reduction_arg._access = AccessType.MIN call.initialise_reduction_variable() assert call.arguments.args[6].intrinsic_type == 'integer' call._reduction_arg = call.arguments.args[6] - # Have to pretend this arg has a reduction access. - call._reduction_arg._access = AccessType.MAX call.initialise_reduction_variable() - # An invalid reduction access. - call._reduction_arg._access = AccessType.INC - with pytest.raises(GenerationError) as err: - call.initialise_reduction_variable() - assert ("Kernel 'testkern_three_scalars_code' performs a reduction of " - "type 'INC' but this is not supported by Kern.initialise_" - "reduction_variable()" in str(err.value)) def test_reduction_sum_error(dist_mem): @@ -1365,7 +1396,7 @@ def test_argument_find_argument(): schedule = invoke.schedule # a) globalsum arg depends on kern arg kern_asum_arg = schedule.children[3].loop_body[0].arguments.args[1] - glob_sum_arg = schedule.children[2].operand + glob_sum_arg = schedule.children[2].scalar result = kern_asum_arg._find_argument(schedule.children) assert result == glob_sum_arg # b) kern arg depends on globalsum arg @@ -1407,6 +1438,21 @@ 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''' @@ -1504,7 +1550,7 @@ def test_argument_forward_dependence(monkeypatch, annexed): schedule = invoke.schedule prev_arg = schedule.children[0].loop_body[0].arguments.args[1] sum_arg = schedule.children[1].loop_body[0].arguments.args[0] - global_sum_arg = schedule.children[2].operand + global_sum_arg = schedule.children[2].scalar next_arg = schedule.children[3].loop_body[0].arguments.args[1] # a) prev kern arg depends on sum result = prev_arg.forward_dependence() @@ -1571,7 +1617,7 @@ def test_argument_backward_dependence(monkeypatch, annexed): schedule = invoke.schedule prev_arg = schedule.children[0].loop_body[0].arguments.args[1] sum_arg = schedule.children[1].loop_body[0].arguments.args[0] - global_sum_arg = schedule.children[2].operand + global_sum_arg = schedule.children[2].scalar next_arg = schedule.children[3].loop_body[0].arguments.args[1] # a) next kern arg depends on global sum arg result = next_arg.backward_dependence() @@ -1662,6 +1708,20 @@ 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 cee0a7328e..4445a64747 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -764,13 +764,15 @@ def test_dag_names(): idx = aref.children[0].detach() assert idx.dag_name == "Literal_0" - # BuiltIn has a specialised dag_name + # GlobalSum and BuiltIn also have 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" From 2f4989748d05fa77d9c44991be230b38038b1207 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 13:57:38 +0000 Subject: [PATCH 37/41] #3263 fix remaining tests --- src/psyclone/psyGen.py | 111 +++++++++++------- .../tests/domain/lfric/lfric_builtins_test.py | 2 +- .../lfric_transformations_test.py | 9 +- 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 440cda5fbd..25457142a0 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -63,7 +63,7 @@ from psyclone.psyir.nodes import ( ArrayReference, Call, Container, Literal, Loop, Node, OMPDoDirective, Reference, Directive, Routine, Schedule, Statement, Assignment, - IntrinsicCall, BinaryOperation, FileContainer, UnaryOperation) + IntrinsicCall, BinaryOperation, FileContainer) from psyclone.psyir.symbols import ( ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, ScalarType, UnresolvedType, ImportInterface, INTEGER_TYPE, RoutineSymbol) @@ -713,6 +713,65 @@ 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 @@ -992,7 +1051,6 @@ def initialise_reduction_variable(self) -> None: configuration file) is less than 1. :raises GenerationError: for a reduction into a scalar that is neither 'real' nor 'integer'. - :raises GenerationError: for an unsupported type of reduction. ''' var_arg = self._reduction_arg @@ -1012,27 +1070,6 @@ def initialise_reduction_variable(self) -> None: f"'real' or an 'integer' scalar but found scalar of type " f"'{var_arg.intrinsic_type}'.") - # Create the initialisation expression - this depends upon the type - # of reduction being performed. - if var_arg.access == AccessType.SUM: - # Sum - initialise to zero. - init_val = Literal("0", variable.datatype.copy()) - elif var_arg.access == AccessType.MIN: - # Minimum - initialise to HUGE. - init_val = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, - [Reference(variable)]) - elif var_arg.access == AccessType.MAX: - # Maximum - initialise to -HUGE. - huge = IntrinsicCall.create(IntrinsicCall.Intrinsic.HUGE, - [Reference(variable)]) - init_val = UnaryOperation.create(UnaryOperation.Operator.MINUS, - huge) - else: - raise GenerationError( - f"Kernel '{self.name}' performs a reduction of type " - f"'{var_arg.access}' but this is not supported by Kern." - f"initialise_reduction_variable()") - # Find a safe location to initialise it. insert_loc = self.ancestor((Loop, Directive)) while insert_loc: @@ -1042,7 +1079,9 @@ def initialise_reduction_variable(self) -> None: insert_loc = loc cursor = insert_loc.position insert_loc = insert_loc.parent - new_node = Assignment.create(lhs=Reference(variable), rhs=init_val) + 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 @@ -1888,12 +1927,12 @@ class DataAccess(): def __init__(self, arg): '''Store the argument associated with the instance of this class and - the Call, HaloExchange or GlobalReduction (or a subclass thereof) + the Call, HaloExchange or GlobalSum (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) + `GlobalSum` (or a subclass thereof) :type arg: :py:class:`psyclone.psyGen.Argument` ''' @@ -2355,13 +2394,8 @@ def _find_argument(self, nodes): :rtype: :py:class:`psyclone.psyGen.Argument` ''' - # pylint: disable=import-outside-toplevel - from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction) - nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, HaloExchange, - GlobalReduction))] + isinstance(x, (Kern, HaloExchange, GlobalSum))] for node in nodes_with_args: for argument in node.args: if self._depends_on(argument): @@ -2386,12 +2420,8 @@ def _find_read_arguments(self, nodes): return [] # We only need consider nodes that have arguments - # pylint: disable=import-outside-toplevel - from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction) nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, HaloExchange, - GlobalReduction))] + isinstance(x, (Kern, HaloExchange, GlobalSum))] access = DataAccess(self) arguments = [] for node in nodes_with_args: @@ -2431,11 +2461,8 @@ def _find_write_arguments(self, nodes, ignore_halos=False): return [] # We only need consider nodes that have arguments - # pylint: disable=import-outside-toplevel - from psyclone.domain.common.psylayer.global_reduction import ( - GlobalReduction) nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, GlobalReduction)) or + isinstance(x, (Kern, GlobalSum)) or (isinstance(x, HaloExchange) and not ignore_halos)] access = DataAccess(self) arguments = [] @@ -2897,6 +2924,6 @@ def validate_options(self, **kwargs): # For Sphinx AutoAPI documentation generation __all__ = ['PSyFactory', 'PSy', 'Invokes', 'Invoke', 'InvokeSchedule', - 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', + 'GlobalSum', 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', 'BuiltIn', 'Arguments', 'DataAccess', 'Argument', 'KernelArgument', 'TransInfo', 'Transformation'] diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 78f42673c9..941ae09d8b 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -2005,7 +2005,7 @@ def test_min_max_x(fortran_writer): "value contained in a field)") code = fortran_writer(kern) - assert "hllow" in code, code + assert "amin = MIN(amin, f1_data(df))" in code, code def test_real_to_int_x(fortran_writer): 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 3e5ae68083..d0bf730203 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -46,14 +46,13 @@ from psyclone.configuration import Config from psyclone.core import AccessType, Signature -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.domain.lfric import LFRicLoop from psyclone.lfric import (LFRicHaloExchangeStart, LFRicHaloExchangeEnd, LFRicHaloExchange) from psyclone.errors import GenerationError, InternalError -from psyclone.psyGen import InvokeSchedule, BuiltIn +from psyclone.psyGen import InvokeSchedule, GlobalSum, BuiltIn from psyclone.psyir.backend.visitor import VisitorError from psyclone.psyir.nodes import ( colored, Loop, Schedule, Literal, Directive, OMPDoDirective, @@ -3839,7 +3838,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", GlobalReduction._colour) + gsum = colored("GlobalSum", GlobalSum._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) @@ -3878,7 +3877,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "1: " + gsum + "[operand='asum']\n" + + indent + "1: " + gsum + "[scalar='asum']\n" + indent + "2: " + ompparallel + "[]\n" + 2*indent + sched + "[]\n" + 3*indent + "0: " + ompdo + "[omp_schedule=static]\n" + @@ -3909,7 +3908,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): 2*indent + ompdefault + "[default=DefaultClauseTypes.SHARED]\n" + 2*indent + ompprivate + "[]\n" + 2*indent + ompfprivate + "[]\n" + - indent + "4: " + gsum + "[operand='bsum']\n") + indent + "4: " + gsum + "[scalar='bsum']\n") if not annexed: expected = expected.replace("nannexed", "ndofs") else: # not dist_mem. annexed can be True or False From e3dc204bc4066a4b6b428a6b236931e291519da8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 14:23:36 +0000 Subject: [PATCH 38/41] #3263 document new builtins --- doc/user_guide/lfric.rst | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index e449270675..0ca362f7c6 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -2797,7 +2797,8 @@ are listed in the table below. +===============+=====================+================+====================+ | GH_SCALAR | GH_INTEGER | n/a | GH_READ | +---------------+---------------------+----------------+--------------------+ -| GH_SCALAR | GH_REAL | n/a | GH_READ, GH_SUM | +| GH_SCALAR | GH_REAL | n/a | GH_READ, GH_SUM, | +| | | | GH_MIN, GH_MAX | +---------------+---------------------+----------------+--------------------+ | GH_FIELD | GH_REAL, GH_INTEGER | ANY_SPACE_ | GH_READ, GH_WRITE, | | | | | GH_READWRITE | @@ -3455,6 +3456,30 @@ the same field (``X = min(a, X)``):: field(:) = MIN(rscalar, field(:)) +Global minimum and maximum field-element values +############################################### + +Built-ins which scan through all elements of a field and return its +maximum or minimum value. + +minval_X +^^^^^^^^ + +**minval_X** (*rscalar*, **field**) + +Returns the minimum value held in the field *field*:: + + rscalar = MINVAL(field(:)) + +maxval_X +^^^^^^^^ + +**maxval_X** (*rscalar*, **field**) + +Returns the maximum value held in the field *field*:: + + rscalar = MAXVAL(field(:)) + Conversion of ``real`` field elements ##################################### From 60db4b92174f7ae904fde76085929a94d739acc0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 14:30:34 +0000 Subject: [PATCH 39/41] #3263 improve testing --- src/psyclone/domain/lfric/lfric_builtins.py | 6 +++--- .../tests/domain/lfric/lfric_builtins_test.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py index d600e2e6ea..46aa6e5e50 100644 --- a/src/psyclone/domain/lfric/lfric_builtins.py +++ b/src/psyclone/domain/lfric/lfric_builtins.py @@ -2807,7 +2807,7 @@ def metadata(cls) -> LFRicKernelMetadata: FieldArgMetadata("gh_real", "gh_read", "any_space_1")]) def __str__(self): - return (f"Built-in: {self._case_name} (compute the global maximimum " + return (f"Built-in: {self._case_name} (compute the global maximum " f"value contained in a field)") def lower_to_language_level(self) -> Node: @@ -2823,8 +2823,8 @@ def lower_to_language_level(self) -> Node: arg_refs = self.get_indexed_field_argument_references() # Get a reference for the kernel scalar reduction argument. lhs = self._reduction_reference() - minval = IntrinsicCall(IntrinsicCall.Intrinsic.MAXVAL, - [arg_refs[0]]) + minval = IntrinsicCall.create(IntrinsicCall.Intrinsic.MAX, + [lhs.copy(), arg_refs[0]]) return self._replace_with_assignment(lhs, minval) diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 941ae09d8b..3b712115ad 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1975,8 +1975,9 @@ def test_int_to_real_x_precision(tmpdir, kind_name): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_min_max_x(fortran_writer): +def test_minmaxval_x(fortran_writer): ''' + Tests for the minval_x and maxval_x builtins. ''' metadata = lfric_builtins.LFRicMinMaxXKern.metadata() assert isinstance(metadata, LFRicKernelMetadata) @@ -2000,13 +2001,17 @@ def test_min_max_x(fortran_writer): _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, dist_mem=False) - kern = invoke.schedule.kernels()[0] - assert str(kern) == ("Built-in: minval_X (compute the global minimum " - "value contained in a field)") - - code = fortran_writer(kern) + kerns = invoke.schedule.kernels() + assert str(kerns[0]) == ("Built-in: minval_X (compute the global minimum " + "value contained in a field)") + code = fortran_writer(kerns[0]) assert "amin = MIN(amin, f1_data(df))" in code, code + assert str(kerns[1]) == ("Built-in: maxval_X (compute the global maximum " + "value contained in a field)") + code = fortran_writer(kerns[1]) + assert "amax = MAX(amax, f1_data(df))" in code, code + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' From abd054f34972819293acdc21cf23c1115b486d4b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Dec 2025 15:16:54 +0000 Subject: [PATCH 40/41] #3263 rm the min_max version and ensure code-gen is rejected when DM enabled --- doc/user_guide/lfric.rst | 4 ++ src/psyclone/domain/lfric/lfric_builtins.py | 41 ------------------- src/psyclone/domain/lfric/lfric_invoke.py | 7 +++- src/psyclone/parse/lfric_builtins_mod.f90 | 15 ------- .../tests/domain/lfric/lfric_builtins_test.py | 15 +++---- 5 files changed, 18 insertions(+), 64 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 0ca362f7c6..509fda4026 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -3462,6 +3462,10 @@ Global minimum and maximum field-element values Built-ins which scan through all elements of a field and return its maximum or minimum value. +.. warning:: + Support for these built-ins is not yet complete and therefore they + cannot currently be used. TODO #2381. + minval_X ^^^^^^^^ diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py index 46aa6e5e50..1c4ca8e141 100644 --- a/src/psyclone/domain/lfric/lfric_builtins.py +++ b/src/psyclone/domain/lfric/lfric_builtins.py @@ -2827,46 +2827,6 @@ def lower_to_language_level(self) -> Node: [lhs.copy(), arg_refs[0]]) return self._replace_with_assignment(lhs, minval) - -class LFRicMinMaxXKern(LFRicBuiltIn): - ''' - Computes the (global) minimum and maximum scalar values held in - the supplied field. - ''' - _case_name = "min_max_X" - _datatype = "real" - - @classmethod - def metadata(cls) -> LFRicKernelMetadata: - """ - :returns: kernel metadata describing this built-in. - """ - return cls._builtin_metadata([ - ScalarArgMetadata("gh_real", "gh_min"), - ScalarArgMetadata("gh_real", "gh_max"), - FieldArgMetadata("gh_real", "gh_read", "any_space_1")]) - - def __str__(self): - return (f"Built-in: {self._case_name} (compute the global minimum and " - f"maximum values contained in a field)") - - def lower_to_language_level(self) -> Node: - ''' - Lowers this LFRic-specific built-in kernel to language-level PSyIR. - This BuiltIn node is replaced by an Assignment node. - - :returns: the lowered version of this node. - - ''' - super().lower_to_language_level() - # Get indexed references for the field (proxy) argument. - arg_refs = self.get_indexed_field_argument_references() - # Get a reference for the kernel scalar reduction argument. - lhs = self._reduction_reference() - minval = IntrinsicCall(IntrinsicCall.Intrinsic.MINVAL, - [arg_refs[0]]) - return self._replace_with_assignment(lhs, minval) - # ------------------------------------------------------------------- # # ============== Converting real to integer field elements ========== # # ------------------------------------------------------------------- # @@ -3397,7 +3357,6 @@ def lower_to_language_level(self) -> Node: # Minimum and maximum values contained in a field "minval_X": LFRicMinvalXKern, "maxval_X": LFRicMaxvalXKern, - "min_max_X": LFRicMinMaxXKern, # Converting real to integer field elements "real_to_int_X": LFRicRealToIntXKern, # Converting real to real field elements diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index f5a73639fc..644a88f64d 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -65,7 +65,8 @@ class LFRicInvoke(Invoke): :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. ''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-locals @@ -185,6 +186,10 @@ def __init__(self, alg_invocation, idx, invokes): arg_types=const.VALID_SCALAR_NAMES, arg_accesses=AccessType.get_valid_reduction_modes(), unique=True): + if scalar.access != AccessType.SUM: + raise GenerationError( + "TODO #2381 - currently only global *sum* " + "reductions are supported.") global_sum = LFRicGlobalSum(scalar, parent=loop.parent) loop.parent.children.insert(loop.position+1, global_sum) diff --git a/src/psyclone/parse/lfric_builtins_mod.f90 b/src/psyclone/parse/lfric_builtins_mod.f90 index 33fe1e51fd..43193a882e 100644 --- a/src/psyclone/parse/lfric_builtins_mod.f90 +++ b/src/psyclone/parse/lfric_builtins_mod.f90 @@ -698,18 +698,6 @@ module lfric_builtins_mod procedure, nopass :: maxval_X_code end type maxval_X - type, public, extends(kernel_type) :: min_max_X - private - type(arg_type) :: meta_args(3) = (/ & - arg_type(GH_SCALAR, GH_REAL, GH_MIN ), & - arg_type(GH_SCALAR, GH_REAL, GH_MAX ), & - arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) & - /) - integer :: operates_on = DOF - contains - procedure, nopass :: min_max_X_code - end type min_max_X - ! ------------------------------------------------------------------- ! ! ============== Converting real to integer field elements ========== ! ! ------------------------------------------------------------------- ! @@ -1214,9 +1202,6 @@ end subroutine minval_X_code subroutine maxval_X_code() end subroutine maxval_X_code - subroutine min_max_X_code() - end subroutine min_max_X_code - ! Converting real to integer field elements subroutine real_to_int_X_code() end subroutine real_to_int_X_code diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 3b712115ad..a291fb5a44 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1979,13 +1979,6 @@ def test_minmaxval_x(fortran_writer): ''' Tests for the minval_x and maxval_x builtins. ''' - metadata = lfric_builtins.LFRicMinMaxXKern.metadata() - assert isinstance(metadata, LFRicKernelMetadata) - assert len(metadata.meta_args) == 3 - assert metadata.meta_args[0].access == "gh_min" - assert metadata.meta_args[1].access == "gh_max" - assert metadata.meta_args[2].access == "gh_read" - assert metadata.meta_args[2].function_space == "any_space_1" metadata = lfric_builtins.LFRicMinvalXKern.metadata() assert isinstance(metadata, LFRicKernelMetadata) assert len(metadata.meta_args) == 2 @@ -2012,6 +2005,14 @@ def test_minmaxval_x(fortran_writer): code = fortran_writer(kerns[1]) 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)) + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' From 7a0d5638695f1e4aaf3af241f919617100d8e1c2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 7 Jan 2026 13:05:09 +0000 Subject: [PATCH 41/41] #3263 fix pesky tabs --- config/psyclone.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/psyclone.cfg b/config/psyclone.cfg index 1f11818c56..d0a8df53bc 100644 --- a/config/psyclone.cfg +++ b/config/psyclone.cfg @@ -65,7 +65,7 @@ FORTRAN_STANDARD = f2008 [lfric] access_mapping = gh_read: read, gh_write: write, gh_readwrite: readwrite, gh_inc: inc, gh_readinc: readinc, gh_sum: sum, - gh_min: min, gh_max: max + gh_min: min, gh_max: max # Specify whether we compute annexed dofs when a kernel is written so # that it iterates over dofs. This is currently only the case for