From 3bbdef570e34cdbe9932c8be8298fc350e29887b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Sep 2024 17:11:51 +0100 Subject: [PATCH 001/100] #2716 add initial fix and test [skip ci] --- src/psyclone/psyGen.py | 6 ++++-- .../kernel_transformation_test.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 4359bcb88c..1ced2f37a6 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1714,14 +1714,16 @@ def _rename_psyir(self, suffix): :param str suffix: the string to insert into the quantity names. ''' - # We need to get the kernel schedule before modifying self.name + # We need to get the kernel schedule before modifying self.name. + # If the kernel corresponds to an interface, this will get the + # implementation that is actually being called. kern_schedule = self.get_kernel_schedule() container = kern_schedule.ancestor(Container) # Use the suffix to create a new kernel name. This will # conform to the PSyclone convention of ending in "_code" orig_mod_name = self.module_name[:] - orig_kern_name = self.name[:] + orig_kern_name = kern_schedule.name[:] # self.name[:] new_kern_name = self._new_name(orig_kern_name, suffix, "_code") new_mod_name = self._new_name(orig_mod_name, suffix, "_mod") diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index cf7658d585..d93ea5f7f6 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -266,6 +266,26 @@ def test_new_same_kern_single(kernel_outputdir, monkeypatch): assert out_files == [new_kernels[1].module_name+".f90"] +def test_transform_kern_with_interface(kernel_outputdir): + ''' + ''' + rtrans = ACCRoutineTrans() + _, invoke = get_invoke("26.8_mixed_precision_args.f90", + api="lfric", idx=0) + sched = invoke.schedule + kern = sched.coded_kernels()[0] + rtrans.apply(kern) + kern.rename_and_write() + out_files = os.listdir(str(kernel_outputdir)) + import pdb; pdb.set_trace() + filename = os.path.join(str(kernel_outputdir), out_files[0]) + assert os.path.isfile(filename) + with open(filename, + "r", encoding="utf-8") as ffile: + contents = ffile.read() + assert "happy days" in contents + + # The following tests test the MarkRoutineForGPUMixin validation, for this # it uses the ACCRoutineTrans as instance of this Mixin. From 459e26df3d745e990b0edb5e377aa075c39a1c81 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Sep 2024 17:13:23 +0100 Subject: [PATCH 002/100] #2716 fix linting --- src/psyclone/psyGen.py | 2 +- .../tests/psyir/transformations/kernel_transformation_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 1ced2f37a6..dae30fccac 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1723,7 +1723,7 @@ def _rename_psyir(self, suffix): # Use the suffix to create a new kernel name. This will # conform to the PSyclone convention of ending in "_code" orig_mod_name = self.module_name[:] - orig_kern_name = kern_schedule.name[:] # self.name[:] + orig_kern_name = kern_schedule.name[:] new_kern_name = self._new_name(orig_kern_name, suffix, "_code") new_mod_name = self._new_name(orig_mod_name, suffix, "_mod") diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index d93ea5f7f6..cd65fc3608 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -277,7 +277,6 @@ def test_transform_kern_with_interface(kernel_outputdir): rtrans.apply(kern) kern.rename_and_write() out_files = os.listdir(str(kernel_outputdir)) - import pdb; pdb.set_trace() filename = os.path.join(str(kernel_outputdir), out_files[0]) assert os.path.isfile(filename) with open(filename, From 37188bf10a07f20aa1dadef0eb2604b0754e2bd1 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 18 Sep 2024 16:08:03 +0100 Subject: [PATCH 003/100] #2716 rm check for polymorphic kernels, ensure renamed kern is public --- src/psyclone/domain/lfric/kernel_interface.py | 8 ++-- src/psyclone/domain/lfric/lfric_types.py | 6 +++ src/psyclone/psyGen.py | 4 +- .../kernel_transformation_test.py | 44 ++++++++++++++----- src/psyclone/transformations.py | 12 ----- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel_interface.py b/src/psyclone/domain/lfric/kernel_interface.py index b64fd0a91f..c0f841fae4 100644 --- a/src/psyclone/domain/lfric/kernel_interface.py +++ b/src/psyclone/domain/lfric/kernel_interface.py @@ -408,15 +408,15 @@ def scalar(self, scalar_arg, var_accesses=None): :param scalar_arg: the scalar to add. :type scalar_arg: :py:class:`psyclone.dynamo0p3.DynKernelArgument` - :param var_accesses: an unused optional argument that stores \ + :param var_accesses: an unused optional argument that stores information about variable accesses. - :type var_accesses: :\ - py:class:`psyclone.core.VariablesAccessInfo` + :type var_accesses: :py:class:`psyclone.core.VariablesAccessInfo` - :raises NotImplementedError: if the datatype of the scalar is \ + :raises NotImplementedError: if the datatype of the scalar is not supported. ''' + # TODO this needs to distinguish between precisions. mapping = { "integer": LFRicTypes("LFRicIntegerScalarDataSymbol"), "real": LFRicTypes("LFRicRealScalarDataSymbol"), diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index 305bda150f..6903cd6d7d 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -188,6 +188,12 @@ def _create_generic_scalars(): LFRicTypes("I_DEF")), GenericScalar("LFRicRealScalar", ScalarType.Intrinsic.REAL, LFRicTypes("R_DEF")), + GenericScalar("LFRicRdefScalar", ScalarType.Intrinsic.REAL, + LFRicTypes("R_DEF")), + GenericScalar("LFRicRsolverScalar", ScalarType.Intrinsic.REAL, + LFRicTypes("R_SOLVER")), + GenericScalar("LFRicRtranScalar", ScalarType.Intrinsic.REAL, + LFRicTypes("R_TRAN")), GenericScalar("LFRicLogicalScalar", ScalarType.Intrinsic.BOOLEAN, LFRicTypes("L_DEF"))] diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index dae30fccac..56208b6084 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1739,6 +1739,7 @@ def _rename_psyir(self, suffix): try: kern_symbol = kern_schedule.symbol_table.lookup(orig_kern_name) container.symbol_table.rename_symbol(kern_symbol, new_kern_name) + kern_symbol.visibility = Symbol.Visibility.PUBLIC except KeyError: # TODO #1013. Right now not all tests have PSyIR symbols because # some only expect f2pygen generation. @@ -1758,11 +1759,10 @@ def _rename_psyir(self, suffix): if isinstance(sym.datatype, UnsupportedFortranType): new_declaration = sym.datatype.declaration.replace( orig_kern_name, new_kern_name) - # pylint: disable=protected-access + # pylint: disable-next=protected-access sym._datatype = UnsupportedFortranType( new_declaration, partial_datatype=sym.datatype.partial_datatype) - # pylint: enable=protected-access @property def modified(self): diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index cd65fc3608..1966c6af16 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -268,21 +268,41 @@ def test_new_same_kern_single(kernel_outputdir, monkeypatch): def test_transform_kern_with_interface(kernel_outputdir): ''' + Test that we can transform a polymorphic kernel - i.e. one where + there is more than one subroutine implementation in order to support + different precisions. + ''' rtrans = ACCRoutineTrans() - _, invoke = get_invoke("26.8_mixed_precision_args.f90", - api="lfric", idx=0) + psy, invoke = get_invoke("26.8_mixed_precision_args.f90", + api="lfric", idx=0) sched = invoke.schedule - kern = sched.coded_kernels()[0] - rtrans.apply(kern) - kern.rename_and_write() - out_files = os.listdir(str(kernel_outputdir)) - filename = os.path.join(str(kernel_outputdir), out_files[0]) - assert os.path.isfile(filename) - with open(filename, - "r", encoding="utf-8") as ffile: - contents = ffile.read() - assert "happy days" in contents + kernels = sched.coded_kernels() + # Have to use 'force' because the test kernel contains a WRITE which + # becomes a CodeBlock. + #rtrans.apply(kernels[0], options={"force": True}) + #kernels[0].rename_and_write() + #out_files = os.listdir(str(kernel_outputdir)) + #filename = os.path.join(str(kernel_outputdir), out_files[0]) + #assert os.path.isfile(filename) + #with open(filename, + # "r", encoding="utf-8") as ffile: + # contents = ffile.read() + # Check that the routine name has been updated within the interface. + #assert "interface mixed_code" in contents + #assert ("module procedure :: mixed_code_32, mixed_code_64_0_code" + # in contents) + # Check that the subroutine itself has been renamed. + #assert "subroutine mixed_code_64_0_code" in contents + #assert ('''real*8, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op +# +# !$acc routine seq''' in contents) + #assert LFRicBuild(kernel_outputdir).code_compiles(psy) + #import pdb; pdb.set_trace() + rtrans.apply(kernels[1], options={"force": True}) + #kernels[1].rename_and_write() + psy.gen + assert LFRicBuild(kernel_outputdir).code_compiles(psy) # The following tests test the MarkRoutineForGPUMixin validation, for this diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index ca34b76607..3b283a8049 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -378,8 +378,6 @@ def validate_it_can_run_on_gpu(self, node, options): :raises TransformationError: if the target is a built-in kernel. :raises TransformationError: if it is a kernel but without an associated PSyIR. - :raises TransformationError: if it is a Kernel that has multiple - implementations (mixed precision). :raises TransformationError: if any of the symbols in the kernel are accessed via a module use statement (and are not compile-time constants). @@ -413,16 +411,6 @@ def validate_it_can_run_on_gpu(self, node, options): f"Failed to create PSyIR for kernel '{node.name}'. " f"Cannot transform such a kernel.") from error - # Check that it's not a mixed-precision kernel (which will have - # more than one Routine implementing it). We can't transform - # these at the moment because we can't correctly manipulate their - # metadata - TODO #1946. - routines = kernel_schedule.root.walk(Routine) - if len(routines) > 1: - raise TransformationError( - f"Cannot apply {self.name} to kernel '{node.name}' as " - f"it has multiple implementations - TODO #1946") - k_or_r = "Kernel" else: # Supplied node is a PSyIR Routine which *is* a Schedule. From 4aa1c0c3fd03243fd2e8686821f2fc2c2f60a949 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 18 Sep 2024 16:10:34 +0100 Subject: [PATCH 004/100] #2716 fix linting --- .../kernel_transformation_test.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 1966c6af16..0aeadf25e0 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -280,27 +280,26 @@ def test_transform_kern_with_interface(kernel_outputdir): kernels = sched.coded_kernels() # Have to use 'force' because the test kernel contains a WRITE which # becomes a CodeBlock. - #rtrans.apply(kernels[0], options={"force": True}) - #kernels[0].rename_and_write() - #out_files = os.listdir(str(kernel_outputdir)) - #filename = os.path.join(str(kernel_outputdir), out_files[0]) - #assert os.path.isfile(filename) - #with open(filename, - # "r", encoding="utf-8") as ffile: - # contents = ffile.read() + rtrans.apply(kernels[0], options={"force": True}) + kernels[0].rename_and_write() + out_files = os.listdir(str(kernel_outputdir)) + filename = os.path.join(str(kernel_outputdir), out_files[0]) + assert os.path.isfile(filename) + with open(filename, + "r", encoding="utf-8") as ffile: + contents = ffile.read() # Check that the routine name has been updated within the interface. - #assert "interface mixed_code" in contents - #assert ("module procedure :: mixed_code_32, mixed_code_64_0_code" - # in contents) + assert "interface mixed_code" in contents + assert ("module procedure :: mixed_code_32, mixed_code_64_0_code" + in contents) # Check that the subroutine itself has been renamed. - #assert "subroutine mixed_code_64_0_code" in contents - #assert ('''real*8, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op -# -# !$acc routine seq''' in contents) - #assert LFRicBuild(kernel_outputdir).code_compiles(psy) - #import pdb; pdb.set_trace() + assert "subroutine mixed_code_64_0_code" in contents + assert ('''real*8, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op + + !$acc routine seq''' in contents) + assert LFRicBuild(kernel_outputdir).code_compiles(psy) + kernels = sched.coded_kernels() rtrans.apply(kernels[1], options={"force": True}) - #kernels[1].rename_and_write() psy.gen assert LFRicBuild(kernel_outputdir).code_compiles(psy) From 4383c7c53958fb40a85a852a91875d17d3328885 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 27 Sep 2024 16:37:33 +0100 Subject: [PATCH 005/100] #2716 WIP exploring options --- .../kernel_module_inline_trans.py | 147 ++++++++++-------- src/psyclone/domain/lfric/lfric_types.py | 4 + .../kernel_module_inline_trans_test.py | 11 +- 3 files changed, 89 insertions(+), 73 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index bad4b3a857..b00a8413a4 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -145,7 +145,7 @@ def validate(self, node, options=None): # Check that the PSyIR of the routine/kernel can be retrieved. try: - _, kernel_schedule = ( + _, kernels, _ = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) except Exception as error: raise TransformationError( @@ -153,6 +153,9 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error + # TODO ARPDBG - need to examine every kernel implementation, not just + # the first one. + kernel_schedule = kernels[0] # We do not support kernels that use symbols representing data # declared in their own parent module (we would need to new imports # from this module for those, and we don't do this yet). @@ -218,7 +221,7 @@ def validate(self, node, options=None): f"subroutines is not supported yet.") @staticmethod - def _prepare_code_to_inline(code_to_inline): + def _prepare_code_to_inline(routines_to_inline, symbol): '''Prepare the PSyIR tree to inline by bringing in to the subroutine all referenced symbols so that the implementation is self contained. @@ -234,65 +237,66 @@ def _prepare_code_to_inline(code_to_inline): ''' # pylint: disable=too-many-branches - source_container = code_to_inline.ancestor(Container) - - # First make a set with all symbols used inside the subroutine - all_symbols = set() - for scope in code_to_inline.walk(ScopingNode): - for symbol in scope.symbol_table.symbols: - all_symbols.add(symbol) - for reference in code_to_inline.walk(Reference): - all_symbols.add(reference.symbol) - for literal in code_to_inline.walk(Literal): - # Literals may reference symbols in their precision - if isinstance(literal.datatype.precision, Symbol): - all_symbols.add(literal.datatype.precision) - for caller in code_to_inline.walk(Call): - all_symbols.add(caller.routine.symbol) - for cblock in code_to_inline.walk(CodeBlock): - for name in cblock.get_symbol_names(): - all_symbols.add(cblock.scope.symbol_table.lookup(name)) - - # Then decide which symbols need to be brought inside the subroutine - symbols_to_bring_in = set() - for symbol in all_symbols: - if symbol.is_unresolved or symbol.is_import: - # This symbol is already in the symbol table, but adding it - # to the 'symbols_to_bring_in' will make the next step bring - # into the subroutine all modules that it could come from. - symbols_to_bring_in.add(symbol) - if isinstance(symbol, DataSymbol): - # DataTypes can reference other symbols - if isinstance(symbol.datatype, DataTypeSymbol): - symbols_to_bring_in.add(symbol.datatype) - elif hasattr(symbol.datatype, 'precision'): - if isinstance(symbol.datatype.precision, Symbol): - symbols_to_bring_in.add(symbol.datatype.precision) - - # Bring the selected symbols inside the subroutine - for symbol in symbols_to_bring_in: - if symbol.name not in code_to_inline.symbol_table: - code_to_inline.symbol_table.add(symbol) - # And when necessary the modules where they come from - if symbol.is_unresolved: - # We don't know where this comes from, we need to bring - # in all top-level imports with wildcard imports - for mod in source_container.symbol_table.containersymbols: - if mod.wildcard_import: - if mod.name not in code_to_inline.symbol_table: - code_to_inline.symbol_table.add(mod) - else: - code_to_inline.symbol_table.lookup(mod.name).\ - wildcard_import = True - elif symbol.is_import: - module_symbol = symbol.interface.container_symbol - if module_symbol.name not in code_to_inline.symbol_table: - code_to_inline.symbol_table.add(module_symbol) - else: - # If it already exists, we know its a container (from the - # validation) so we just need to point to it - symbol.interface.container_symbol = \ - code_to_inline.symbol_table.lookup(module_symbol.name) + source_container = routines_to_inline[0].ancestor(Container) + + for code_to_inline in routines_to_inline: + # First make a set with all symbols used inside the subroutine + all_symbols = set() + for scope in code_to_inline.walk(ScopingNode): + for symbol in scope.symbol_table.symbols: + all_symbols.add(symbol) + for reference in code_to_inline.walk(Reference): + all_symbols.add(reference.symbol) + for literal in code_to_inline.walk(Literal): + # Literals may reference symbols in their precision + if isinstance(literal.datatype.precision, Symbol): + all_symbols.add(literal.datatype.precision) + for caller in code_to_inline.walk(Call): + all_symbols.add(caller.routine.symbol) + for cblock in code_to_inline.walk(CodeBlock): + for name in cblock.get_symbol_names(): + all_symbols.add(cblock.scope.symbol_table.lookup(name)) + + # Then decide which symbols need to be brought inside the subroutine + symbols_to_bring_in = set() + for symbol in all_symbols: + if symbol.is_unresolved or symbol.is_import: + # This symbol is already in the symbol table, but adding it + # to the 'symbols_to_bring_in' will make the next step bring + # into the subroutine all modules that it could come from. + symbols_to_bring_in.add(symbol) + if isinstance(symbol, DataSymbol): + # DataTypes can reference other symbols + if isinstance(symbol.datatype, DataTypeSymbol): + symbols_to_bring_in.add(symbol.datatype) + elif hasattr(symbol.datatype, 'precision'): + if isinstance(symbol.datatype.precision, Symbol): + symbols_to_bring_in.add(symbol.datatype.precision) + + # Bring the selected symbols inside the subroutine + for symbol in symbols_to_bring_in: + if symbol.name not in code_to_inline.symbol_table: + code_to_inline.symbol_table.add(symbol) + # And when necessary the modules where they come from + if symbol.is_unresolved: + # We don't know where this comes from, we need to bring + # in all top-level imports with wildcard imports + for mod in source_container.symbol_table.containersymbols: + if mod.wildcard_import: + if mod.name not in code_to_inline.symbol_table: + code_to_inline.symbol_table.add(mod) + else: + code_to_inline.symbol_table.lookup(mod.name).\ + wildcard_import = True + elif symbol.is_import: + module_symbol = symbol.interface.container_symbol + if module_symbol.name not in code_to_inline.symbol_table: + code_to_inline.symbol_table.add(module_symbol) + else: + # If it already exists, we know its a container (from the + # validation) so we just need to point to it + symbol.interface.container_symbol = \ + code_to_inline.symbol_table.lookup(module_symbol.name) @staticmethod def _get_psyir_to_inline(node): @@ -306,7 +310,8 @@ def _get_psyir_to_inline(node): :returns: the name of the routine as seen by the caller and the PSyIR of the routine implementation. - :rtype: Tuple(str, :py:class:`psyclone.psyir.nodes.Call`) + :rtype: Tuple(str, list[:py:class:`psyclone.psyir.nodes.Routine`], + :py:class:`psyclone.psyir.symbols.Symbol`) :raises TransformationError: if we have a call to a language-level Routine that maps to an Interface block as this is not yet @@ -321,6 +326,7 @@ def _get_psyir_to_inline(node): # interface matching the arguments in the call. routines = [node.get_kernel_schedule()] caller_name = node.name.lower() + interface_sym = None else: # We have a generic routine call. routines = node.get_callees() @@ -334,7 +340,7 @@ def _get_psyir_to_inline(node): f"The target of the call to '{caller_name}' cannot be " f"inserted because multiple implementations were found: " f"{[rout.name for rout in routines]}. TODO #924") - return (caller_name, routines[0]) + return (caller_name, routines, interface_sym) def apply(self, node, options=None): ''' Bring the kernel subroutine into this Container. @@ -364,16 +370,19 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - caller_name, code_to_inline = ( + caller_name, code_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) - callee_name = code_to_inline.name + if interface_sym: + callee_name = interface_sym.name + else: + callee_name = code_to_inline[0].name try: existing_symbol = node.scope.symbol_table.lookup(callee_name) except KeyError: existing_symbol = None - self._prepare_code_to_inline(code_to_inline) + self._prepare_code_to_inline(code_to_inline, interface_sym) container = node.ancestor(Container) if not existing_symbol: @@ -384,7 +393,8 @@ def apply(self, node, options=None): visibility=Symbol.Visibility.PRIVATE) container.symbol_table.add(routine_symbol) # 2) Insert the relevant code into the tree. - container.addchild(code_to_inline.detach()) + for routine in code_to_inline: + container.addchild(routine.detach()) else: if existing_symbol.is_import: # The RoutineSymbol is in the table but that is because it is @@ -400,7 +410,8 @@ def apply(self, node, options=None): existing_symbol.visibility = Symbol.Visibility.PRIVATE if remove_csym: ctable.remove(csym) - container.addchild(code_to_inline.detach()) + for routine in code_to_inline: + container.addchild(routine.detach()) else: # The routine symbol already exists, and we know from the # validation that it's a Routine. Now check if they are diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index 6903cd6d7d..afa066b1dc 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -183,6 +183,10 @@ def _create_generic_scalars(): ''' GenericScalar = namedtuple('GenericScalar', ["name", "intrinsic", "precision"]) + api_config = Config.get().api_conf("lfric") + + lfric_kinds = list(api_config.precision_map.keys()) + generic_scalar_datatypes = [ GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER, LFRicTypes("I_DEF")), diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 97eadbba9d..4124a945e4 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -743,11 +743,12 @@ def test_module_inline_with_interfaces(tmpdir): kern_call = invoke.schedule.walk(CodedKern)[0] inline_trans = KernelModuleInlineTrans() inline_trans.apply(kern_call) - gen = str(psy.gen) - # Both the caller and the callee are in the file and use the specialized - # implementation name. - assert "CALL mixed_code_64(" in gen - assert "SUBROUTINE mixed_code_64(" in gen + gen = str(psy.gen).lower() + # Both the caller and the callee are in the file and use the interface + # name. + assert "call mixed_code(" in gen + assert "subroutine mixed_code_64(" in gen + assert "subroutine mixed_code_32(" in gen # And it is valid code assert LFRicBuild(tmpdir).code_compiles(psy) From 62b5da5a090216d1af868c90a24922e5a33d54a4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 2 Oct 2024 15:27:08 +0100 Subject: [PATCH 006/100] #2716 WIP plumbing-in inlining of multiple kernel routines --- .../kernel_module_inline_trans.py | 126 +++++++++--------- src/psyclone/domain/lfric/lfric_kern.py | 86 +++++++----- src/psyclone/gocean1p0.py | 8 +- src/psyclone/psyGen.py | 13 +- .../kernel_module_inline_trans_test.py | 27 ++-- 5 files changed, 143 insertions(+), 117 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index b00a8413a4..e3ace7b938 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -153,6 +153,8 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error + if not isinstance(kernels, list): + import pdb; pdb.set_trace() # TODO ARPDBG - need to examine every kernel implementation, not just # the first one. kernel_schedule = kernels[0] @@ -324,12 +326,13 @@ def _get_psyir_to_inline(node): # Where mixed-precision kernels are supported (e.g. in LFRic) the # call to get_kernel_schedule() will return the one which has an # interface matching the arguments in the call. - routines = [node.get_kernel_schedule()] + interface_sym, routines = node.get_kernel_schedule() caller_name = node.name.lower() - interface_sym = None else: # We have a generic routine call. routines = node.get_callees() + if not isinstance(routines, list): + routines = [routines] caller_name = node.routine.name.lower() # TODO #924 - at this point we may have found (an interface to) # multiple implementations. We can try to work out which one this @@ -340,6 +343,7 @@ def _get_psyir_to_inline(node): f"The target of the call to '{caller_name}' cannot be " f"inserted because multiple implementations were found: " f"{[rout.name for rout in routines]}. TODO #924") + interface_sym = None # TODO return (caller_name, routines, interface_sym) def apply(self, node, options=None): @@ -370,76 +374,76 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - caller_name, code_to_inline, interface_sym = ( + caller_name, codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) if interface_sym: callee_name = interface_sym.name else: - callee_name = code_to_inline[0].name + callee_name = codes_to_inline[0].name try: existing_symbol = node.scope.symbol_table.lookup(callee_name) except KeyError: existing_symbol = None - self._prepare_code_to_inline(code_to_inline, interface_sym) - - container = node.ancestor(Container) - if not existing_symbol: - # If it doesn't exist already, module-inline the subroutine by: - # 1) Registering the subroutine symbol in the Container - routine_symbol = RoutineSymbol( - callee_name, interface=DefaultModuleInterface(), - visibility=Symbol.Visibility.PRIVATE) - container.symbol_table.add(routine_symbol) - # 2) Insert the relevant code into the tree. - for routine in code_to_inline: - container.addchild(routine.detach()) - else: - if existing_symbol.is_import: - # The RoutineSymbol is in the table but that is because it is - # imported. We must therefore update its interface and - # potentially remove the ContainerSymbol (from which it is - # imported) altogether. - csym = existing_symbol.interface.container_symbol - # The import of the routine symbol may be in an outer scope. - ctable = csym.find_symbol_table(node) - remove_csym = (ctable.symbols_imported_from(csym) == - [existing_symbol]) - existing_symbol.interface = DefaultModuleInterface() - existing_symbol.visibility = Symbol.Visibility.PRIVATE - if remove_csym: - ctable.remove(csym) - for routine in code_to_inline: - container.addchild(routine.detach()) - else: - # The routine symbol already exists, and we know from the - # validation that it's a Routine. Now check if they are - # exactly the same. - for routine in container.walk(Routine, stop_type=Routine): - if routine.name == caller_name: - # This TransformationError happens here and not in the - # validation because it needs the symbols_to_bring_in - # applied to effectively compare both versions. - # This will be fixed when module-inlining versioning is - # implemented. - # (It is OK to fail here because we have not yet made - # any modifications to the tree - code_to_inline is a - # detached copy.) - if routine != code_to_inline: - raise TransformationError( - f"Cannot inline subroutine '{caller_name}' " - f"because another, different, subroutine with " - f"the same name already exists and versioning " - f"of module-inlined subroutines is not " - f"implemented yet.") - # Finally, ensure that the RoutineSymbol for the inlined routine is - # in the correct symbol table. - routine_symbol = existing_symbol - table = routine_symbol.find_symbol_table(node) - if table.node is not container: + self._prepare_code_to_inline(codes_to_inline, interface_sym) + + for code_to_inline in codes_to_inline: + container = node.ancestor(Container) + if not existing_symbol: + # If it doesn't exist already, module-inline the subroutine by: + # 1) Registering the subroutine symbol in the Container + routine_symbol = RoutineSymbol( + callee_name, interface=DefaultModuleInterface(), + visibility=Symbol.Visibility.PRIVATE) container.symbol_table.add(routine_symbol) - table.remove(routine_symbol) + # 2) Insert the relevant code into the tree. + container.addchild(code_to_inline.detach()) + else: + if existing_symbol.is_import: + # The RoutineSymbol is in the table but that is because it is + # imported. We must therefore update its interface and + # potentially remove the ContainerSymbol (from which it is + # imported) altogether. + csym = existing_symbol.interface.container_symbol + # The import of the routine symbol may be in an outer scope. + ctable = csym.find_symbol_table(node) + remove_csym = (ctable.symbols_imported_from(csym) == + [existing_symbol]) + existing_symbol.interface = DefaultModuleInterface() + existing_symbol.visibility = Symbol.Visibility.PRIVATE + if remove_csym: + ctable.remove(csym) + for routine in code_to_inline: + container.addchild(routine.detach()) + else: + # The routine symbol already exists, and we know from the + # validation that it's a Routine. Now check if they are + # exactly the same. + for routine in container.walk(Routine, stop_type=Routine): + if routine.name == caller_name: + # This TransformationError happens here and not in the + # validation because it needs the symbols_to_bring_in + # applied to effectively compare both versions. + # This will be fixed when module-inlining versioning is + # implemented. + # (It is OK to fail here because we have not yet made + # any modifications to the tree - code_to_inline is a + # detached copy.) + if routine != code_to_inline: + raise TransformationError( + f"Cannot inline subroutine '{caller_name}' " + f"because another, different, subroutine with " + f"the same name already exists and versioning " + f"of module-inlined subroutines is not " + f"implemented yet.") + # Finally, ensure that the RoutineSymbol for the inlined routine is + # in the correct symbol table. + routine_symbol = existing_symbol + table = routine_symbol.find_symbol_table(node) + if table.node is not container: + container.symbol_table.add(routine_symbol) + table.remove(routine_symbol) # We only modify the kernel call name after the equality check to # ensure the apply will succeed and we don't leave with an inconsistent diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 3731ccaf90..180aee6083 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -648,11 +648,11 @@ class creates the PSyIR schedule on first invocation which is :returns: Schedule representing the kernel code. :rtype: :py:class:`psyclone.psyGen.KernelSchedule` - :raises GenerationError: if no subroutine matching this kernel can \ + :raises GenerationError: if no subroutine matching this kernel can be found in the parse tree of the associated source code. ''' if self._kern_schedule: - return self._kern_schedule + return self._interface_symbol, self._kern_schedule # Get the PSyIR Kernel Schedule(s) routines = Fparser2Reader().get_routine_schedules(self.name, self.ast) @@ -676,42 +676,54 @@ class creates the PSyIR schedule on first invocation which is # problem in LFRic that this fixes. routine.debug_string() - if len(routines) == 1: - sched = routines[0] - # TODO #928: We don't validate the arguments yet because the - # validation has many false negatives. - # self.validate_kernel_code_args(sched.symbol_table) +# if len(routines) == 1: +# sched = routines[0] +# # TODO #928: We don't validate the arguments yet because the +# # validation has many false negatives. +# # self.validate_kernel_code_args(sched.symbol_table) +# else: +# # The kernel name corresponds to an interface block. Find which +# # of the routines matches the precision of the arguments. +# for routine in routines: +# try: +# # The validity check for the kernel arguments will raise +# # an exception if the precisions don't match. +# self.validate_kernel_code_args(routine.symbol_table) +# sched = routine +# break +# except GenerationError: +# pass +# else: +# raise GenerationError( +# f"Failed to find a kernel implementation with an interface" +# f" that matches the invoke of '{self.name}'. (Tried " +# f"routines {[item.name for item in routines]}.)") +# + + #import pdb; pdb.set_trace() + if len(routines) > 1: + table = self._kern_schedule[0].scope.symbol_table + sym = table.lookup(self.name) else: - # The kernel name corresponds to an interface block. Find which - # of the routines matches the precision of the arguments. - for routine in routines: - try: - # The validity check for the kernel arguments will raise - # an exception if the precisions don't match. - self.validate_kernel_code_args(routine.symbol_table) - sched = routine - break - except GenerationError: - pass - else: - raise GenerationError( - f"Failed to find a kernel implementation with an interface" - f" that matches the invoke of '{self.name}'. (Tried " - f"routines {[item.name for item in routines]}.)") - - # TODO #935 - replace the PSyIR argument data symbols with LFRic data - # symbols. For the moment we just return the unmodified PSyIR schedule - # but this should use RaisePSyIR2LFRicKernTrans once KernelInterface - # is fully functional (#928). - ksched = KernelSchedule(sched.name, - symbol_table=sched.symbol_table.detach()) - for child in sched.pop_all_children(): - ksched.addchild(child) - sched.replace_with(ksched) - - self._kern_schedule = ksched - - return self._kern_schedule + sym = None + + new_schedules = [] + for sched in routines: + + # TODO #935 - replace the PSyIR argument data symbols with LFRic data + # symbols. For the moment we just return the unmodified PSyIR schedule + # but this should use RaisePSyIR2LFRicKernTrans once KernelInterface + # is fully functional (#928). + ksched = KernelSchedule(sched.name, + symbol_table=sched.symbol_table.detach()) + for child in sched.pop_all_children(): + ksched.addchild(child) + sched.replace_with(ksched) + new_schedules.append(ksched) + + self._kern_schedule = new_schedules + + return sym, self._kern_schedule def validate_kernel_code_args(self, table): '''Check that the arguments in the kernel code match the expected diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index 8b24a121d9..0b587a711c 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1231,11 +1231,11 @@ def get_kernel_schedule(self): :returns: a schedule representing the GOcean kernel code. :rtype: :py:class:`psyclone.gocean1p0.GOKernelSchedule` - :raises GenerationError: if there is a problem raising the language- \ + :raises GenerationError: if there is a problem raising the language- level PSyIR of this kernel to GOcean PSyIR. ''' if self._kern_schedule: - return self._kern_schedule + return None, self._kern_schedule # Construct the PSyIR of the Fortran parse tree. astp = Fparser2Reader() @@ -1256,9 +1256,9 @@ def get_kernel_schedule(self): # We know the above loop will find the named routine because the # previous raising transformation would have failed otherwise. # pylint: disable=undefined-loop-variable - self._kern_schedule = routine + self._kern_schedule = [routine] - return self._kern_schedule + return None, self._kern_schedule class GOKernelArguments(Arguments): diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 56208b6084..71a174bd1a 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1355,7 +1355,8 @@ def __init__(self, KernelArguments, call, parent=None, check=True): self._module_code = call.ktype._ast self._kernel_code = call.ktype.procedure self._fp2_ast = None # The fparser2 AST for the kernel - self._kern_schedule = None # PSyIR schedule for the kernel + self._kern_schedule = None # PSyIR schedules for the kernel + self._interface_symbol = None # Whether or not this kernel has been transformed self._modified = False # Whether or not to in-line this kernel into the module containing @@ -1370,15 +1371,17 @@ def get_kernel_schedule(self): is just generated on first invocation, this allows us to retain transformations that may subsequently be applied to the Schedule. - :returns: Schedule representing the kernel code. - :rtype: :py:class:`psyclone.psyir.nodes.KernelSchedule` + :returns: Interface symbol (if any) and Schedule(s) representing the + kernel code. + :rtype: tuple[:py:class:`psyclone.psyir.symbols.Symbol`, + list[:py:class:`psyclone.psyir.nodes.KernelSchedule`]] ''' from psyclone.psyir.frontend.fparser2 import Fparser2Reader if self._kern_schedule is None: astp = Fparser2Reader() - self._kern_schedule = astp.generate_schedule(self.name, self.ast) + self._kern_schedule = [astp.generate_schedule(self.name, self.ast)] # TODO: Validate kernel with metadata (issue #288). - return self._kern_schedule + return None, self._kern_schedule @property def opencl_options(self): diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 4124a945e4..412e2f9ab0 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -102,7 +102,8 @@ def test_validate_with_imported_subroutine_call(): schedule = invoke.schedule kern_call = schedule.walk(CodedKern)[0] # Create a call to made up subroutine and module symbols - kern_schedule = kern_call.get_kernel_schedule() + _, kern_schedules = kern_call.get_kernel_schedule() + kern_schedule = kern_schedules[0] mymod = kern_schedule.symbol_table.new_symbol( "mymod", symbol_type=ContainerSymbol) @@ -161,8 +162,10 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - kernels[0].get_kernel_schedule().pop_all_children() - kernels[0].get_kernel_schedule().addchild(block) + _, kschedules = kernels[0].get_kernel_schedule() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild(block) with pytest.raises(TransformationError) as err: inline_trans.validate(kernels[0]) @@ -177,8 +180,10 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - kernels[0].get_kernel_schedule().pop_all_children() - kernels[0].get_kernel_schedule().addchild(block) + _, kschedules = kernels[0].get_kernel_schedule() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild(block) with pytest.raises(TransformationError) as err: inline_trans.validate(kernels[0]) assert ("Kernel 'kernel_with_global_code' contains accesses to 'unknown' " @@ -187,8 +192,10 @@ def test_validate_no_inline_global_var(parser): # But make sure that an IntrinsicCall routine name is not considered # a global symbol, as they are implicitly declared everywhere - kernels[0].get_kernel_schedule().pop_all_children() - kernels[0].get_kernel_schedule().addchild( + _, kschedules = kernels[0].get_kernel_schedule() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild( IntrinsicCall.create(IntrinsicCall.Intrinsic.DATE_AND_TIME, [])) inline_trans.validate(kernels[0]) @@ -253,7 +260,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", routine) + monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) # and try to apply the transformation inline_trans = KernelModuleInlineTrans() @@ -276,7 +283,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", routine) + monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) # and try to apply the transformation with pytest.raises(TransformationError) as err: @@ -298,7 +305,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", routine) + monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) container = kern_call.ancestor(Container) assert "compute_cv_code" not in container.symbol_table From 5a10b8824f1c25ac9309f5a12808e93a4194c168 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 2 Oct 2024 17:12:28 +0100 Subject: [PATCH 007/100] #2716 more fixes [skip ci] --- .../kernel_module_inline_trans.py | 11 ++++--- src/psyclone/domain/lfric/lfric_kern.py | 2 +- .../kernel_module_inline_trans_test.py | 30 ++++++++++++------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index e3ace7b938..7b397a85c8 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -393,10 +393,10 @@ def apply(self, node, options=None): if not existing_symbol: # If it doesn't exist already, module-inline the subroutine by: # 1) Registering the subroutine symbol in the Container - routine_symbol = RoutineSymbol( - callee_name, interface=DefaultModuleInterface(), - visibility=Symbol.Visibility.PRIVATE) - container.symbol_table.add(routine_symbol) + #routine_symbol = RoutineSymbol( + # callee_name, interface=DefaultModuleInterface(), + # visibility=Symbol.Visibility.PRIVATE) + #container.symbol_table.add(routine_symbol) # 2) Insert the relevant code into the tree. container.addchild(code_to_inline.detach()) else: @@ -414,8 +414,7 @@ def apply(self, node, options=None): existing_symbol.visibility = Symbol.Visibility.PRIVATE if remove_csym: ctable.remove(csym) - for routine in code_to_inline: - container.addchild(routine.detach()) + container.addchild(routine.detach()) else: # The routine symbol already exists, and we know from the # validation that it's a Routine. Now check if they are diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 180aee6083..b5ac10db5b 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -702,7 +702,7 @@ class creates the PSyIR schedule on first invocation which is #import pdb; pdb.set_trace() if len(routines) > 1: - table = self._kern_schedule[0].scope.symbol_table + table = routines[0].scope.symbol_table sym = table.lookup(self.name) else: sym = None diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 412e2f9ab0..9199b75630 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -526,7 +526,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1" in result assert "use external_mod2" in result @@ -548,7 +549,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : a" in result assert "use external_mod2, only : b=>var1, c=>var2" in result @@ -572,7 +574,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : a, d" in result assert "use external_mod2, only : b=>var1, c=>var2, var1" in result @@ -596,7 +599,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : r_def" in result assert "use external_mod2, only : my_user_type" in result @@ -617,7 +621,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : r_def" in result assert "use not_needed" not in result @@ -637,7 +642,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : my_sub" in result @@ -654,7 +660,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : a, b" in result @@ -674,7 +681,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod1, only : c" in result @@ -692,7 +700,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod\n" in result assert "use external_mod, only : r_def" not in result @@ -708,7 +717,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline(routine) + sym = routine.scope.symbol_table.lookup(routine.name) + inline_trans._prepare_code_to_inline([routine], sym) result = fortran_writer(routine) assert "use external_mod, only : a" in result From cc657a9e9b12a82c73e2c2eae2de18777f875141 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 09:58:24 +0100 Subject: [PATCH 008/100] #2716 get KernelModuleInlineTrans tests working [skip ci] --- .../kernel_module_inline_trans.py | 39 +++++----- src/psyclone/domain/lfric/lfric_kern.py | 11 ++- src/psyclone/psyGen.py | 2 +- .../kernel_module_inline_trans_test.py | 76 +++---------------- 4 files changed, 39 insertions(+), 89 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 9e6c55802d..22265d22db 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -223,7 +223,7 @@ def validate(self, node, options=None): f"subroutines is not supported yet.") @staticmethod - def _prepare_code_to_inline(routines_to_inline, symbol): + def _prepare_code_to_inline(routines_to_inline): '''Prepare the PSyIR tree to inline by bringing in to the subroutine all referenced symbols so that the implementation is self contained. @@ -295,7 +295,7 @@ def _prepare_code_to_inline(routines_to_inline, symbol): if module_symbol.name not in code_to_inline.symbol_table: code_to_inline.symbol_table.add(module_symbol) else: - # If it already exists, we know its a container (from the + # If it already exists, we know it's a container (from the # validation) so we just need to point to it symbol.interface.container_symbol = \ code_to_inline.symbol_table.lookup(module_symbol.name) @@ -334,15 +334,6 @@ def _get_psyir_to_inline(node): if not isinstance(routines, list): routines = [routines] caller_name = node.routine.name.lower() - # TODO #924 - at this point we may have found (an interface to) - # multiple implementations. We can try to work out which one this - # call will map to. Failing that, we'll have to insert all of them - # plus the interface definition. - if len(routines) > 1: - raise TransformationError( - f"The target of the call to '{caller_name}' cannot be " - f"inserted because multiple implementations were found: " - f"{[rout.name for rout in routines]}. TODO #924") interface_sym = None # TODO return (caller_name, routines, interface_sym) @@ -376,20 +367,26 @@ def apply(self, node, options=None): # implementation. caller_name, codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) - if interface_sym: - callee_name = interface_sym.name + + # This will be the name of the interface if the call is to a + # polymorphic routine. + if isinstance(node, Call): + callee_name = node.routine.name else: - callee_name = codes_to_inline[0].name + callee_name = node.name try: existing_symbol = node.scope.symbol_table.lookup(callee_name) except KeyError: existing_symbol = None - self._prepare_code_to_inline(codes_to_inline, interface_sym) + self._prepare_code_to_inline(codes_to_inline) container = node.ancestor(Container) + if interface_sym: + container.symbol_table.add(interface_sym) + for code_to_inline in codes_to_inline: if not existing_symbol: # If it doesn't exist already, module-inline the subroutine by @@ -409,14 +406,20 @@ def apply(self, node, options=None): ctable = csym.find_symbol_table(node) remove_csym = (ctable.symbols_imported_from(csym) == [existing_symbol]) - existing_symbol.interface = DefaultModuleInterface() - existing_symbol.visibility = Symbol.Visibility.PRIVATE + #existing_symbol.interface = DefaultModuleInterface() + #existing_symbol.visibility = Symbol.Visibility.PRIVATE + if code_to_inline.name == existing_symbol.name: + # Have to remove Symbol as adding the Routine into + # the Container will insert it again. + ctable._symbols.pop(existing_symbol.name) if remove_csym: ctable.remove(csym) code_to_inline = code_to_inline.detach() # Set the routine's symbol to the existing_symbol - code_to_inline.symbol = existing_symbol + #code_to_inline.symbol = existing_symbol container.addchild(code_to_inline) + sym = ctable.lookup(code_to_inline.name) + sym.visibility = Symbol.Visibility.PRIVATE else: # The routine symbol already exists, and we know from the # validation that it's a Routine. Now check if they are diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 9b3c161447..5e12b54be0 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -645,8 +645,11 @@ class creates the PSyIR schedule on first invocation which is Once issue #935 is implemented, this routine will return the PSyIR Schedule using LFRic-specific PSyIR where possible. - :returns: Schedule representing the kernel code. - :rtype: :py:class:`psyclone.psyGen.KernelSchedule` + :returns: the Symbol defining the interface to this kernel (if it is + polymorphic and a list of the Schedule(s) representing the kernel + code. + :rtype: tuple[Optional[:py:class:`psyclone.psyir.symbols.Symbol`], + list[:py:class:`psyclone.psyGen.KernelSchedule`]] :raises GenerationError: if no subroutine matching this kernel can be found in the parse tree of the associated source code. @@ -700,7 +703,6 @@ class creates the PSyIR schedule on first invocation which is # f"routines {[item.name for item in routines]}.)") # - #import pdb; pdb.set_trace() if len(routines) > 1: table = routines[0].scope.symbol_table sym = table.lookup(self.name) @@ -720,9 +722,10 @@ class creates the PSyIR schedule on first invocation which is sched.replace_with(ksched) new_schedules.append(ksched) + self._interface_symbol = sym self._kern_schedule = new_schedules - return sym, self._kern_schedule + return self._interface_symbol, self._kern_schedule def validate_kernel_code_args(self, table): '''Check that the arguments in the kernel code match the expected diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 98c6915aa7..b09bbb818e 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1528,7 +1528,7 @@ def lower_to_language_level(self): shadowing=True, interface=ImportInterface(csymbol)) else: - # If its inlined, the symbol must exist + # If it's inlined, the symbol must exist try: rsymbol = self.scope.symbol_table.lookup(self._name) except KeyError as err: diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 32b120938f..2919986e8f 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -524,8 +524,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1" in result assert "use external_mod2" in result @@ -547,8 +546,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : a" in result assert "use external_mod2, only : b=>var1, c=>var2" in result @@ -572,8 +570,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : a, d" in result assert "use external_mod2, only : b=>var1, c=>var2, var1" in result @@ -597,8 +594,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : r_def" in result assert "use external_mod2, only : my_user_type" in result @@ -619,8 +615,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : r_def" in result assert "use not_needed" not in result @@ -640,8 +635,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : my_sub" in result @@ -658,8 +652,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : a, b" in result @@ -679,8 +672,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod1, only : c" in result @@ -698,8 +690,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod\n" in result assert "use external_mod, only : r_def" not in result @@ -715,8 +706,7 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - sym = routine.scope.symbol_table.lookup(routine.name) - inline_trans._prepare_code_to_inline([routine], sym) + inline_trans._prepare_code_to_inline([routine]) result = fortran_writer(routine) assert "use external_mod, only : a" in result @@ -769,28 +759,6 @@ def test_module_inline_with_interfaces(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_get_psyir_to_inline(monkeypatch): - ''' - Test that _get_psyir_to_inline() raises the expected error if more than - one potential routine implementation is found. - - ''' - sym = RoutineSymbol("my_sym") - rout = Routine.create("my_sym", SymbolTable(), []) - node = Call.create(sym) - # For simplicity we just monkeypatch Call.get_callees() so that it appears - # to return more than one Routine. - monkeypatch.setattr(node, "get_callees", lambda: [rout, rout]) - with pytest.raises(TransformationError) as err: - KernelModuleInlineTrans._get_psyir_to_inline(node) - # The duplicated symbol name below is purely a result of the monkeypatch - # - in reality these names will come from a generic interface and be - # different. - assert ("The target of the call to 'my_sym' cannot be inserted because " - "multiple implementations were found: ['my_sym', 'my_sym']." in - str(err.value)) - - @pytest.mark.parametrize("mod_use, sub_use", [("use my_mod, only: my_sub, my_other_sub", ""), ("", "use my_mod, only: my_sub, my_other_sub")]) @@ -853,30 +821,6 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, assert "use my_mod, only : my_other_sub\n" in output # We can't test the compilation of this code because of the 'use my_mod.' - # Check that we raise the expected error if the name of the obtained - # subroutine doesn't match that of the caller. It is not - # possible to create this situation (because _get_psyir_to_inline - # will raise an exception) so we monkeypatch. - # TODO #924 - ultimately we should be able to support this. - def fake_get_code(node): - ''' - :param node: the routine for which to get PSyIR. - :type node: :py:class:`psyclone.psyir.symbols.RoutineSymbol` - - :returns: a fake routine name and the routine PSyIR. - :rtype: Tuple(str, :py:class:`psyclone.psyir.nodes.Routine`) - ''' - code_to_inline = node.get_callees()[0] - return "broken", code_to_inline - - monkeypatch.setattr(KernelModuleInlineTrans, "_get_psyir_to_inline", - fake_get_code) - with pytest.raises(InternalError) as err: - intrans.apply(calls[1]) - assert ("Cannot module-inline call to 'broken' because its name does not " - "match that of the callee: 'my_other_sub'. TODO #924." - in str(err.value)) - def test_mod_inline_no_container(fortran_reader, fortran_writer, tmpdir, monkeypatch): From 08f18c837378671fab9bc85572b69178825c9e77 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 10:10:10 +0100 Subject: [PATCH 009/100] #2716 fix linting --- .../kernel_module_inline_trans.py | 70 +++++++++---------- src/psyclone/domain/lfric/lfric_kern.py | 47 ++++++------- src/psyclone/domain/lfric/lfric_types.py | 2 - .../kernel_module_inline_trans_test.py | 3 +- 4 files changed, 57 insertions(+), 65 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 22265d22db..0886a8bc0e 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -48,7 +48,7 @@ from psyclone.psyGen import Transformation, CodedKern from psyclone.psyir.transformations import TransformationError from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, DefaultModuleInterface, + ContainerSymbol, DataSymbol, DataTypeSymbol, IntrinsicSymbol, RoutineSymbol, Symbol) from psyclone.psyir.nodes import ( Container, Reference, Routine, ScopingNode, @@ -153,8 +153,6 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error - if not isinstance(kernels, list): - import pdb; pdb.set_trace() # TODO ARPDBG - need to examine every kernel implementation, not just # the first one. kernel_schedule = kernels[0] @@ -259,13 +257,14 @@ def _prepare_code_to_inline(routines_to_inline): for name in cblock.get_symbol_names(): all_symbols.add(cblock.scope.symbol_table.lookup(name)) - # Then decide which symbols need to be brought inside the subroutine + # Decide which symbols need to be brought inside the subroutine symbols_to_bring_in = set() for symbol in all_symbols: if symbol.is_unresolved or symbol.is_import: # This symbol is already in the symbol table, but adding it - # to the 'symbols_to_bring_in' will make the next step bring - # into the subroutine all modules that it could come from. + # to the 'symbols_to_bring_in' will make the next step + # bring into the subroutine all modules that it could come + # from. symbols_to_bring_in.add(symbol) if isinstance(symbol, DataSymbol): # DataTypes can reference other symbols @@ -295,10 +294,11 @@ def _prepare_code_to_inline(routines_to_inline): if module_symbol.name not in code_to_inline.symbol_table: code_to_inline.symbol_table.add(module_symbol) else: - # If it already exists, we know it's a container (from the - # validation) so we just need to point to it + # If it already exists, we know it's a container (from + # the validation) so we just need to point to it symbol.interface.container_symbol = \ - code_to_inline.symbol_table.lookup(module_symbol.name) + code_to_inline.symbol_table.lookup( + module_symbol.name) @staticmethod def _get_psyir_to_inline(node): @@ -397,26 +397,26 @@ def apply(self, node, options=None): node.ancestor(Container).addchild(code_to_inline.detach()) else: if existing_symbol.is_import: - # The RoutineSymbol is in the table but that is because it is - # imported. We must therefore update its interface and - # potentially remove the ContainerSymbol (from which it is - # imported) altogether. + # The RoutineSymbol is in the table but that is + # because it is imported. We must therefore update + # its interface and potentially remove the + # ContainerSymbol (from which it is imported) + # altogether. csym = existing_symbol.interface.container_symbol - # The import of the routine symbol may be in an outer scope. + # The import of the routine symbol may be in an + # outer scope. ctable = csym.find_symbol_table(node) remove_csym = (ctable.symbols_imported_from(csym) == [existing_symbol]) - #existing_symbol.interface = DefaultModuleInterface() - #existing_symbol.visibility = Symbol.Visibility.PRIVATE if code_to_inline.name == existing_symbol.name: # Have to remove Symbol as adding the Routine into # the Container will insert it again. ctable._symbols.pop(existing_symbol.name) if remove_csym: ctable.remove(csym) + # Inline the code. This will automatically add the + # associated RoutineSymbol into the Container. code_to_inline = code_to_inline.detach() - # Set the routine's symbol to the existing_symbol - #code_to_inline.symbol = existing_symbol container.addchild(code_to_inline) sym = ctable.lookup(code_to_inline.name) sym.visibility = Symbol.Visibility.PRIVATE @@ -426,31 +426,31 @@ def apply(self, node, options=None): # exactly the same. for routine in container.walk(Routine, stop_type=Routine): if routine.name == caller_name: - # This TransformationError happens here and not in the - # validation because it needs the symbols_to_bring_in - # applied to effectively compare both versions. - # This will be fixed when module-inlining versioning is - # implemented. - # (It is OK to fail here because we have not yet made - # any modifications to the tree - code_to_inline is a - # detached copy.) + # This TransformationError happens here and not in + # the validation because it needs the + # symbols_to_bring_in applied to effectively + # compare both versions. This will be fixed when + # module-inlining versioning is implemented. (It + # is OK to fail here because we have not yet made + # any modifications to the tree - code_to_inline + # is a detached copy.) if routine != code_to_inline: raise TransformationError( - f"Cannot inline subroutine '{caller_name}' " - f"because another, different, subroutine with " - f"the same name already exists and versioning " - f"of module-inlined subroutines is not " - f"implemented yet.") - # Finally, ensure that the RoutineSymbol for the inlined routine is - # in the correct symbol table. + f"Cannot inline subroutine '{caller_name}'" + f" because another, different, subroutine " + f"with the same name already exists and " + f"versioning of module-inlined subroutines" + f" is not implemented yet.") + # Finally, ensure that the RoutineSymbol for the inlined + # routine is in the correct symbol table. routine_symbol = existing_symbol table = routine_symbol.find_symbol_table(node) if table.node is not container: # Set the visibility of the symbol to always be private. sym = container.symbol_table.lookup(routine_symbol.name) sym.visibility = Symbol.Visibility.PRIVATE - # Force removal of the routine_symbol if its also present in - # the Routine's symbol table. + # Force removal of the routine_symbol if it's also present + # in the Routine's symbol table. table.lookup(routine_symbol.name) norm_name = table._normalize(routine_symbol.name) table._symbols.pop(norm_name) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 5e12b54be0..ce67fdb499 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -679,29 +679,23 @@ class creates the PSyIR schedule on first invocation which is # problem in LFRic that this fixes. routine.debug_string() -# if len(routines) == 1: -# sched = routines[0] -# # TODO #928: We don't validate the arguments yet because the -# # validation has many false negatives. -# # self.validate_kernel_code_args(sched.symbol_table) +# # The kernel name corresponds to an interface block. Find which +# # of the routines matches the precision of the arguments. +# for routine in routines: +# try: +# # The validity check for the kernel arguments should raise +# # an exception if the precisions don't match but currently +# # does not! +# self.validate_kernel_code_args(routine.symbol_table) +# sched = routine +# break +# except GenerationError: +# pass # else: -# # The kernel name corresponds to an interface block. Find which -# # of the routines matches the precision of the arguments. -# for routine in routines: -# try: -# # The validity check for the kernel arguments will raise -# # an exception if the precisions don't match. -# self.validate_kernel_code_args(routine.symbol_table) -# sched = routine -# break -# except GenerationError: -# pass -# else: -# raise GenerationError( -# f"Failed to find a kernel implementation with an interface" -# f" that matches the invoke of '{self.name}'. (Tried " -# f"routines {[item.name for item in routines]}.)") -# +# raise GenerationError( +# f"Failed to find a kernel implementation with an interface" +# f" that matches the invoke of '{self.name}'. (Tried " +# f"routines {[item.name for item in routines]}.)") if len(routines) > 1: table = routines[0].scope.symbol_table @@ -711,10 +705,11 @@ class creates the PSyIR schedule on first invocation which is new_schedules = [] for sched in routines: - # TODO #935 - replace the PSyIR argument data symbols with LFRic data - # symbols. For the moment we just return the unmodified PSyIR schedule - # but this should use RaisePSyIR2LFRicKernTrans once KernelInterface - # is fully functional (#928). + # TODO #935 - replace the PSyIR argument data symbols with + # LFRic data symbols. For the moment we just return the + # unmodified PSyIR schedule but this should use + # RaisePSyIR2LFRicKernTrans once KernelInterface is fully + # functional (#928). ksched = KernelSchedule(sched.symbol, symbol_table=sched.symbol_table.detach()) for child in sched.pop_all_children(): diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index afa066b1dc..fe71a834a3 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -185,8 +185,6 @@ def _create_generic_scalars(): "precision"]) api_config = Config.get().api_conf("lfric") - lfric_kinds = list(api_config.precision_map.keys()) - generic_scalar_datatypes = [ GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER, LFRicTypes("I_DEF")), diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 2919986e8f..682242257f 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -44,13 +44,12 @@ from fparser.common.readfortran import FortranStringReader from psyclone.configuration import Config from psyclone.domain.common.transformations import KernelModuleInlineTrans -from psyclone.errors import InternalError from psyclone.psyGen import CodedKern, Kern from psyclone.psyir.nodes import ( Container, Routine, CodeBlock, Call, IntrinsicCall) from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, ImportInterface, RoutineSymbol, REAL_TYPE, - Symbol, SymbolError, SymbolTable) + Symbol, SymbolError) from psyclone.psyir.transformations import TransformationError from psyclone.tests.gocean_build import GOceanBuild from psyclone.tests.lfric_build import LFRicBuild From 53147c6cbb2e775e870c26b3492e34d0b53f18f5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 10:10:52 +0100 Subject: [PATCH 010/100] #2716 more linting --- src/psyclone/domain/lfric/lfric_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index fe71a834a3..c3bb104eca 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -183,7 +183,6 @@ def _create_generic_scalars(): ''' GenericScalar = namedtuple('GenericScalar', ["name", "intrinsic", "precision"]) - api_config = Config.get().api_conf("lfric") generic_scalar_datatypes = [ GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER, From 4198608d9be383d83667a0b8b98430cdef53ba27 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 11:46:25 +0100 Subject: [PATCH 011/100] #2716 fix a lot of tests --- ...teration_boundaries_inside_kernel_trans.py | 10 +- .../transformations/gocean_opencl_trans.py | 9 +- src/psyclone/psyGen.py | 10 +- .../tests/domain/gocean/kernel/gokern_test.py | 10 +- .../transformations/globalstoargs_test.py | 6 +- .../gocean1p0_transformations_test.py | 15 +- ...ion_boundaries_inside_kernel_trans_test.py | 7 +- .../tests/domain/lfric/lfric_kern_test.py | 25 +- .../dynamo0p3_transformations_test.py | 6 +- src/psyclone/tests/psyGen_test.py | 6 +- .../fparser2_find_or_create_symbol_test.py | 3 +- src/psyclone/transformations.py | 268 +++++++++--------- 12 files changed, 215 insertions(+), 160 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index d65cbc2d9e..0424f69265 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -109,6 +109,12 @@ def validate(self, node, options=None): f"can only be applied to 'GOKern' nodes, but found " f"'{type(node).__name__}'.") + _, kschedules = node.get_kernel_schedule() + if len(kschedules) > 1: + raise TransformationError( + f"Error in {self.name} transformation. Polymorphic kernels " + f"are not supported but kernel '{node.name}' corresponds to ") + def apply(self, node, options=None): '''Apply this transformation to the supplied node. @@ -180,7 +186,9 @@ def apply(self, node, options=None): outer_loop.iteration_space = "go_all_pts" # Update Kernel - kschedule = node.get_kernel_schedule() + _, kschedules = node.get_kernel_schedule() + # validate() has checked that this kernel is not polymorphic. + kschedule = kschedules[0] kernel_st = kschedule.symbol_table iteration_indices = kernel_st.iteration_indices data_arguments = kernel_st.data_arguments diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index aa5c57f0ab..4d84aeeb34 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -192,7 +192,10 @@ def validate(self, node, options=None): # any form of global data (that is not a routine argument). for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) - ksched = kern.get_kernel_schedule() + _, kschedules = kern.get_kernel_schedule() + if len(kschedules) > 1: + raise TransformationError("TODO") + ksched = kschedules[0] global_variables = ksched.symbol_table.imported_symbols if global_variables: raise TransformationError( @@ -749,7 +752,9 @@ def _insert_kernel_code_in_opencl_file(self, kernel): # Create a copy of the kernel and remove precision symbols since they # are not supported in the OpenCL backend. - kernel_copy = kernel.get_kernel_schedule().copy() + _, schedules = kernel.get_kernel_schedule() + # validate() has checked that the kernel is not polymorphic. + kernel_copy = schedules[0].copy() symtab = kernel_copy.symbol_table # TODO #898: Removing symbols is not properly supported by PSyIR diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index b09bbb818e..5c70a7f9e8 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1683,7 +1683,8 @@ def rename_and_write(self): # Start from the root of the schedule as we want to output # any module information surrounding the kernel subroutine # as well as the subroutine itself. - new_kern_code = fortran_writer(self.get_kernel_schedule().root) + _, schedules = self.get_kernel_schedule() + new_kern_code = fortran_writer(schedules[0].root) fll = FortLineLength() new_kern_code = fll.process(new_kern_code) @@ -1722,9 +1723,10 @@ def _rename_psyir(self, suffix): ''' # We need to get the kernel schedule before modifying self.name. - # If the kernel corresponds to an interface, this will get the - # implementation that is actually being called. - kern_schedule = self.get_kernel_schedule() + _, kern_schedules = self.get_kernel_schedule() + if len(kern_schedules) > 1: + raise NotImplementedError("TODO") + kern_schedule = kern_schedules[0] container = kern_schedule.ancestor(Container) # Use the suffix to create a new kernel name. This will diff --git a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py index 5d84ce1e3a..b88525c68a 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py @@ -100,11 +100,15 @@ def test_gok_get_kernel_schedule(): schedule = psy.invokes.invoke_list[0].schedule kern = schedule.walk(GOKern)[0] assert kern._kern_schedule is None - sched = kern.get_kernel_schedule() + sym, scheds = kern.get_kernel_schedule() + assert sym is None + assert isinstance(scheds, list) + assert len(scheds) == 1 + sched = scheds[0] assert isinstance(sched, GOKernelSchedule) # A second call should just return the previously-obtained schedule. - sched2 = kern.get_kernel_schedule() - assert sched2 is sched + sym, scheds2 = kern.get_kernel_schedule() + assert scheds2[0] is sched # Check that the expected error is raised if the subroutine that # implements the kernel cannot be found. kern._kern_schedule = None diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 5206abf84b..560bc3a498 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -220,7 +220,8 @@ def create_data_symbol(arg): trans.apply(kernel) fwriter = FortranWriter() - kernel_code = fwriter(kernel.get_kernel_schedule()) + _, kernels = kernel.get_kernel_schedule() + kernel_code = fwriter(kernels[0]) assert ("subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " "magic)" in kernel_code) @@ -298,7 +299,8 @@ def create_data_symbol(arg): monkeypatch.setattr(DataSymbol, "resolve_type", create_data_symbol) for num, kernel in enumerate(invoke.schedule.coded_kernels()): - kschedule = kernel.get_kernel_schedule() + _, kernels = kernel.get_kernel_schedule() + kschedule = kernels[0] trans.apply(kernel) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py index b965696b15..1d917a5d7f 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -1499,7 +1499,8 @@ def test_accroutinetrans_with_kern(fortran_writer, monkeypatch): assert rtrans.name == "ACCRoutineTrans" rtrans.apply(kern) # Check that there is a acc routine directive in the kernel - code = fortran_writer(kern.get_kernel_schedule()) + _, schedules = kern.get_kernel_schedule() + code = fortran_writer(schedules[0]) assert "!$acc routine seq\n" in code # If the kernel schedule is not accessible, the transformation fails @@ -1522,16 +1523,16 @@ def test_accroutinetrans_with_routine(fortran_writer): assert isinstance(kern, GOKern) rtrans = ACCRoutineTrans() assert rtrans.name == "ACCRoutineTrans" - routine = kern.get_kernel_schedule() - rtrans.apply(routine) + _, routines = kern.get_kernel_schedule() + rtrans.apply(routines[0]) # Check that there is a acc routine directive in the routine - code = fortran_writer(routine) + code = fortran_writer(routines[0]) assert "!$acc routine seq\n" in code # Even if applied multiple times the Directive is only there once - previous_num_children = len(routine.children) - rtrans.apply(routine) - assert previous_num_children == len(routine.children) + previous_num_children = len(routines[0].children) + rtrans.apply(routines[0]) + assert previous_num_children == len(routines[0].children) def test_accroutinetrans_with_invalid_node(): diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index 4a1f1f6b7e..d54431aa4a 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -81,7 +81,9 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): # Add some name conflicting symbols in the Invoke and the Kernel kernel.ancestor(Container).symbol_table.new_symbol("xstop") - kernel.get_kernel_schedule().symbol_table.new_symbol("ystart") + _, routines = kernel.get_kernel_schedule() + ksched = routines[0] + ksched.symbol_table.new_symbol("ystart") # Apply the transformation trans = GOMoveIterationBoundariesInsideKernelTrans() @@ -118,7 +120,8 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): assert kernel.arguments.args[-1].argument_type == "scalar" # Check that the kernel subroutine has been transformed: - kschedule = kernel.get_kernel_schedule() + _, kschedules = kernel.get_kernel_schedule() + kschedule = kschedules[0] # - It has the boundary conditions mask assert isinstance(kschedule.children[0], IfBlock) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index c8ef438fe1..fba33c75a8 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -151,14 +151,20 @@ def test_get_kernel_schedule(): assert kernel._kern_schedule is None - kernel_schedule = kernel.get_kernel_schedule() - assert isinstance(kernel_schedule, KernelSchedule) - assert kernel._kern_schedule is kernel_schedule + sym, kernel_schedules = kernel.get_kernel_schedule() + assert sym is None + assert len(kernel_schedules) == 1 + assert isinstance(kernel_schedules[0], KernelSchedule) + assert kernel._kern_schedule[0] is kernel_schedules[0] - kernel_schedule_2 = kernel.get_kernel_schedule() - assert kernel_schedule is kernel_schedule_2 + _, kernel_schedules_2 = kernel.get_kernel_schedule() + assert kernel_schedules[0] is kernel_schedules_2[0] +@pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" + " implementations of a polymorphic kernel. We need to " + "put back (and fix) the ability to resolve which " + "implementation is being called.") def test_get_kernel_schedule_mixed_precision(): ''' Test that we can get the correct schedule for a mixed-precision kernel. @@ -183,6 +189,10 @@ def test_get_kernel_schedule_mixed_precision(): assert sched.name == f"mixed_code_{8*precision}" +@pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" + " implementations of a polymorphic kernel. We need to " + "put back (and fix) the ability to resolve which " + "implementation is being called.") def test_get_kernel_sched_mixed_precision_no_match(monkeypatch): ''' Test that we get the expected error if there's no matching implementation @@ -222,7 +232,8 @@ def test_validate_kernel_code_args(monkeypatch): schedule = psy.invokes.invoke_list[0].schedule # matrix vector kernel kernel = schedule[2].loop_body[0] - sched = kernel.get_kernel_schedule() + _, schedules = kernel.get_kernel_schedule() + sched = schedules[0] kernel.validate_kernel_code_args(sched.symbol_table) # Force LFRicKern to think that this kernel is an 'apply' kernel and @@ -230,7 +241,7 @@ def test_validate_kernel_code_args(monkeypatch): monkeypatch.setattr(kernel, "_cma_operation", "apply") with pytest.raises(GenerationError) as info: kernel.validate_kernel_code_args( - kernel.get_kernel_schedule().symbol_table) + sched.symbol_table) assert ( "In kernel 'matrix_vector_code' the number of arguments indicated by " "the kernel metadata is 8 but the actual number of kernel arguments " diff --git a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py index 7917d1cfa3..a176224744 100644 --- a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py @@ -7586,7 +7586,8 @@ def test_kern_const_invalid_make_constant1(): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") - kernel_schedule = kernel.get_kernel_schedule() + _, kernel_schedules = kernel.get_kernel_schedule() + kernel_schedule = kernel_schedules[0] symbol_table = kernel_schedule.symbol_table # Make the symbol table's argument list empty. We have to make sure that # the interface of any existing argument Symbols is set to @@ -7613,7 +7614,8 @@ def test_kern_const_invalid_make_constant2(): kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") kctrans = Dynamo0p3KernelConstTrans() - kernel_schedule = kernel.get_kernel_schedule() + _, kernel_schedules = kernel.get_kernel_schedule() + kernel_schedule = kernel_schedules[0] symbol_table = kernel_schedule.symbol_table symbol = symbol_table._argument_list[7] # Expecting scalar integer. Set to array. diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index c5277faea7..a4b84ba0e8 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -466,8 +466,10 @@ def test_kern_get_kernel_schedule(): psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) schedule = psy.invokes.invoke_list[0].schedule kern = schedule.children[0].loop_body[0] - kern_schedule = kern.get_kernel_schedule() - assert isinstance(kern_schedule, KernelSchedule) + sym, kern_schedules = kern.get_kernel_schedule() + assert sym is None + assert len(kern_schedules) == 1 + assert isinstance(kern_schedules[0], KernelSchedule) def test_codedkern_node_str(): diff --git a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py index 9efe567f97..15705749b8 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py @@ -61,7 +61,8 @@ def test_find_or_create_unresolved_symbol(): api="gocean", idx=0) sched = invoke.schedule kernels = sched.walk(Kern) - kernel_schedule = kernels[0].get_kernel_schedule() + _, kernel_schedules = kernels[0].get_kernel_schedule() + kernel_schedule = kernel_schedules[0] references = kernel_schedule.walk(Reference) # Symbol in KernelSchedule SymbolTable diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 3b283a8049..7dfcaffccc 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -405,7 +405,7 @@ def validate_it_can_run_on_gpu(self, node, options): # or that the frontend failed to convert it into PSyIR) reraise it # as a TransformationError try: - kernel_schedule = node.get_kernel_schedule() + _, kernel_schedules = node.get_kernel_schedule() except Exception as error: raise TransformationError( f"Failed to create PSyIR for kernel '{node.name}'. " @@ -414,72 +414,73 @@ def validate_it_can_run_on_gpu(self, node, options): k_or_r = "Kernel" else: # Supplied node is a PSyIR Routine which *is* a Schedule. - kernel_schedule = node + kernel_schedules = [node] k_or_r = "routine" # Check that the routine does not access any data that is imported via # a 'use' statement. # TODO #2271 - this implementation will not catch symbols from literal # precisions or intialisation expressions. - refs = kernel_schedule.walk(Reference) - for ref in refs: - if ref.symbol.is_import: - # resolve_type does nothing if the Symbol type is known. - try: - ref.symbol.resolve_type() - except (SymbolError, FileNotFoundError): - # TODO #11 - log that we failed to resolve this Symbol. - pass - if (isinstance(ref.symbol, DataSymbol) and - ref.symbol.is_constant): - # An import of a compile-time constant is fine. - continue - raise TransformationError( - f"{k_or_r} '{node.name}' accesses the symbol " - f"'{ref.symbol}' which is imported. If this symbol " - f"represents data then it must first be converted to a " - f"{k_or_r} argument using the KernelImportsToArguments " - f"transformation.") - - # We forbid CodeBlocks because we can't be certain that what they - # contain can be executed on a GPU. However, we do permit the user - # to override this check. - cblocks = kernel_schedule.walk(CodeBlock) - if not force: - if cblocks: - cblock_txt = ("\n " + "\n ".join(str(node) for node in - cblocks[0].get_ast_nodes) - + "\n") - option_txt = "options={'force': True}" - raise TransformationError( - f"Cannot safely apply {type(self).__name__} to {k_or_r} " - f"'{node.name}' because its PSyIR contains one or more " - f"CodeBlocks:{cblock_txt}You may use '{option_txt}' to " - f"override this check.") - else: - # Check any accesses within CodeBlocks. - # TODO #2271 - this will be handled as part of the checking to be - # implemented using the dependence analysis. - for cblock in cblocks: - names = cblock.get_symbol_names() - for name in names: - sym = kernel_schedule.symbol_table.lookup(name) - if sym.is_import: - raise TransformationError( - f"{k_or_r} '{node.name}' accesses the symbol " - f"'{sym.name}' within a CodeBlock and this symbol " - f"is imported. {type(self).__name__} cannot be " - f"applied to such a {k_or_r}.") - - calls = kernel_schedule.walk(Call) - for call in calls: - if not call.is_available_on_device(): - call_str = call.debug_string().rstrip("\n") - raise TransformationError( - f"{k_or_r} '{node.name}' calls another routine " - f"'{call_str}' which is not available on the " - f"accelerator device and therefore cannot have " - f"{type(self).__name__} applied to it (TODO #342).") + for ksched in kernel_schedules: + refs = ksched.walk(Reference) + for ref in refs: + if ref.symbol.is_import: + # resolve_type does nothing if the Symbol type is known. + try: + ref.symbol.resolve_type() + except (SymbolError, FileNotFoundError): + # TODO #11 - log that we failed to resolve this Symbol. + pass + if (isinstance(ref.symbol, DataSymbol) and + ref.symbol.is_constant): + # An import of a compile-time constant is fine. + continue + raise TransformationError( + f"{k_or_r} '{node.name}' accesses the symbol " + f"'{ref.symbol}' which is imported. If this symbol " + f"represents data then it must first be converted to a" + f" {k_or_r} argument using the " + f"KernelImportsToArguments transformation.") + + # We forbid CodeBlocks because we can't be certain that what they + # contain can be executed on a GPU. However, we do permit the user + # to override this check. + cblocks = ksched.walk(CodeBlock) + if not force: + if cblocks: + cblock_txt = ("\n " + "\n ".join( + str(node) for node in cblocks[0].get_ast_nodes) + + "\n") + option_txt = "options={'force': True}" + raise TransformationError( + f"Cannot safely apply {type(self).__name__} to {k_or_r} " + f"'{node.name}' because its PSyIR contains one or more " + f"CodeBlocks:{cblock_txt}You may use '{option_txt}' to " + f"override this check.") + else: + # Check any accesses within CodeBlocks. + # TODO #2271 - this will be handled as part of the checking to be + # implemented using the dependence analysis. + for cblock in cblocks: + names = cblock.get_symbol_names() + for name in names: + sym = ksched.symbol_table.lookup(name) + if sym.is_import: + raise TransformationError( + f"{k_or_r} '{node.name}' accesses the symbol " + f"'{sym.name}' within a CodeBlock and this symbol " + f"is imported. {type(self).__name__} cannot be " + f"applied to such a {k_or_r}.") + + calls = ksched.walk(Call) + for call in calls: + if not call.is_available_on_device(): + call_str = call.debug_string().rstrip("\n") + raise TransformationError( + f"{k_or_r} '{node.name}' calls another routine " + f"'{call_str}' which is not available on the " + f"accelerator device and therefore cannot have " + f"{type(self).__name__} applied to it (TODO #342).") class OMPDeclareTargetTrans(Transformation, MarkRoutineForGPUMixin): @@ -2264,58 +2265,59 @@ def make_constant(symbol_table, arg_position, value, arg_list_info = KernCallArgList(kernel) arg_list_info.generate() try: - kernel_schedule = kernel.get_kernel_schedule() + _, kernel_schedules = kernel.get_kernel_schedule() except NotImplementedError as excinfo: raise TransformationError( f"Failed to parse kernel '{kernel.name}'. Error reported was " f"'{excinfo}'.") from excinfo - symbol_table = kernel_schedule.symbol_table - if number_of_layers: - make_constant(symbol_table, arg_list_info.nlayers_positions[0], - number_of_layers) - - if quadrature and arg_list_info.nqp_positions: - # TODO #705 - support the transformation of kernels requiring - # other quadrature types (face/edge, multiple). - if kernel.eval_shapes == ["gh_quadrature_xyoz"]: - make_constant(symbol_table, - arg_list_info.nqp_positions[0]["horizontal"], - element_order+3) - make_constant(symbol_table, - arg_list_info.nqp_positions[0]["vertical"], - element_order+3) - else: - raise TransformationError( - f"Error in Dynamo0p3KernelConstTrans transformation. " - f"Support is currently limited to 'xyoz' quadrature but " - f"found {kernel.eval_shapes}.") - - const = LFRicConstants() - if element_order is not None: - # Modify the symbol table for degrees of freedom here. - for info in arg_list_info.ndf_positions: - if (info.function_space.lower() in - (const.VALID_ANY_SPACE_NAMES + - const.VALID_ANY_DISCONTINUOUS_SPACE_NAMES + - ["any_w2"])): - # skip any_space_*, any_discontinuous_space_* and any_w2 - print(f" Skipped dofs, arg position {info.position}, " - f"function space {info.function_space}") + for kernel_schedule in kernel_schedules: + symbol_table = kernel_schedule.symbol_table + if number_of_layers: + make_constant(symbol_table, arg_list_info.nlayers_positions[0], + number_of_layers) + + if quadrature and arg_list_info.nqp_positions: + # TODO #705 - support the transformation of kernels requiring + # other quadrature types (face/edge, multiple). + if kernel.eval_shapes == ["gh_quadrature_xyoz"]: + make_constant(symbol_table, + arg_list_info.nqp_positions[0]["horizontal"], + element_order+3) + make_constant(symbol_table, + arg_list_info.nqp_positions[0]["vertical"], + element_order+3) else: - try: - ndofs = Dynamo0p3KernelConstTrans. \ - space_to_dofs[ - info.function_space](element_order) - except KeyError as err: - raise InternalError( - f"Error in Dynamo0p3KernelConstTrans " - f"transformation. Unsupported function space " - f"'{info.function_space}' found. Expecting one of " - f"""{Dynamo0p3KernelConstTrans. - space_to_dofs.keys()}.""") from err - make_constant(symbol_table, info.position, ndofs, - function_space=info.function_space) + raise TransformationError( + f"Error in Dynamo0p3KernelConstTrans transformation. " + f"Support is currently limited to 'xyoz' quadrature but " + f"found {kernel.eval_shapes}.") + + const = LFRicConstants() + if element_order is not None: + # Modify the symbol table for degrees of freedom here. + for info in arg_list_info.ndf_positions: + if (info.function_space.lower() in + (const.VALID_ANY_SPACE_NAMES + + const.VALID_ANY_DISCONTINUOUS_SPACE_NAMES + + ["any_w2"])): + # skip any_space_*, any_discontinuous_space_* and any_w2 + print(f" Skipped dofs, arg position {info.position}, " + f"function space {info.function_space}") + else: + try: + ndofs = Dynamo0p3KernelConstTrans. \ + space_to_dofs[ + info.function_space](element_order) + except KeyError as err: + raise InternalError( + f"Error in Dynamo0p3KernelConstTrans " + f"transformation. Unsupported function space " + f"'{info.function_space}' found. Expecting one of " + f"""{Dynamo0p3KernelConstTrans. + space_to_dofs.keys()}.""") from err + make_constant(symbol_table, info.position, ndofs, + function_space=info.function_space) # Flag that the kernel has been modified kernel.modified = True @@ -2568,18 +2570,19 @@ def apply(self, node, options=None): node.modified = True # Get the schedule representing the kernel subroutine - routine = node.get_kernel_schedule() + sym, routines = node.get_kernel_schedule() else: - routine = node - - # Insert the directive to the routine if it doesn't already exist - for child in routine.children: - if isinstance(child, ACCRoutineDirective): - return # The routine is already marked with ACCRoutine + routines = [node] para = options.get("parallelism", "seq") if options else "seq" + for routine in routines: + # Insert the directive to the routine if it doesn't already exist + for child in routine.children: + if isinstance(child, ACCRoutineDirective): + return # The routine is already marked with ACCRoutine - routine.children.insert(0, ACCRoutineDirective(parallelism=para)) + routine.children.insert( + 0, ACCRoutineDirective(parallelism=para)) def validate(self, node, options=None): ''' @@ -2786,10 +2789,12 @@ def validate(self, node, options=None): :type options: Optional[Dict[str, Any]] :raises TransformationError: if the supplied node is not a CodedKern. - :raises TransformationError: if this transformation is not applied to \ + :raises TransformationError: if this transformation is not applied to a Gocean API Invoke. - :raises TransformationError: if the supplied kernel contains wildcard \ - imports of symbols from one or more containers (e.g. a USE without\ + :raises TransformationError: if the supplied node is a polymorphic + Kernel. + :raises TransformationError: if the supplied kernel contains wildcard + imports of symbols from one or more containers (e.g. a USE without an ONLY clause in Fortran). ''' if not isinstance(node, CodedKern): @@ -2806,19 +2811,26 @@ def validate(self, node, options=None): # Check that there are no unqualified imports or undeclared symbols try: - kernel = node.get_kernel_schedule() + _, kernels = node.get_kernel_schedule() except SymbolError as err: raise TransformationError( f"Kernel '{node.name}' contains undeclared symbol: " f"{err.value}") from err - symtab = kernel.symbol_table - for container in symtab.containersymbols: - if container.wildcard_import: - raise TransformationError( - f"Kernel '{node.name}' has a wildcard import of symbols " - f"from container '{container.name}'. This is not " - f"supported.") + if len(kernels) > 1: + raise TransformationError( + f"The {self.name} transformation does not support polymorphic " + f"kernels but found the following implementations for kernel " + f"'{node.name}': {[kern.name for kern in kernels]}") + + for kernel in kernels: + symtab = kernel.symbol_table + for container in symtab.containersymbols: + if container.wildcard_import: + raise TransformationError( + f"Kernel '{node.name}' has a wildcard import of " + f"symbols from container '{container.name}'. This is " + f"not supported.") # TODO #649. Check for variables accessed by the kernel but declared # in an outer scope. @@ -2838,7 +2850,9 @@ def apply(self, node, options=None): self.validate(node, options) - kernel = node.get_kernel_schedule() + _, kernels = node.get_kernel_schedule() + # validate() has ensured that there is only one kernel routine. + kernel = kernels[0] symtab = kernel.symbol_table invoke_symtab = node.ancestor(InvokeSchedule).symbol_table count_imported_vars_removed = 0 From e0799889d2bedbc0afe5e9889bad52d072776a16 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 13:27:51 +0100 Subject: [PATCH 012/100] #2716 fix remaining tests --- src/psyclone/psyGen.py | 34 +++++++------- .../kernel_transformation_test.py | 45 ++++++------------- src/psyclone/transformations.py | 32 ++++++------- 3 files changed, 49 insertions(+), 62 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 5c70a7f9e8..b886cb2910 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1723,30 +1723,34 @@ def _rename_psyir(self, suffix): ''' # We need to get the kernel schedule before modifying self.name. - _, kern_schedules = self.get_kernel_schedule() - if len(kern_schedules) > 1: - raise NotImplementedError("TODO") - kern_schedule = kern_schedules[0] - container = kern_schedule.ancestor(Container) + interface_sym, kern_schedules = self.get_kernel_schedule() + container = kern_schedules[0].ancestor(Container) # Use the suffix to create a new kernel name. This will # conform to the PSyclone convention of ending in "_code" orig_mod_name = self.module_name[:] - orig_kern_name = kern_schedule.name[:] - - new_kern_name = self._new_name(orig_kern_name, suffix, "_code") new_mod_name = self._new_name(orig_mod_name, suffix, "_mod") - # Change the name of this kernel and the associated - # module. These names are used when generating the PSy-layer. - self.name = new_kern_name[:] + # If the kernel is polymorphic, we can just change the name of + # the interface. + if interface_sym: + orig_kern_name = interface_sym.name + new_kern_name = self._new_name(orig_kern_name, suffix, "_code") + container.symbol_table.rename_symbol(interface_sym, new_kern_name) + self.name = new_kern_name + else: + kern_schedule = kern_schedules[0] + orig_kern_name = kern_schedule.name[:] + new_kern_name = self._new_name(orig_kern_name, suffix, "_code") + + # Change the name of this kernel and the associated + # module. These names are used when generating the PSy-layer. + self.name = new_kern_name[:] + kern_schedule.name = new_kern_name[:] + self._module_name = new_mod_name[:] - kern_schedule.name = new_kern_name[:] container.name = new_mod_name[:] - # Change the name of the Kernel Schedule - kern_schedule.name = new_kern_name - # Ensure the metadata points to the correct procedure now. Since this # routine is general purpose, we won't always have a domain-specific # Container here and if we don't, it won't have a 'metadata' property. diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 0aeadf25e0..b5fd9e08b7 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -288,12 +288,17 @@ def test_transform_kern_with_interface(kernel_outputdir): with open(filename, "r", encoding="utf-8") as ffile: contents = ffile.read() - # Check that the routine name has been updated within the interface. - assert "interface mixed_code" in contents - assert ("module procedure :: mixed_code_32, mixed_code_64_0_code" + # Check that the interface name has been updated. + assert "interface mixed_0_code" in contents + assert ("module procedure :: mixed_code_32, mixed_code_64" in contents) - # Check that the subroutine itself has been renamed. - assert "subroutine mixed_code_64_0_code" in contents + # Check that the subroutines themselves haven't been renamed. + assert "subroutine mixed_code_32" in contents + assert "subroutine mixed_code_64" in contents + # But they have been transformed. + assert ('''real*4, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op + + !$acc routine seq''' in contents) assert ('''real*8, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op !$acc routine seq''' in contents) @@ -320,30 +325,6 @@ def test_gpumixin_validate_wrong_node_type(): "Routine but got 'FileContainer'" in str(err.value)) -def test_gpumixin_kernel_interface(kernel_outputdir, monkeypatch, - fortran_reader, fortran_writer): - ''' - Test that the MarkRoutineForGPUMixin.validate() rejects a kernel that has - multiple implementations (i.e. for different precisions). - - TODO this limitation is the subject of #1946. - - ''' - # Ensure kernel-output directory is uninitialised - config = Config.get() - monkeypatch.setattr(config, "_kernel_naming", "multiple") - psy, invoke = get_invoke("26.8_mixed_precision_args.f90", - api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.walk(Kern) - rtrans = ACCRoutineTrans() - # Use force because the kernel contains a WRITE statement. - with pytest.raises(TransformationError) as err: - rtrans.apply(kernels[0], options={"force": True}) - assert ("Cannot apply ACCRoutineTrans to kernel 'mixed_code' as it has " - "multiple implementations - TODO #1946" in str(err.value)) - - def test_gpumixin_validate_no_schedule(monkeypatch): ''' Test that the MarkRoutineForGPUMixin.validate_it_can_run_on_gpu() method @@ -456,7 +437,8 @@ def test_gpumixin_validate_no_call(): in str(err.value)) # The same error happens for unsupported GPU intrinsics - call = kernel.get_kernel_schedule().walk(Call)[0] + _, kschedules = kernel.get_kernel_schedule() + call = kschedules[0].walk(Call)[0] call.replace_with( IntrinsicCall.create(IntrinsicCall.Intrinsic.GET_COMMAND)) with pytest.raises(TransformationError) as err: @@ -464,8 +446,7 @@ def test_gpumixin_validate_no_call(): assert ("Kernel 'testkern_with_call_code' calls another routine " "'GET_COMMAND()' which is not available on the accelerator device " "and therefore cannot have ACCRoutineTrans applied to it " - "(TODO #342)." - in str(err.value)) + "(TODO #342)." in str(err.value)) def test_1kern_trans(kernel_outputdir): diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 7dfcaffccc..d2c3678fc5 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -453,14 +453,14 @@ def validate_it_can_run_on_gpu(self, node, options): + "\n") option_txt = "options={'force': True}" raise TransformationError( - f"Cannot safely apply {type(self).__name__} to {k_or_r} " - f"'{node.name}' because its PSyIR contains one or more " - f"CodeBlocks:{cblock_txt}You may use '{option_txt}' to " - f"override this check.") + f"Cannot safely apply {type(self).__name__} to " + f"{k_or_r} '{node.name}' because its PSyIR contains " + f"one or more CodeBlocks:{cblock_txt}You may use " + f"'{option_txt}' to override this check.") else: # Check any accesses within CodeBlocks. - # TODO #2271 - this will be handled as part of the checking to be - # implemented using the dependence analysis. + # TODO #2271 - this will be handled as part of the checking to + # be implemented using the dependence analysis. for cblock in cblocks: names = cblock.get_symbol_names() for name in names: @@ -468,9 +468,9 @@ def validate_it_can_run_on_gpu(self, node, options): if sym.is_import: raise TransformationError( f"{k_or_r} '{node.name}' accesses the symbol " - f"'{sym.name}' within a CodeBlock and this symbol " - f"is imported. {type(self).__name__} cannot be " - f"applied to such a {k_or_r}.") + f"'{sym.name}' within a CodeBlock and this " + f"symbol is imported. {type(self).__name__} " + f"cannot be applied to such a {k_or_r}.") calls = ksched.walk(Call) for call in calls: @@ -2290,8 +2290,8 @@ def make_constant(symbol_table, arg_position, value, else: raise TransformationError( f"Error in Dynamo0p3KernelConstTrans transformation. " - f"Support is currently limited to 'xyoz' quadrature but " - f"found {kernel.eval_shapes}.") + f"Support is currently limited to 'xyoz' quadrature " + f"but found {kernel.eval_shapes}.") const = LFRicConstants() if element_order is not None: @@ -2301,9 +2301,10 @@ def make_constant(symbol_table, arg_position, value, (const.VALID_ANY_SPACE_NAMES + const.VALID_ANY_DISCONTINUOUS_SPACE_NAMES + ["any_w2"])): - # skip any_space_*, any_discontinuous_space_* and any_w2 - print(f" Skipped dofs, arg position {info.position}, " - f"function space {info.function_space}") + # skip any_space_*, any_discontinuous_space_* & any_w2 + print(f" Skipped dofs, arg position " + f"{info.position}, function space " + f"{info.function_space}") else: try: ndofs = Dynamo0p3KernelConstTrans. \ @@ -2313,7 +2314,8 @@ def make_constant(symbol_table, arg_position, value, raise InternalError( f"Error in Dynamo0p3KernelConstTrans " f"transformation. Unsupported function space " - f"'{info.function_space}' found. Expecting one of " + f"'{info.function_space}' found. Expecting one" + f" of " f"""{Dynamo0p3KernelConstTrans. space_to_dofs.keys()}.""") from err make_constant(symbol_table, info.position, ndofs, From ca153a4dd58e77c0f14fb092345f870d6c14f4eb Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 14:21:15 +0100 Subject: [PATCH 013/100] #2716 fix examples --- examples/gocean/eg3/ocl_trans.py | 13 ++++++++----- examples/lfric/eg15/matvec_opt.py | 7 +++++-- examples/lfric/scripts/kernel_print.py | 11 ++++++----- examples/xdsl/backend/xdsl.py | 4 +++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index 3cd05527b2..32b50005c1 100644 --- a/examples/gocean/eg3/ocl_trans.py +++ b/examples/gocean/eg3/ocl_trans.py @@ -36,10 +36,10 @@ ''' Module providing a transformation script that converts the Schedule of the first Invoke to use OpenCL. ''' -from psyclone.psyir.transformations import \ - FoldConditionalReturnExpressionsTrans -from psyclone.domain.gocean.transformations import GOOpenCLTrans, \ - GOMoveIterationBoundariesInsideKernelTrans +from psyclone.psyir.transformations import ( + FoldConditionalReturnExpressionsTrans) +from psyclone.domain.gocean.transformations import ( + GOOpenCLTrans, GOMoveIterationBoundariesInsideKernelTrans) def trans(psy): @@ -68,7 +68,10 @@ def trans(psy): move_boundaries_trans.apply(kern) # Change the syntax to remove the return statements introduced by the # previous transformation - fold_trans.apply(kern.get_kernel_schedule()) + _, kschedules = kern.get_kernel_schedule() + # NOTE: we assume the kernel is not polymorphic and thus there is + # only one schedule associated with it. + fold_trans.apply(kschedules[0]) # Specify the OpenCL queue and workgroup size of the kernel # In this case we dispatch each kernel in a different queue to check # that the output code has the necessary barriers to guarantee the diff --git a/examples/lfric/eg15/matvec_opt.py b/examples/lfric/eg15/matvec_opt.py index 078480249f..924afdb615 100644 --- a/examples/lfric/eg15/matvec_opt.py +++ b/examples/lfric/eg15/matvec_opt.py @@ -75,7 +75,7 @@ def trans(psy): - '''PSyclone transformation script for the Dynamo0.3 API to optimise + '''PSyclone transformation script for the LFRic API to optimise the matvec kernel for many-core CPUs. For the moment simply find the first matvec kernel in the example, transform the matmul intrinsic to equivalant inline code and then print out its PSyIR @@ -90,7 +90,10 @@ def trans(psy): schedule = invoke.schedule for kernel in schedule.coded_kernels(): if kernel.name.lower() == "matrix_vector_kernel_code": - kernel_schedule = kernel.get_kernel_schedule() + _, kernel_schedules = kernel.get_kernel_schedule() + # For simplicity, ASSUME that the kernel is not polymorphic and + # thus only has one schedule. + kernel_schedule = kernel_schedules[0] # Replace matmul with inline code for icall in kernel_schedule.walk(IntrinsicCall): if icall.intrinsic is IntrinsicCall.Intrinsic.MATMUL: diff --git a/examples/lfric/scripts/kernel_print.py b/examples/lfric/scripts/kernel_print.py index 1cac60f5e2..083f109d49 100644 --- a/examples/lfric/scripts/kernel_print.py +++ b/examples/lfric/scripts/kernel_print.py @@ -54,11 +54,12 @@ def trans(psy): # Loop over all of the Kernels in this Schedule. for kernel in schedule.coded_kernels(): try: - kernel_schedule = kernel.get_kernel_schedule() - if kernel_schedule not in already_printed: - kern = fortran_writer(kernel_schedule) - print(kern) - already_printed.append(kernel_schedule) + _, kernel_schedules = kernel.get_kernel_schedule() + for ksched in kernel_schedules: + if ksched not in already_printed: + kern = fortran_writer(ksched) + print(kern) + already_printed.append(ksched) except Exception as err: # pylint: disable=broad-except print(f"Code of '{kernel.name}' in '{invoke.name}' " f"cannot be printed because:\n{err}") diff --git a/examples/xdsl/backend/xdsl.py b/examples/xdsl/backend/xdsl.py index cf8af2f069..01d50ef5f6 100644 --- a/examples/xdsl/backend/xdsl.py +++ b/examples/xdsl/backend/xdsl.py @@ -439,7 +439,9 @@ def checkIfStringIsType(self, string, typ): def nemokern_node(self, node): exec_statements = [] - schedule = node.get_kernel_schedule() + _, schedules = node.get_kernel_schedule() + # IGNORE polymorphic routines. + schedule = schedules[0] for child in schedule.children: exec_statements.append(self._visit(child)) return exec_statements From 71a2630c60deb6304ea82091a59b6e095464f64d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 3 Oct 2024 14:32:56 +0100 Subject: [PATCH 014/100] #2716 revert some unnecessary changes --- src/psyclone/domain/lfric/kernel_interface.py | 8 ++++---- src/psyclone/domain/lfric/lfric_types.py | 7 ------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel_interface.py b/src/psyclone/domain/lfric/kernel_interface.py index c0f841fae4..b64fd0a91f 100644 --- a/src/psyclone/domain/lfric/kernel_interface.py +++ b/src/psyclone/domain/lfric/kernel_interface.py @@ -408,15 +408,15 @@ def scalar(self, scalar_arg, var_accesses=None): :param scalar_arg: the scalar to add. :type scalar_arg: :py:class:`psyclone.dynamo0p3.DynKernelArgument` - :param var_accesses: an unused optional argument that stores + :param var_accesses: an unused optional argument that stores \ information about variable accesses. - :type var_accesses: :py:class:`psyclone.core.VariablesAccessInfo` + :type var_accesses: :\ + py:class:`psyclone.core.VariablesAccessInfo` - :raises NotImplementedError: if the datatype of the scalar is + :raises NotImplementedError: if the datatype of the scalar is \ not supported. ''' - # TODO this needs to distinguish between precisions. mapping = { "integer": LFRicTypes("LFRicIntegerScalarDataSymbol"), "real": LFRicTypes("LFRicRealScalarDataSymbol"), diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index c3bb104eca..305bda150f 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -183,18 +183,11 @@ def _create_generic_scalars(): ''' GenericScalar = namedtuple('GenericScalar', ["name", "intrinsic", "precision"]) - generic_scalar_datatypes = [ GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER, LFRicTypes("I_DEF")), GenericScalar("LFRicRealScalar", ScalarType.Intrinsic.REAL, LFRicTypes("R_DEF")), - GenericScalar("LFRicRdefScalar", ScalarType.Intrinsic.REAL, - LFRicTypes("R_DEF")), - GenericScalar("LFRicRsolverScalar", ScalarType.Intrinsic.REAL, - LFRicTypes("R_SOLVER")), - GenericScalar("LFRicRtranScalar", ScalarType.Intrinsic.REAL, - LFRicTypes("R_TRAN")), GenericScalar("LFRicLogicalScalar", ScalarType.Intrinsic.BOOLEAN, LFRicTypes("L_DEF"))] From ba58c82c739c70b80eb61433166690cdfb2bc0c5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 4 Oct 2024 17:03:52 +0100 Subject: [PATCH 015/100] #2716 tidying and improving comments/docstrings --- .../kernel_module_inline_trans.py | 126 ++++++++++-------- .../kernel_module_inline_trans_test.py | 69 ++++++---- 2 files changed, 111 insertions(+), 84 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 0886a8bc0e..9f89e266de 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -43,8 +43,6 @@ ''' - -from psyclone.errors import InternalError from psyclone.psyGen import Transformation, CodedKern from psyclone.psyir.transformations import TransformationError from psyclone.psyir.symbols import ( @@ -225,6 +223,9 @@ def _prepare_code_to_inline(routines_to_inline): '''Prepare the PSyIR tree to inline by bringing in to the subroutine all referenced symbols so that the implementation is self contained. + The provided routines are copied so that the original PSyIR is left + unmodified. + TODO #2271 will improve this method and could potentially avoid the need for debug_string() within get_kernel_schedule() in dynamo0p3.py. Sergi suggests that we may be missing the @@ -232,14 +233,26 @@ def _prepare_code_to_inline(routines_to_inline): might solve the problem. I'm not so sure and explain why in get_kernel_schedule() but still referencing this issue. - :param code_to_inline: the subroutine to module-inline. - :type code_to_inline: :py:class:`psyclone.psyir.node.Routine` + :param code_to_inline: the routine(s) to module-inline. + :type code_to_inline: list[:py:class:`psyclone.psyir.node.Routine`] + + :returns: the updated routine(s) to module-inline. + :rtype: list[:py:class:`psyclone.psyir.node.Routine`] ''' # pylint: disable=too-many-branches - source_container = routines_to_inline[0].ancestor(Container) - - for code_to_inline in routines_to_inline: + orig_container = routines_to_inline[0].ancestor(Container) + # Since we will be detaching Routines, we work with a copy of + # the Container that encapsulates them. + source_container = orig_container.copy() + new_routines = dict() + for routine in source_container.walk(Routine): + new_routines[routine.name] = routine + + copied_routines = [] + for orig_routine in routines_to_inline: + code_to_inline = new_routines[orig_routine.name] + copied_routines.append(code_to_inline) # First make a set with all symbols used inside the subroutine all_symbols = set() for scope in code_to_inline.walk(ScopingNode): @@ -299,6 +312,7 @@ def _prepare_code_to_inline(routines_to_inline): symbol.interface.container_symbol = \ code_to_inline.symbol_table.lookup( module_symbol.name) + return copied_routines @staticmethod def _get_psyir_to_inline(node): @@ -331,12 +345,42 @@ def _get_psyir_to_inline(node): else: # We have a generic routine call. routines = node.get_callees() - if not isinstance(routines, list): - routines = [routines] caller_name = node.routine.name.lower() - interface_sym = None # TODO + interface_sym = None + if len(routines) > 1: + interface_sym = routines[0].symbol_table.lookup(caller_name) + return (caller_name, routines, interface_sym) + @staticmethod + def _rm_imported_symbol(name, table): + ''' + If the named symbol is in the supplied table (or an ancestor) *and* is + an import then it is removed. If the Container from which it was being + imported no longer has any imports associated with it then the + ContainerSymbol is also removed. + + :param str name: the name of the symbol to remove. + :param table: the symbol table from which to search for the symbol. + :type table: :py:class:`psyclone.psyir.symbols.SymbolTable` + + ''' + symbol = table.lookup(name) + if not symbol.is_import: + return + + # The RoutineSymbol is in the table (or an outer scope) and is + # imported. We therefore remove it and potentially the ContainerSymbol + # from which it is imported. + csym = symbol.interface.container_symbol + + actual_table = (symbol.find_symbol_table(table.node) if + symbol.name not in table else table) + remove_csym = actual_table.symbols_imported_from(csym) == [symbol] + actual_table._symbols.pop(symbol.name) + if remove_csym: + actual_table.remove(csym) + def apply(self, node, options=None): ''' Bring the kernel subroutine into this Container. @@ -368,26 +412,18 @@ def apply(self, node, options=None): caller_name, codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) - # This will be the name of the interface if the call is to a - # polymorphic routine. - if isinstance(node, Call): - callee_name = node.routine.name - else: - callee_name = node.name - - try: - existing_symbol = node.scope.symbol_table.lookup(callee_name) - except KeyError: - existing_symbol = None - - self._prepare_code_to_inline(codes_to_inline) + updated_routines = self._prepare_code_to_inline(codes_to_inline) container = node.ancestor(Container) + local_table = node.scope.symbol_table - if interface_sym: - container.symbol_table.add(interface_sym) + for code_to_inline in updated_routines: + try: + existing_symbol = node.scope.symbol_table.lookup( + code_to_inline.name) + except KeyError: + existing_symbol = None - for code_to_inline in codes_to_inline: if not existing_symbol: # If it doesn't exist already, module-inline the subroutine by # inserting the relevant code into the tree. @@ -441,36 +477,14 @@ def apply(self, node, options=None): f"with the same name already exists and " f"versioning of module-inlined subroutines" f" is not implemented yet.") - # Finally, ensure that the RoutineSymbol for the inlined - # routine is in the correct symbol table. - routine_symbol = existing_symbol - table = routine_symbol.find_symbol_table(node) - if table.node is not container: - # Set the visibility of the symbol to always be private. - sym = container.symbol_table.lookup(routine_symbol.name) - sym.visibility = Symbol.Visibility.PRIVATE - # Force removal of the routine_symbol if it's also present - # in the Routine's symbol table. - table.lookup(routine_symbol.name) - norm_name = table._normalize(routine_symbol.name) - table._symbols.pop(norm_name) - - # We only modify the kernel call name after the equality check to - # ensure the apply will succeed and we don't leave with an inconsistent - # tree. - if callee_name != caller_name: - if isinstance(node, CodedKern): - node.name = callee_name - else: - # TODO #924 - we can't currently resolve a subroutine if its - # name doesn't match that in the caller (as will be the case - # if it's being called via an Interface in Fortran). This - # should have been picked-up in validate() so this is just a - # safety check. - raise InternalError( - f"Cannot module-inline call to '{caller_name}' because its" - f" name does not match that of the callee: " - f"'{callee_name}'. TODO #924.") + + if interface_sym: + local_sym = local_table.lookup(interface_sym.name, otherwise=None) + if local_sym: + self._rm_imported_symbol(interface_sym.name, + local_table) + container.symbol_table.add(interface_sym) + interface_sym.replace_symbols_using(container.symbol_table) # Set the module-inline flag to avoid generating the kernel imports # TODO #1823. If the kernel imports were generated at PSy-layer diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 682242257f..f6fe8be1ec 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -523,8 +523,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1" in result assert "use external_mod2" in result assert "not_needed" not in result @@ -545,8 +545,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : a" in result assert "use external_mod2, only : b=>var1, c=>var2" in result assert "not_needed" not in result @@ -569,8 +569,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : a, d" in result assert "use external_mod2, only : b=>var1, c=>var2, var1" in result assert "not_needed" not in result @@ -593,8 +593,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : r_def" in result assert "use external_mod2, only : my_user_type" in result assert "use not_needed" not in result @@ -614,8 +614,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : r_def" in result assert "use not_needed" not in result @@ -634,8 +634,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : my_sub" in result # Also, if they are inside CodeBlocks @@ -651,8 +651,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : a, b" in result # Check that symbol shadowing is respected (in this example @@ -671,8 +671,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod1, only : c" in result # Another shadowing example where the local module should be @@ -689,8 +689,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod\n" in result assert "use external_mod, only : r_def" not in result @@ -705,8 +705,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod, only : a" in result @@ -758,9 +758,10 @@ def test_module_inline_with_interfaces(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) -@pytest.mark.parametrize("mod_use, sub_use", - [("use my_mod, only: my_sub, my_other_sub", ""), - ("", "use my_mod, only: my_sub, my_other_sub")]) +@pytest.mark.parametrize( + "mod_use, sub_use", + [("use my_mod, only: my_sub, my_other_sub, my_interface", ""), + ("", "use my_mod, only: my_sub, my_other_sub, my_interface")]) @pytest.mark.usefixtures("clear_module_manager_instance") def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, monkeypatch, mod_use, sub_use): @@ -776,9 +777,11 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, contains subroutine a_sub() {sub_use} - real, dimension(10) :: a + real*8, dimension(10) :: a + real*4, dimension(10) :: b call my_sub(a) - call my_other_sub(a) + call my_other_sub(b) + call my_interface(b) end subroutine a_sub end module a_mod ''' @@ -791,13 +794,16 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, "w", encoding="utf-8") as mfile: mfile.write('''\ module my_mod + interface my_interface + module procedure :: my_sub, my_other_sub + end interface my_interface contains subroutine my_sub(arg) - real, dimension(10), intent(inout) :: arg + real*8, dimension(10), intent(inout) :: arg arg(1:10) = 1.0 end subroutine my_sub subroutine my_other_sub(arg) - real, dimension(10), intent(inout) :: arg + real*4, dimension(10), intent(inout) :: arg arg(1:10) = 1.0 end subroutine my_other_sub end module my_mod @@ -817,9 +823,16 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, output = fortran_writer(psyir) assert "subroutine a_sub" in output assert "subroutine my_sub" in output - assert "use my_mod, only : my_other_sub\n" in output + assert "use my_mod, only : my_interface, my_other_sub\n" in output # We can't test the compilation of this code because of the 'use my_mod.' + intrans.apply(calls[2]) + routines = container.walk(Routine) + output = fortran_writer(psyir) + assert "use my_mod" not in output + assert "subroutine my_other_sub" in output + assert "interface my_interface" in output + def test_mod_inline_no_container(fortran_reader, fortran_writer, tmpdir, monkeypatch): From 2ac83b59c7d4dbd8cbe09e57abcba762ad1e669f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 7 Oct 2024 11:19:53 +0100 Subject: [PATCH 016/100] #2716 add tests for KernelModuleInlineTrans --- .../kernel_module_inline_trans.py | 3 +++ .../kernel_module_inline_trans_test.py | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 9f89e266de..337fee22bc 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -377,6 +377,9 @@ def _rm_imported_symbol(name, table): actual_table = (symbol.find_symbol_table(table.node) if symbol.name not in table else table) remove_csym = actual_table.symbols_imported_from(csym) == [symbol] + # We have to force the removal as there will be calls that reference + # this Symbol. + # pylint:disable-next=protected-access actual_table._symbols.pop(symbol.name) if remove_csym: actual_table.remove(csym) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index f6fe8be1ec..ca33124123 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -49,7 +49,7 @@ Container, Routine, CodeBlock, Call, IntrinsicCall) from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, ImportInterface, RoutineSymbol, REAL_TYPE, - Symbol, SymbolError) + Symbol, SymbolError, SymbolTable) from psyclone.psyir.transformations import TransformationError from psyclone.tests.gocean_build import GOceanBuild from psyclone.tests.lfric_build import LFRicBuild @@ -374,6 +374,28 @@ def test_validate_fail_to_get_psyir(fortran_reader): in str(err.value)) +def test_rm_imported_symbol(): + ''' + Tests for the _rm_imported_symbol() utility method. + + ''' + table = SymbolTable() + csym = ContainerSymbol("ankh") + table.add(csym) + moist_sym = DataSymbol("moist", REAL_TYPE, + interface=ImportInterface(csym)) + table.add(moist_sym) + local_sym = DataSymbol("local", REAL_TYPE) + table.add(local_sym) + KernelModuleInlineTrans._rm_imported_symbol("moist", table) + assert "moist" not in table + # Container has been removed too. + assert "ankh" not in table + # If the symbol is not imported then it is left unchanged. + KernelModuleInlineTrans._rm_imported_symbol("local", table) + assert "local" in table + + def test_module_inline_apply_transformation(tmpdir, fortran_writer): ''' Test that we can succesfully inline a basic kernel subroutine routine into the PSy layer module using a transformation ''' From e5d569983ab34907e5c50271445a9c9603165e72 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 7 Oct 2024 11:34:07 +0100 Subject: [PATCH 017/100] #2716 fix coverage of gocean_move_iteration_boundaries_inside --- ...teration_boundaries_inside_kernel_trans.py | 3 +- ...ion_boundaries_inside_kernel_trans_test.py | 38 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index 0424f69265..8da23ad036 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -113,7 +113,8 @@ def validate(self, node, options=None): if len(kschedules) > 1: raise TransformationError( f"Error in {self.name} transformation. Polymorphic kernels " - f"are not supported but kernel '{node.name}' corresponds to ") + f"are not supported but kernel '{node.name}' has " + f"implementations: {[sched.name for sched in kschedules]}") def apply(self, node, options=None): '''Apply this transformation to the supplied node. diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index d54431aa4a..0e167c861e 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -40,13 +40,13 @@ import pytest from psyclone.tests.utilities import get_invoke -from psyclone.domain.gocean.transformations import \ - GOMoveIterationBoundariesInsideKernelTrans -from psyclone.psyir.nodes import Assignment, Container, IfBlock, Return +from psyclone.domain.gocean.transformations import ( + GOMoveIterationBoundariesInsideKernelTrans) +from psyclone.psyir.nodes import ( + Assignment, Container, IfBlock, Return, Routine) from psyclone.psyir.symbols import ArgumentInterface -from psyclone.gocean1p0 import GOLoop +from psyclone.gocean1p0 import GOKern, GOLoop from psyclone.psyir.transformations import TransformationError -from psyclone.psyir.backend.fortran import FortranWriter API = "gocean" @@ -59,14 +59,30 @@ def test_description(): "Move kernel iteration boundaries inside the kernel code." -def test_validation(): - ''' Check that the transformation can only be applied to routine nodes ''' +def test_validation(monkeypatch): + ''' Check that the transformation can only be applied to routine nodes and + that polymorphic kernels are rejected. + + ''' trans = GOMoveIterationBoundariesInsideKernelTrans() with pytest.raises(TransformationError) as info: - trans.apply(None) + trans.validate(None) assert ("Error in GOMoveIterationBoundariesInsideKernelTrans " "transformation. This transformation can only be applied to " "'GOKern' nodes, but found 'NoneType'." in str(info.value)) + # Test that a polymorphic kernel is rejected. GOcean doesn't actually + # support these so we use monkeypatch. + psy, _ = get_invoke("single_invoke.f90", API, idx=0, dist_mem=False) + sched = psy.invokes.invoke_list[0].schedule + kern = sched.walk(GOKern)[0] + sched1 = Routine.create("kern_32") + sched2 = Routine.create("kern_64") + monkeypatch.setattr(kern, "get_kernel_schedule", + lambda: (None, [sched1, sched2])) + with pytest.raises(TransformationError) as err: + trans.validate(kern) + assert ("but kernel 'compute_cu_code' has implementations: ['kern_32', " + "'kern_64']" in str(err.value)) def test_go_move_iteration_boundaries_inside_kernel_trans(): @@ -154,7 +170,8 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): ArgumentInterface) -def test_go_move_iteration_boundaries_inside_kernel_two_kernels_apply_twice(): +def test_go_move_iteration_boundaries_inside_kernel_two_kernels_apply_twice( + fortran_writer): ''' Tests that the GOMoveIterationBoundariesInsideKernelTrans transformation for the GOcean API produces the expected code when the invoke has two kernels and the transformation is applied twice. @@ -215,5 +232,4 @@ def test_go_move_iteration_boundaries_inside_kernel_two_kernels_apply_twice(): end subroutine invoke_0 ''' - writer = FortranWriter() - assert writer(sched) == expected + assert fortran_writer(sched) == expected From f026e213c3885104805532633205062071fba790 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 7 Oct 2024 12:15:52 +0100 Subject: [PATCH 018/100] #2716 rm need for polymorphic checks for GOcean Kernels --- ...teration_boundaries_inside_kernel_trans.py | 9 +------ .../transformations/gocean_opencl_trans.py | 25 ++++++++++--------- src/psyclone/gocean1p0.py | 9 ++++++- ...ion_boundaries_inside_kernel_trans_test.py | 22 +++------------- 4 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index 8da23ad036..e15327e58d 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -109,13 +109,6 @@ def validate(self, node, options=None): f"can only be applied to 'GOKern' nodes, but found " f"'{type(node).__name__}'.") - _, kschedules = node.get_kernel_schedule() - if len(kschedules) > 1: - raise TransformationError( - f"Error in {self.name} transformation. Polymorphic kernels " - f"are not supported but kernel '{node.name}' has " - f"implementations: {[sched.name for sched in kschedules]}") - def apply(self, node, options=None): '''Apply this transformation to the supplied node. @@ -188,7 +181,7 @@ def apply(self, node, options=None): # Update Kernel _, kschedules = node.get_kernel_schedule() - # validate() has checked that this kernel is not polymorphic. + # GOcean Kernels must have a single implementation. kschedule = kschedules[0] kernel_st = kschedule.symbol_table iteration_indices = kernel_st.iteration_indices diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index 4d84aeeb34..eab964cdda 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -147,17 +147,19 @@ def validate(self, node, options=None): # Validate options map valid_options = ['end_barrier', 'enable_profiling', 'out_of_order'] - for key, value in options.items(): - if key in valid_options: - # All current options should contain boolean values - if not isinstance(value, bool): + if options: + for key, value in options.items(): + if key in valid_options: + # All current options should contain boolean values + if not isinstance(value, bool): + raise TransformationError( + f"InvokeSchedule OpenCL option '{key}' should be " + f"a boolean.") + else: raise TransformationError( - f"InvokeSchedule OpenCL option '{key}' should be a " - f"boolean.") - else: - raise TransformationError( - f"InvokeSchedule does not support the OpenCL option " - f"'{key}'. The supported options are: {valid_options}.") + f"InvokeSchedule does not support the OpenCL option " + f"'{key}'. The supported options are: " + f"{valid_options}.") # Validate that the options are valid with previously generated OpenCL if self._transformed_invokes > 0: @@ -193,8 +195,7 @@ def validate(self, node, options=None): for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) _, kschedules = kern.get_kernel_schedule() - if len(kschedules) > 1: - raise TransformationError("TODO") + # GOcean Kernels must have a single implementation. ksched = kschedules[0] global_variables = ksched.symbol_table.imported_symbols if global_variables: diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index 4100c251ae..4a456e5f37 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1235,8 +1235,15 @@ def index_offset(self): def get_kernel_schedule(self): ''' + Obtains and returns the PSyIR Schedule representing the kernel code. + + For consistency with LFRic kernels (which may be polymorphic), this + method actually returns a tuple with the second element containing a + list comprising just one Schedule. + :returns: a schedule representing the GOcean kernel code. - :rtype: :py:class:`psyclone.gocean1p0.GOKernelSchedule` + :rtype: tuple[NoneType, + list[:py:class:`psyclone.gocean1p0.GOKernelSchedule`]] :raises GenerationError: if there is a problem raising the language- level PSyIR of this kernel to GOcean PSyIR. diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index 0e167c861e..2d72b8951f 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -43,9 +43,9 @@ from psyclone.domain.gocean.transformations import ( GOMoveIterationBoundariesInsideKernelTrans) from psyclone.psyir.nodes import ( - Assignment, Container, IfBlock, Return, Routine) + Assignment, Container, IfBlock, Return) from psyclone.psyir.symbols import ArgumentInterface -from psyclone.gocean1p0 import GOKern, GOLoop +from psyclone.gocean1p0 import GOLoop from psyclone.psyir.transformations import TransformationError API = "gocean" @@ -60,29 +60,13 @@ def test_description(): def test_validation(monkeypatch): - ''' Check that the transformation can only be applied to routine nodes and - that polymorphic kernels are rejected. - - ''' + '''Check that the transformation can only be applied to routine nodes.''' trans = GOMoveIterationBoundariesInsideKernelTrans() with pytest.raises(TransformationError) as info: trans.validate(None) assert ("Error in GOMoveIterationBoundariesInsideKernelTrans " "transformation. This transformation can only be applied to " "'GOKern' nodes, but found 'NoneType'." in str(info.value)) - # Test that a polymorphic kernel is rejected. GOcean doesn't actually - # support these so we use monkeypatch. - psy, _ = get_invoke("single_invoke.f90", API, idx=0, dist_mem=False) - sched = psy.invokes.invoke_list[0].schedule - kern = sched.walk(GOKern)[0] - sched1 = Routine.create("kern_32") - sched2 = Routine.create("kern_64") - monkeypatch.setattr(kern, "get_kernel_schedule", - lambda: (None, [sched1, sched2])) - with pytest.raises(TransformationError) as err: - trans.validate(kern) - assert ("but kernel 'compute_cu_code' has implementations: ['kern_32', " - "'kern_64']" in str(err.value)) def test_go_move_iteration_boundaries_inside_kernel_trans(): From 69256974e1c3814195b77240f54c58eda231f202 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 7 Oct 2024 13:28:17 +0100 Subject: [PATCH 019/100] #2716 improve coverage --- src/psyclone/psyGen.py | 12 ++++----- .../transformations/globalstoargs_test.py | 25 +++++++++++++++++-- src/psyclone/tests/psyGen_test.py | 17 +++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index b886cb2910..921fe6e907 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1379,13 +1379,13 @@ def get_kernel_schedule(self): kernel code. :rtype: tuple[:py:class:`psyclone.psyir.symbols.Symbol`, list[:py:class:`psyclone.psyir.nodes.KernelSchedule`]] + + :raises NotImplementedError: must be overridden in sub-class. + ''' - from psyclone.psyir.frontend.fparser2 import Fparser2Reader - if self._kern_schedule is None: - astp = Fparser2Reader() - self._kern_schedule = [astp.generate_schedule(self.name, self.ast)] - # TODO: Validate kernel with metadata (issue #288). - return None, self._kern_schedule + raise NotImplementedError( + f"get_kernel_schedule() must be overridden in class " + f"{self.__class__}") @property def opencl_options(self): diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 560bc3a498..278eaac1ab 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -34,15 +34,16 @@ # Authors: A. R. Porter and S. Siso, STFC Daresbury Lab # Modified by R. W. Ford, STFC Daresbury Lab -''' Tests the KernelImportsToArguments Transformation for the GOcean -1.0 API.''' +''' Tests the KernelImportsToArguments Transformation for the GOcean API.''' import os import pytest +from psyclone.gocean1p0 import GOInvokeSchedule from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory, InvokeSchedule from psyclone.psyir.symbols import DataSymbol, REAL_TYPE, INTEGER_TYPE, \ CHARACTER_TYPE, Symbol +from psyclone.tests.utilities import get_invoke from psyclone.transformations import KernelImportsToArguments, \ TransformationError @@ -106,6 +107,26 @@ def test_kernelimportstoargumentstrans_no_wildcard_import(): "container 'model_mod'" in str(err.value)) +def test_kernelimportstoargumentstrans_no_polymorphic(monkeypatch): + ''' + Check that the transformation rejects polymorphic kernels. + + ''' + trans = KernelImportsToArguments() + _, invoke = get_invoke("26.8_mixed_precision_args.f90", api="lfric", idx=0) + kernel = invoke.schedule.coded_kernels()[0] + invsched = kernel.ancestor(InvokeSchedule) + # Currently this transformation will only work for the GOcean API so + # monkeypatch the class of the parent InvokeSchedule. + monkeypatch.setattr(invsched, "__class__", GOInvokeSchedule) + with pytest.raises(TransformationError) as err: + trans.validate(kernel) + assert ("KernelImportsToArguments transformation does not support " + "polymorphic kernels but found the following implementations for " + "kernel 'mixed_code': ['mixed_code_32', 'mixed_code_64']" + in str(err.value)) + + @pytest.mark.xfail(reason="Transformation does not set modified property " "of kernel - #663") @pytest.mark.usefixtures("kernel_outputdir") diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index a4b84ba0e8..09fd60d0d9 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -703,6 +703,23 @@ def test_kern_children_validation(): "is a LeafNode and doesn't accept children.") in str(excinfo.value) +def test_codedkern_get_kernel_schedule(monkeypatch): + ''' + Check that CodedKern.get_kernel_schedule() raises a NotImplementedError + (as it must be implemented by sub-classes). + + ''' + ast = fpapi.parse(FAKE_KERNEL_METADATA, ignore_comments=False) + metadata = LFRicKernMetadata(ast) + kern = LFRicKern() + kern.load_meta(metadata) + monkeypatch.setattr(kern, "__class__", CodedKern) + with pytest.raises(NotImplementedError) as err: + kern.get_kernel_schedule() + assert ("get_kernel_schedule() must be overridden in class " + in str(err.value)) + + def test_inlinedkern_children_validation(): '''Test that children added to InlinedKern are validated. An InlinedKern must have one child that is a Schedule (which is created by its From 0b6ce32b97bb72c1a6c890974023a9ce5c39bf13 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 09:02:18 +0100 Subject: [PATCH 020/100] #2716 improve _rm_imported_symbol and only attempt to add interface sym if not present [skip ci] --- .../transformations/kernel_module_inline_trans.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 337fee22bc..604b4cf2c4 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -365,8 +365,8 @@ def _rm_imported_symbol(name, table): :type table: :py:class:`psyclone.psyir.symbols.SymbolTable` ''' - symbol = table.lookup(name) - if not symbol.is_import: + symbol = table.lookup(name, otherwise=None) + if not symbol or not symbol.is_import: return # The RoutineSymbol is in the table (or an outer scope) and is @@ -482,12 +482,10 @@ def apply(self, node, options=None): f" is not implemented yet.") if interface_sym: - local_sym = local_table.lookup(interface_sym.name, otherwise=None) - if local_sym: - self._rm_imported_symbol(interface_sym.name, - local_table) - container.symbol_table.add(interface_sym) - interface_sym.replace_symbols_using(container.symbol_table) + self._rm_imported_symbol(interface_sym.name, local_table) + if interface_sym.name not in container.symbol_table: + container.symbol_table.add(interface_sym) + interface_sym.replace_symbols_using(container.symbol_table) # Set the module-inline flag to avoid generating the kernel imports # TODO #1823. If the kernel imports were generated at PSy-layer From a00f3670a3ee8599396505a8fca795333933d8fa Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 11:57:14 +0100 Subject: [PATCH 021/100] #2716 add InterfaceDeclGen to f2pygen --- src/psyclone/f2pygen.py | 52 ++++++++++++++++++++++++++++++ src/psyclone/tests/f2pygen_test.py | 35 ++++++++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/psyclone/f2pygen.py b/src/psyclone/f2pygen.py index 4db3407e44..f983e8f715 100644 --- a/src/psyclone/f2pygen.py +++ b/src/psyclone/f2pygen.py @@ -1275,6 +1275,58 @@ def _check_initial_values(self, _type, _values): "for derived-type declarations are not supported.") +class InterfaceDeclGen(BaseDeclGen): + ''' + Generates the declaration for a Fortran interface block of the form: + + interface my_wrapper + module procedure :: name1, name1, ... + end interface my_wrapper + + The declaration is added to the supplied parent node. + + :param parent: f2pygen node to which to add this declaration as a child. + :type parent: :py:class:`psyclone.f2pygen.ModuleGen` + :param str name: the name of the interface block. + :param entity_decls: names of procedures to declare within the block. + :type entity_decls: list[str] + + :raises TypeError: if the supplied parent is not a ModuleGen (since + interfaces must be declared within modules). + :raises ValueError: if a list of procedure names is not supplied. + + ''' + def __init__(self, parent, name, entity_decls): + if not isinstance(parent, ModuleGen): + raise TypeError(f"An InterfaceDeclGen must have a ModuleGen as " + f"parent but got '{type(parent).__name__}'") + if not isinstance(entity_decls, list) or not entity_decls: + raise ValueError( + f"The routine names to use within Interface '{name}' must be " + f"supplied as a list of str to 'entity_decls' but got " + f"{entity_decls}") + name_list = ", ".join(entity_decls) + reader = FortranStringReader(f"interface {name}\n" + f" module procedure :: {name_list}\n" + f"end interface {name}\n") + reader.set_format(FortranFormat(True, True)) # free form, strict + line1 = reader.next() + interf = fparser1.block_statements.Interface(parent.root, line1) + line2 = reader.next() + procedures = fparser1.block_statements.ModuleProcedure(parent.root, + line2) + interf.content.append(procedures) + line3 = reader.next() + end_interf = fparser1.block_statements.EndInterface(interf, line3) + self._decl = interf + interf.content.append(end_interf) + + def _check_initial_values(self): + ''' + Empty routine required to override that from abstract base class. + ''' + + class TypeCase(Case): ''' Generate a Fortran SELECT CASE statement ''' # TODO can this whole class be deleted? diff --git a/src/psyclone/tests/f2pygen_test.py b/src/psyclone/tests/f2pygen_test.py index fccefd5b24..78e48bf1b6 100644 --- a/src/psyclone/tests/f2pygen_test.py +++ b/src/psyclone/tests/f2pygen_test.py @@ -40,7 +40,8 @@ from psyclone.f2pygen import ( adduse, AssignGen, AllocateGen, BaseGen, CallGen, CharDeclGen, CommentGen, DeallocateGen, DeclGen, DirectiveGen, DoGen, IfThenGen, ImplicitNoneGen, - ModuleGen, PSyIRGen, SelectionGen, SubroutineGen, TypeDeclGen, UseGen) + InterfaceDeclGen, ModuleGen, PSyIRGen, SelectionGen, SubroutineGen, + TypeDeclGen, UseGen) from psyclone.errors import GenerationError, InternalError from psyclone.psyir.nodes import Node, Return from psyclone.tests.utilities import Compile, count_lines, line_number @@ -1342,7 +1343,6 @@ def test_declgen_multiple_use2(): sub.add(DeclGen(sub, datatype="integer", entity_decls=datanames)) gen = str(sub.root) - print(gen) expected = ( " INTEGER data1, data2\n" " REAL data1") @@ -1351,6 +1351,36 @@ def test_declgen_multiple_use2(): assert datanames == ["data1", "data2"] +def test_interfacedeclgen(): + '''Tests for InterfaceDeclGen.''' + module = ModuleGen(name="testmodule") + sub1 = SubroutineGen(module, name="sub32") + module.add(sub1) + sub2 = SubroutineGen(module, name="sub64") + module.add(sub2) + with pytest.raises(TypeError) as err: + InterfaceDeclGen(sub1, name="my_interface", entity_decls=[]) + assert ("An InterfaceDeclGen must have a ModuleGen as parent" + in str(err.value)) + with pytest.raises(ValueError) as err: + InterfaceDeclGen(module, name="my_interface", entity_decls=[]) + assert ("The routine names to use within Interface 'my_interface' must be" + in str(err.value)) + module.add(InterfaceDeclGen(module, name="my_interface", + entity_decls=["sub32", "sub64"])) + gen = str(module.root).lower() + expected = """\ + module testmodule + implicit none + interface my_interface + module procedure sub32, sub64 + end interface my_interface + contains""" + assert expected in gen + # Can't compile this without properly creating subroutines with arguments + # of different precisions. + + @pytest.mark.xfail(reason="No way to add body of DEFAULT clause") def test_selectiongen(): ''' Check that SelectionGen works as expected ''' @@ -1364,7 +1394,6 @@ def test_selectiongen(): # TODO how do we specify what happens in the default case? sgen.adddefault() gen = str(sub.root) - print(gen) expected = ("SELECT CASE ( my_var )\n" "CASE ( 1 )\n" " happy = .TRUE.\n" From 2b822019399957aa05308b7688e1f8127508a745 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 12:07:03 +0100 Subject: [PATCH 022/100] #2716 fixes for the transformation in LFRic --- .../kernel_module_inline_trans.py | 2 +- src/psyclone/dynamo0p3.py | 18 +++++++++++++----- src/psyclone/psyGen.py | 8 -------- .../kernel_module_inline_trans_test.py | 8 ++++++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 604b4cf2c4..c8a4f960ce 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -464,7 +464,7 @@ def apply(self, node, options=None): # validation that it's a Routine. Now check if they are # exactly the same. for routine in container.walk(Routine, stop_type=Routine): - if routine.name == caller_name: + if routine.name == existing_symbol.name: # This TransformationError happens here and not in # the validation because it needs the # symbols_to_bring_in applied to effectively diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py index a0fa3514c3..2a1aed25d0 100644 --- a/src/psyclone/dynamo0p3.py +++ b/src/psyclone/dynamo0p3.py @@ -60,7 +60,7 @@ from psyclone.domain.lfric.lfric_invoke_schedule import LFRicInvokeSchedule from psyclone.errors import GenerationError, InternalError, FieldNotFoundError from psyclone.f2pygen import (AllocateGen, AssignGen, CallGen, CommentGen, - DeallocateGen, DeclGen, DoGen, + DeallocateGen, DeclGen, DoGen, InterfaceDeclGen, ModuleGen, TypeDeclGen, UseGen, PSyIRGen) from psyclone.parse.kernel import getkerneldescriptors from psyclone.parse.utils import ParseError @@ -71,10 +71,10 @@ from psyclone.psyir.nodes import ( Reference, ACCEnterDataDirective, ScopingNode, ArrayOfStructuresReference, StructureReference, Literal, IfBlock, Call, BinaryOperation, IntrinsicCall) -from psyclone.psyir.symbols import (INTEGER_TYPE, DataSymbol, ScalarType, - UnresolvedType, DataTypeSymbol, - ContainerSymbol, ImportInterface, - ArrayType, UnsupportedFortranType) +from psyclone.psyir.symbols import (ArrayType, ContainerSymbol, DataSymbol, + DataTypeSymbol, GenericInterfaceSymbol, + INTEGER_TYPE, ScalarType, ImportInterface, + UnresolvedType, UnsupportedFortranType) # pylint: disable=too-many-lines @@ -473,6 +473,14 @@ def gen(self): if not isinstance(routine, InvokeSchedule): psy_module.add(PSyIRGen(psy_module, routine)) + # Similarly, we have to take care of any Interface symbols (which + # we may have if we've inlined a polymorphic Kernel). + for sym in self.container.symbol_table.symbols: + if isinstance(sym, GenericInterfaceSymbol): + names = [rt.symbol.name for rt in sym.routines] + psy_module.add( + InterfaceDeclGen(psy_module, sym.name, names)) + # Add all invoke-specific information self.invokes.gen_code(psy_module) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 921fe6e907..9457a5c7a1 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -710,14 +710,6 @@ def __init__(self, symbol, KernFactory, BuiltInFactory, alg_calls=None, else: self.addchild(KernFactory.create(call, parent=self)) - @property - def symbol_table(self): - ''' - :returns: Table containing symbol information for the schedule. - :rtype: :py:class:`psyclone.psyir.symbols.SymbolTable` - ''' - return self._symbol_table - @property def invoke(self): return self._invoke diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index ca33124123..0258da26b1 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -766,13 +766,17 @@ def test_module_inline_with_interfaces(tmpdir): ''' psy, invoke = get_invoke("26.8_mixed_precision_args.f90", "lfric", name="invoke_0", dist_mem=False) - kern_call = invoke.schedule.walk(CodedKern)[0] + kern_calls = invoke.schedule.walk(CodedKern) inline_trans = KernelModuleInlineTrans() - inline_trans.apply(kern_call) + inline_trans.apply(kern_calls[0]) + # Check that module-inlining the second kernel call (which is to the + # same interface) doesn't break anything. + inline_trans.apply(kern_calls[1]) gen = str(psy.gen).lower() # Both the caller and the callee are in the file and use the interface # name. assert "call mixed_code(" in gen + assert "interface mixed_code" in gen assert "subroutine mixed_code_64(" in gen assert "subroutine mixed_code_32(" in gen From 0fd946484f9ecbdfecc415e54baced1e393a2b1b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 15:08:36 +0100 Subject: [PATCH 023/100] #2716 fix tests broken by merge --- .../transformations/kernel_transformation_test.py | 3 ++- src/psyclone/transformations.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 14a7b19a01..a6b0bebe44 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -464,7 +464,8 @@ def test_kernel_gpu_annotation_trans(rtrans, expected_directive, rtrans.apply(kern) # Check that the directive has been added to the kernel code - code = fortran_writer(kern.get_kernel_schedule()) + _, kschedules = kern.get_kernel_schedule() + code = fortran_writer(kschedules[0]) assert expected_directive in code diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 9904a36fad..b3913d11ed 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -546,15 +546,15 @@ def apply(self, node, options=None): node.modified = True # Get the schedule representing the kernel subroutine - routine = node.get_kernel_schedule() + _, routines = node.get_kernel_schedule() else: - routine = node - - for child in routine.children: - if isinstance(child, OMPDeclareTargetDirective): - return # The routine is already marked with OMPDeclareTarget + routines = [node] - routine.children.insert(0, OMPDeclareTargetDirective()) + for routine in routines: + for child in routine.children: + if isinstance(child, OMPDeclareTargetDirective): + break # routine is already marked with OMPDeclareTarget + routine.children.insert(0, OMPDeclareTargetDirective()) def validate(self, node, options=None): ''' Check that an OMPDeclareTargetDirective can be inserted. From 2e76d3a2e7546d69a3ad9be289c33033ad257e33 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 15:54:25 +0100 Subject: [PATCH 024/100] #2716 update opt script in repo and fix OMPDeclareTargetTrans --- examples/lfric/scripts/gpu_offloading.py | 11 ++++++++++- src/psyclone/transformations.py | 7 +++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 2167119fa1..92617eb439 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -43,6 +43,7 @@ ''' import os import sys +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import LFRicConstants from psyclone.psyir.nodes import Directive, Loop from psyclone.psyir.transformations import ( @@ -73,6 +74,7 @@ def trans(psy): otrans = Dynamo0p3OMPLoopTrans() const = LFRicConstants() cpu_parallel = OMPParallelTrans() + intrans = KernelModuleInlineTrans() if OFFLOAD_DIRECTIVES == "omp": # Use OpenMP offloading @@ -119,7 +121,8 @@ def trans(psy): else: offload = True - # Keep a record of any kernels we fail to offload + # Keep a record of any kernels we fail to offload. + failed_inline = set() failed_to_offload = set() # Colour loops over cells unless they are on discontinuous spaces @@ -136,6 +139,12 @@ def trans(psy): if loop.iteration_space == "cell_column": if offload: for kern in loop.kernels(): + try: + intrans.apply(kern) + except TransformationError as err: + failed_inline.add(kern.name.lower()) + print(f"Failed to module-inline kernel " + f"'{kern.name}' due to:\n{err.value}") try: gpu_annotation_trans.apply(kern) except TransformationError as err: diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index b3913d11ed..6552d5fba7 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -551,10 +551,9 @@ def apply(self, node, options=None): routines = [node] for routine in routines: - for child in routine.children: - if isinstance(child, OMPDeclareTargetDirective): - break # routine is already marked with OMPDeclareTarget - routine.children.insert(0, OMPDeclareTargetDirective()) + if not any(isinstance(child, OMPDeclareTargetDirective) for + child in routine.children): + routine.children.insert(0, OMPDeclareTargetDirective()) def validate(self, node, options=None): ''' Check that an OMPDeclareTargetDirective can be inserted. From 721ff5dae2becec0faa99fb631f3326af151f782 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 15:54:48 +0100 Subject: [PATCH 025/100] #2716 mark MATMUL as available on GPU --- src/psyclone/psyir/nodes/intrinsic_call.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 22c68d966b..04244b6bd6 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -797,9 +797,10 @@ def is_available_on_device(self): IntrinsicCall.Intrinsic.SIGN, IntrinsicCall.Intrinsic.SIN, IntrinsicCall.Intrinsic.SINH, IntrinsicCall.Intrinsic.SQRT, IntrinsicCall.Intrinsic.TAN, IntrinsicCall.Intrinsic.TANH, - # The one below are not documented on nvidia compiler + # The ones below are not documented in the nvidia compiler docs IntrinsicCall.Intrinsic.PRODUCT, IntrinsicCall.Intrinsic.SIZE, IntrinsicCall.Intrinsic.SUM, IntrinsicCall.Intrinsic.LBOUND, + IntrinsicCall.Intrinsic.MATMUL, IntrinsicCall.Intrinsic.UBOUND) @classmethod From 8319e951848640ad4fcdefa6ac9d0ab5993889c0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 8 Oct 2024 16:57:59 +0100 Subject: [PATCH 026/100] #2716 fix test for matmul on gpu --- src/psyclone/tests/psyir/nodes/intrinsic_call_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py index 38e16131f7..037762fa27 100644 --- a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py +++ b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py @@ -120,7 +120,7 @@ def test_intrinsiccall_is_inquiry(): (IntrinsicCall.Intrinsic.MAX, True), (IntrinsicCall.Intrinsic.MAXVAL, False), (IntrinsicCall.Intrinsic.ALLOCATE, False), - (IntrinsicCall.Intrinsic.MATMUL, False), + (IntrinsicCall.Intrinsic.MATMUL, True), (IntrinsicCall.Intrinsic.ACOS, True), (IntrinsicCall.Intrinsic.AINT, True), (IntrinsicCall.Intrinsic.ANINT, True), From 33376ff30721abe37f26a05adef57e46b8e2e3ec Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 9 Oct 2024 14:48:36 +0100 Subject: [PATCH 027/100] #2716 ensure Kern points to inlined PSyIR after transformation [skip ci] --- .../kernel_module_inline_trans.py | 22 +++- src/psyclone/domain/lfric/lfric_kern.py | 106 +++++++++--------- src/psyclone/psyir/nodes/call.py | 6 +- .../kernel_module_inline_trans_test.py | 3 + 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index c8a4f960ce..5ed7ed7aee 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -385,10 +385,11 @@ def _rm_imported_symbol(name, table): actual_table.remove(csym) def apply(self, node, options=None): - ''' Bring the kernel subroutine into this Container. + ''' Bring the kernel/subroutine into this Container. - :param node: the kernel to module-inline. - :type node: :py:class:`psyclone.psyGen.CodedKern` + :param node: the Kernel or Call to module-inline. + :type node: :py:class:`psyclone.psyGen.CodedKern` | + :py:class:`psyclone.psyir.nodes.Call` :param options: a dictionary with options for transformations. :type options: Optional[Dict[str, Any]] @@ -412,10 +413,21 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - caller_name, codes_to_inline, interface_sym = ( + # TODO - get rid of 'caller name' return value from + # _get_psyir_to_inline? + _, codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) updated_routines = self._prepare_code_to_inline(codes_to_inline) + # Update the Kernel to point to the updated PSyIR. + if isinstance(node, CodedKern): + # TODO - add setter for these properties to Kern? + # pylint: disable=protected-access + node._kern_schedule = updated_routines + if interface_sym: + node._interface_symbol = ( + updated_routines[0].scope.symbol_table.lookup( + interface_sym.name)) container = node.ancestor(Container) local_table = node.scope.symbol_table @@ -475,7 +487,7 @@ def apply(self, node, options=None): # is a detached copy.) if routine != code_to_inline: raise TransformationError( - f"Cannot inline subroutine '{caller_name}'" + f"Cannot inline subroutine '{node.name}'" f" because another, different, subroutine " f"with the same name already exists and " f"versioning of module-inlined subroutines" diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index ce67fdb499..da8c4365f9 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -53,7 +53,7 @@ from psyclone.parse.algorithm import Arg, KernelCall from psyclone.psyGen import InvokeSchedule, CodedKern, args_filter from psyclone.psyir.frontend.fparser2 import Fparser2Reader -from psyclone.psyir.nodes import (Loop, Literal, Reference, +from psyclone.psyir.nodes import (Container, Loop, Literal, Reference, KernelSchedule) from psyclone.psyir.symbols import DataSymbol, ScalarType, ArrayType @@ -653,49 +653,59 @@ class creates the PSyIR schedule on first invocation which is :raises GenerationError: if no subroutine matching this kernel can be found in the parse tree of the associated source code. + ''' if self._kern_schedule: return self._interface_symbol, self._kern_schedule - # Get the PSyIR Kernel Schedule(s) - routines = Fparser2Reader().get_routine_schedules(self.name, self.ast) - for routine in routines: - # If one of the symbols is not declared in a routine then - # this is only picked up when writing out the routine - # (raising a VisitorError), so we check here so that - # invalid code is not inlined. We use debug_string() to - # minimise the overhead. - - # TODO #2271 could potentially avoid the need for - # debug_string() within. Sergi suggests that we may be - # missing the traversal of the declaration init - # expressions and that might solve the problem. I'm not so - # sure as we are talking about unknown symbols that will - # only be resolved in the back-end (or not). If I am right - # then one option would be to use the FortranWriter, but - # that would be bigger overhead, or perhaps just the - # declarations part of FortranWriter if that is possible. - # Also see TODO issue #2336 which captures the specific - # problem in LFRic that this fixes. - routine.debug_string() - -# # The kernel name corresponds to an interface block. Find which -# # of the routines matches the precision of the arguments. -# for routine in routines: -# try: -# # The validity check for the kernel arguments should raise -# # an exception if the precisions don't match but currently -# # does not! -# self.validate_kernel_code_args(routine.symbol_table) -# sched = routine -# break -# except GenerationError: -# pass -# else: -# raise GenerationError( -# f"Failed to find a kernel implementation with an interface" -# f" that matches the invoke of '{self.name}'. (Tried " -# f"routines {[item.name for item in routines]}.)") + # Check for a local implementation of this kernel first. + container = self.ancestor(Container) + if container: + names = container.resolve_routine(self.name) + routines = [] + for name in names: + rt_psyir = container.find_routine_psyir(name, + allow_private=True) + routines.append(rt_psyir) + + # Otherwise, get the PSyIR Kernel Schedule(s) from the original + # parse tree. + if not routines: + routines = Fparser2Reader().get_routine_schedules(self.name, + self.ast) + new_schedules = [] + for routine in routines[:]: + # If one of the symbols is not declared in a routine then + # this is only picked up when writing out the routine + # (raising a VisitorError), so we check here so that + # invalid code is not inlined. We use debug_string() to + # minimise the overhead. + + # TODO #2271 could potentially avoid the need for + # debug_string() within. Sergi suggests that we may be + # missing the traversal of the declaration init + # expressions and that might solve the problem. I'm not so + # sure as we are talking about unknown symbols that will + # only be resolved in the back-end (or not). If I am right + # then one option would be to use the FortranWriter, but + # that would be bigger overhead, or perhaps just the + # declarations part of FortranWriter if that is possible. + # Also see TODO issue #2336 which captures the specific + # problem in LFRic that this fixes. + routine.debug_string() + + # TODO #935 - replace the PSyIR argument data symbols with + # LFRic data symbols. For the moment we just return the + # unmodified PSyIR schedule but this should use + # RaisePSyIR2LFRicKernTrans once KernelInterface is fully + # functional (#928). + ksched = KernelSchedule(routine.symbol, + symbol_table=routine.symbol_table.detach()) + for child in routine.pop_all_children(): + ksched.addchild(child) + routine.replace_with(ksched) + new_schedules.append(ksched) + routines = new_schedules if len(routines) > 1: table = routines[0].scope.symbol_table @@ -703,22 +713,8 @@ class creates the PSyIR schedule on first invocation which is else: sym = None - new_schedules = [] - for sched in routines: - # TODO #935 - replace the PSyIR argument data symbols with - # LFRic data symbols. For the moment we just return the - # unmodified PSyIR schedule but this should use - # RaisePSyIR2LFRicKernTrans once KernelInterface is fully - # functional (#928). - ksched = KernelSchedule(sched.symbol, - symbol_table=sched.symbol_table.detach()) - for child in sched.pop_all_children(): - ksched.addchild(child) - sched.replace_with(ksched) - new_schedules.append(ksched) - self._interface_symbol = sym - self._kern_schedule = new_schedules + self._kern_schedule = routines return self._interface_symbol, self._kern_schedule diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 00f8888de6..039a2698d7 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -581,12 +581,14 @@ def _location_txt(node): if isinstance(container, Container): routines = [] - for name in container.resolve_routine(rsym.name): + all_names = container.resolve_routine(rsym.name) + for name in all_names: psyir = container.find_routine_psyir( name, allow_private=can_be_private) if psyir: routines.append(psyir) - if routines: + if len(routines) == len(all_names): + # We've resolved everything. return routines raise SymbolError( diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 0258da26b1..364da0ebb7 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -852,12 +852,15 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, assert "use my_mod, only : my_interface, my_other_sub\n" in output # We can't test the compilation of this code because of the 'use my_mod.' + # Finally, inline the call to the interface. This should then remove all + # imports from 'my_mod'. intrans.apply(calls[2]) routines = container.walk(Routine) output = fortran_writer(psyir) assert "use my_mod" not in output assert "subroutine my_other_sub" in output assert "interface my_interface" in output + assert Compile(tmpdir).string_compiles(output) def test_mod_inline_no_container(fortran_reader, fortran_writer, tmpdir, From e8b3c0bd7ad507e3a7d5132ec52070affc5d6b37 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Oct 2024 11:18:52 +0100 Subject: [PATCH 028/100] #2716 improvements to validation of calls that resolve to multiple routines --- .../kernel_module_inline_trans.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 5ed7ed7aee..ddb5e08f6e 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -97,12 +97,6 @@ def validate(self, node, options=None): :raises TransformationError: if there is no explicit import of the called Routine and there is already a Routine of that name in the parent Container. - :raises TransformationError: if the PSyIR of the implementation of the - called Routine/kernel cannot be retrieved. - :raises TransformationError: if the name of the routine that - implements the kernel is not the same as the kernel name. This - will happen if the kernel is polymorphic (uses a Fortran - INTERFACE) and will be resolved by #1824. :raises TransformationError: if the kernel cannot be safely inlined. ''' @@ -151,9 +145,29 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error - # TODO ARPDBG - need to examine every kernel implementation, not just - # the first one. - kernel_schedule = kernels[0] + # Validate the PSyIR of each routine/kernel. + for kernel_schedule in kernels: + self._validate_schedule(node, kname, kern_or_call, kernel_schedule) + + def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): + ''' + Validates that the supplied schedule can be module-inlined. + + :param node: the candidate kernel/routine call to inline. + :type node: :py:class:`psyclone.psyGen.CodedKern` | + :py:class:`psyclone.psyir.nodes.Call` + :param str kname: the name of the kernel/routine. + :param str kern_or_call: text for readable error messages. + :param kernel_schedule: the schedule of the routine to inline. + :type kernel_schedule: :py:class:`psyclone.psyir.nodes.Schedule` + + :raises TransformationError: if the called routine contains accesses + to data declared in the same module scope or of unknown origin. + :raises TransformationError: if the called routine has the same + name as an existing Symbol in the calling scope (other than the + one representing the routine itself). + + ''' # We do not support kernels that use symbols representing data # declared in their own parent module (we would need to new imports # from this module for those, and we don't do this yet). @@ -207,11 +221,13 @@ def validate(self, node, options=None): f"symbol name of the module container " f"'{symbol.name}'.") - # If the symbol already exist at the call site it must be referring + # If the symbol already exists at the call site it must be referring # to a Routine existing_symbol = node.scope.symbol_table.lookup(kernel_schedule.name, otherwise=None) - if existing_symbol and not isinstance(existing_symbol, RoutineSymbol): + if not existing_symbol: + return + if not isinstance(existing_symbol, RoutineSymbol): raise TransformationError( f"Cannot module-inline {kern_or_call} '{kname}' because " f"symbol '{existing_symbol}' with the same name already " @@ -445,7 +461,7 @@ def apply(self, node, options=None): # We need to set the visibility of the routine's symbol to # be private. code_to_inline.symbol.visibility = Symbol.Visibility.PRIVATE - node.ancestor(Container).addchild(code_to_inline.detach()) + container.addchild(code_to_inline.detach()) else: if existing_symbol.is_import: # The RoutineSymbol is in the table but that is From 0903b0a567a3f7be1a6d23353f1fa97fbb242f1a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Oct 2024 13:10:23 +0100 Subject: [PATCH 029/100] #2716 add new inlining test --- .../kernel_module_inline_trans.py | 2 +- src/psyclone/domain/lfric/lfric_kern.py | 4 +- .../kernel_module_inline_trans_test.py | 44 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index ddb5e08f6e..b449fe3b28 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -492,7 +492,7 @@ def apply(self, node, options=None): # validation that it's a Routine. Now check if they are # exactly the same. for routine in container.walk(Routine, stop_type=Routine): - if routine.name == existing_symbol.name: + if routine.name.lower() == code_to_inline.name.lower(): # This TransformationError happens here and not in # the validation because it needs the # symbols_to_bring_in applied to effectively diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index da8c4365f9..4002ff8995 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -699,8 +699,8 @@ class creates the PSyIR schedule on first invocation which is # unmodified PSyIR schedule but this should use # RaisePSyIR2LFRicKernTrans once KernelInterface is fully # functional (#928). - ksched = KernelSchedule(routine.symbol, - symbol_table=routine.symbol_table.detach()) + ksched = KernelSchedule( + routine.symbol, symbol_table=routine.symbol_table.detach()) for child in routine.pop_all_children(): ksched.addchild(child) routine.replace_with(ksched) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 364da0ebb7..8700b8af56 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -54,6 +54,7 @@ from psyclone.tests.gocean_build import GOceanBuild from psyclone.tests.lfric_build import LFRicBuild from psyclone.tests.utilities import Compile, count_lines, get_invoke +from psyclone.transformations import ACCRoutineTrans def test_module_inline_constructor_and_str(): @@ -483,6 +484,49 @@ def test_module_inline_apply_kernel_in_multiple_invokes(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) +def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): + ''' Check that module-inline works as expected when the same, polymorphic, + kernel is provided in different invokes and is transformed after being + inlined. ''' + psy, _ = get_invoke("3.5_multi_polymorphic_kernels_multi_invokes.f90", + "lfric", idx=0, dist_mem=False) + + # Module inline kernel in invoke 1 + inline_trans = KernelModuleInlineTrans() + artrans = ACCRoutineTrans() + schedule1 = psy.invokes.invoke_list[0].schedule + for coded_kern in schedule1.walk(CodedKern): + if coded_kern.name == "mixed_code": + inline_trans.apply(coded_kern) + # Transform that kernel. We have to use 'force' as it contains + # a CodeBlock. + artrans.apply(coded_kern, options={"force": True}) + output = str(psy.gen).lower() + assert "subroutine mixed_code_32" in output + assert "!$acc routine seq" in output + assert "subroutine mixed_code_64" in output + assert ("""subroutine invoke_1(scalar_r_phys, field_r_phys, \ +operator_r_def, f1, f2, m1, a, m2, istp, qr) + use testkern_qr_mod, only: testkern_qr_code + use mixed_kernel_mod, only: mixed_code + use quadrature""" in output) + + assert LFRicBuild(tmpdir).code_compiles(psy) + + # Module inline kernel in invoke 2 + schedule2 = psy.invokes.invoke_list[1].schedule + for coded_kern in schedule2.walk(CodedKern): + if coded_kern.name == "mixed_code": + inline_trans.apply(coded_kern) + + assert ("""SUBROUTINE invoke_1(scalar_r_phys, field_r_phys, \ +operator_r_def, f1, f2, m1, a, m2, istp, qr) + USE testkern_qr_mod, ONLY: testkern_qr_code + USE quadrature""" in str(psy.gen)) + + assert LFRicBuild(tmpdir).code_compiles(psy) + + def test_module_inline_apply_with_sub_use(tmpdir): ''' Test that we can module inline a kernel subroutine which contains a use statement''' From a8d357d67c42acdcbbbbe78f38222de2c0bdb05a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Oct 2024 13:26:00 +0100 Subject: [PATCH 030/100] #2716 add new test source file --- ...ulti_polymorphic_kernels_multi_invokes.f90 | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/psyclone/tests/test_files/dynamo0p3/3.5_multi_polymorphic_kernels_multi_invokes.f90 diff --git a/src/psyclone/tests/test_files/dynamo0p3/3.5_multi_polymorphic_kernels_multi_invokes.f90 b/src/psyclone/tests/test_files/dynamo0p3/3.5_multi_polymorphic_kernels_multi_invokes.f90 new file mode 100644 index 0000000000..ac2e35832c --- /dev/null +++ b/src/psyclone/tests/test_files/dynamo0p3/3.5_multi_polymorphic_kernels_multi_invokes.f90 @@ -0,0 +1,68 @@ +! ----------------------------------------------------------------------------- +! BSD 3-Clause License +! +! Copyright (c) 2024, 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 + +program multi_functions_multi_invokes + + ! Description: multiple invoke calls, each involving the same + ! polymorphic kernel. + use constants_mod, only: r_def, i_def + use field_mod, only: field_type + use quadrature_xyoz_mod, only: quadrature_xyoz_type + use mixed_kernel_mod, only: mixed_kernel_type + use testkern_qr_mod, only: testkern_qr_type + + implicit none + + type(field_type) :: f1, f2, m1, m2 + type(field_type) :: fieLd_r_def + type(r_phys_field_type) :: fiEld_r_phys + type(quadrature_xyoz_type) :: qr + type(operator_type) :: operator_r_def + real(r_def) :: a + integer(i_def) :: istp + real(r_def) :: Scalar_r_def + real(r_phys) :: scalAr_r_phys + + call invoke( & + mixed_kernel_type(scalar_r_deF, field_R_def, opeRator_r_def), & + testkern_qr_type(f1, f2, m1, a, m2, istp, qr) & + ) + + call invoke( & + mixed_kernel_type(scaLar_r_phys, fIeld_r_phys, opeRator_r_def), & + testkern_qr_type(f1, f2, m1, a, m2, istp, qr) & + ) + +end program multi_functions_multi_invokes From 48bac41b67b6dcad040d7f72836088ff19b0993a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 11 Oct 2024 11:30:57 +0100 Subject: [PATCH 031/100] #2716 return early if PSyKAl kernel already module inlined --- .../common/transformations/kernel_module_inline_trans.py | 7 +++++++ .../transformations/kernel_module_inline_trans_test.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index b449fe3b28..cd0e8ad7f9 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -403,6 +403,9 @@ def _rm_imported_symbol(name, table): def apply(self, node, options=None): ''' Bring the kernel/subroutine into this Container. + NOTE: when applying this transformation to a Kernel in a PSyKAl invoke, + *all* Kernels of that name in that invoke are marked as inlined. + :param node: the Kernel or Call to module-inline. :type node: :py:class:`psyclone.psyGen.CodedKern` | :py:class:`psyclone.psyir.nodes.Call` @@ -416,6 +419,10 @@ def apply(self, node, options=None): of the caller. ''' + if isinstance(node, CodedKern) and node.module_inline: + # This PSyKal Kernel is already module inlined. + return + if not options: options = {} diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 8700b8af56..93f0a35218 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -458,10 +458,12 @@ def test_module_inline_apply_kernel_in_multiple_invokes(tmpdir): # Module inline kernel in invoke 1 inline_trans = KernelModuleInlineTrans() + artrans = ACCRoutineTrans() schedule1 = psy.invokes.invoke_list[0].schedule for coded_kern in schedule1.walk(CodedKern): if coded_kern.name == "testkern_qr_code": inline_trans.apply(coded_kern) + artrans.apply(coded_kern) gen = str(psy.gen) # After this, one invoke uses the inlined top-level subroutine From c26b8cc856d25bcbe044f5540a437c6d30ff38e8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 11 Oct 2024 20:50:45 +0100 Subject: [PATCH 032/100] #2716 improve apply() so that it returns early if routine already inlined --- .../kernel_module_inline_trans.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index cd0e8ad7f9..4375273e6c 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -441,6 +441,32 @@ def apply(self, node, options=None): _, codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) + container = node.ancestor(Container) + local_table = node.scope.symbol_table + + if interface_sym: + local_sym = local_table.lookup(interface_sym.name, otherwise=None) + if interface_sym is local_sym and not local_sym.is_import: + # Interface symbol is already local so nothing to do. + if isinstance(node, CodedKern): + node.module_inline = True + # TODO #11 - log this. + return + else: + for routine in codes_to_inline: + local_sym = local_table.lookup(routine.symbol.name, + otherwise=None) + if (not local_sym or local_sym is not routine.symbol or + local_sym.is_import): + # This routine is not module-inlined. + break + else: + # All routines are module-inlined so there's nothing to do. + # TODO #11 - log this. + if isinstance(node, CodedKern): + node.module_inline = True + return + updated_routines = self._prepare_code_to_inline(codes_to_inline) # Update the Kernel to point to the updated PSyIR. if isinstance(node, CodedKern): @@ -452,9 +478,6 @@ def apply(self, node, options=None): updated_routines[0].scope.symbol_table.lookup( interface_sym.name)) - container = node.ancestor(Container) - local_table = node.scope.symbol_table - for code_to_inline in updated_routines: try: existing_symbol = node.scope.symbol_table.lookup( From 4568467d387b2b8c94a1e421a12cbace10ef6aec Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 14 Oct 2024 10:54:39 +0100 Subject: [PATCH 033/100] #2716 update lfric inlining example (eg2) --- examples/lfric/eg2/module_inline_trans.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/lfric/eg2/module_inline_trans.py b/examples/lfric/eg2/module_inline_trans.py index e1fd7fb55b..5a92f7c4e8 100644 --- a/examples/lfric/eg2/module_inline_trans.py +++ b/examples/lfric/eg2/module_inline_trans.py @@ -46,13 +46,10 @@ def trans(psy): PSyclone transformation routine. This is an example which module-inlines the kernel used in the second 'invoke' in the supplied PSy object. - :param psy: the PSy object that PSyclone has constructed for the \ + :param psy: the PSy object that PSyclone has constructed for the 'invoke'(s) found in the Algorithm file. :type psy: :py:class:`psyclone.dynamo0p3.DynamoPSy` - :returns: the transformed PSy object. - :rtype: :py:class:`psyclone.dynamo0p3.DynamoPSy` - ''' invokes = psy.invokes print(psy.invokes.names) @@ -61,15 +58,7 @@ def trans(psy): print(schedule.view()) # Find the kernel we want to inline. kern = schedule.walk(Kern)[0] - # Setting module inline directly. - kern.module_inline = True - print(schedule.view()) - # Unsetting module inline via a transformation. inline_trans = KernelModuleInlineTrans() - inline_trans.apply(kern, {"inline": False}) - print(schedule.view()) - # Setting module inline via a transformation. + # Apply the inlining transformation. inline_trans.apply(kern) print(schedule.view()) - - return psy From dbdec4f7384afb9046abc472ab4588f37e6dc7ba Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 14 Nov 2024 13:41:25 +0000 Subject: [PATCH 034/100] #2716 tidying after merge --- examples/gocean/eg3/ocl_trans.py | 8 ++++---- examples/lfric/scripts/kernel_print.py | 4 ++-- src/psyclone/dynamo0p3.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index ee8ec35407..c2f850967b 100644 --- a/examples/gocean/eg3/ocl_trans.py +++ b/examples/gocean/eg3/ocl_trans.py @@ -37,10 +37,10 @@ the first Invoke to use OpenCL. ''' from psyclone.psyGen import InvokeSchedule -from psyclone.psyir.transformations import \ - FoldConditionalReturnExpressionsTrans -from psyclone.domain.gocean.transformations import GOOpenCLTrans, \ - GOMoveIterationBoundariesInsideKernelTrans +from psyclone.psyir.transformations import ( + FoldConditionalReturnExpressionsTrans) +from psyclone.domain.gocean.transformations import ( + GOOpenCLTrans, GOMoveIterationBoundariesInsideKernelTrans) def trans(psyir): diff --git a/examples/lfric/scripts/kernel_print.py b/examples/lfric/scripts/kernel_print.py index 09ff53b6c4..49a2fed16e 100644 --- a/examples/lfric/scripts/kernel_print.py +++ b/examples/lfric/scripts/kernel_print.py @@ -58,9 +58,9 @@ def trans(psyir): _, kernel_schedules = kernel.get_kernel_schedule() for ksched in kernel_schedules: if ksched not in already_printed: - kern = fortran_writer(kernel_schedule) + kern = fortran_writer(ksched) print(kern) - already_printed.append(kernel_schedule) + already_printed.append(ksched) except Exception as err: # pylint: disable=broad-except print(f"Code of '{kernel.name}' " f"cannot be printed because:\n{err}") diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py index d33ef462bb..970a296c17 100644 --- a/src/psyclone/dynamo0p3.py +++ b/src/psyclone/dynamo0p3.py @@ -72,10 +72,10 @@ Assignment, ACCEnterDataDirective, ScopingNode, ArrayOfStructuresReference, Reference, Schedule, StructureReference, Literal, IfBlock, Call, BinaryOperation, IntrinsicCall, Container) -from psyclone.psyir.symbols import (INTEGER_TYPE, DataSymbol, ScalarType, - UnresolvedType, DataTypeSymbol, - ContainerSymbol, ImportInterface, - ArrayType, UnsupportedFortranType) +from psyclone.psyir.symbols import ( + GenericInterfaceSymbol, INTEGER_TYPE, DataSymbol, DataTypeSymbol, + ScalarType, UnresolvedType, ContainerSymbol, ImportInterface, + ArrayType, UnsupportedFortranType) # pylint: disable=too-many-lines From 62b53d6eea7f4c98ac028eac2760168992c8324b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 18 Nov 2024 15:31:32 +0000 Subject: [PATCH 035/100] #2716 add RANDOM_NUMBER to intrinsics available on device --- src/psyclone/psyir/nodes/intrinsic_call.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 04244b6bd6..05e3e0c4b7 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -801,6 +801,7 @@ def is_available_on_device(self): IntrinsicCall.Intrinsic.PRODUCT, IntrinsicCall.Intrinsic.SIZE, IntrinsicCall.Intrinsic.SUM, IntrinsicCall.Intrinsic.LBOUND, IntrinsicCall.Intrinsic.MATMUL, + IntrinsicCall.Intrinsic.RANDOM_NUMBER, IntrinsicCall.Intrinsic.UBOUND) @classmethod From 8b025dbfe7637563c07cb5afd82b1dc15cc3a2a3 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 18 Nov 2024 20:45:11 +0000 Subject: [PATCH 036/100] #2716 rename Kern._kern_schedule to plural and tidy --- .../kernel_module_inline_trans.py | 45 +++++++++---------- src/psyclone/domain/lfric/lfric_kern.py | 10 ++--- src/psyclone/gocean1p0.py | 8 ++-- src/psyclone/psyGen.py | 10 ++--- .../kernel_module_inline_trans_test.py | 6 +-- .../tests/domain/gocean/kernel/gokern_test.py | 4 +- .../tests/domain/lfric/lfric_kern_test.py | 4 +- 7 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 4375273e6c..4cf4bb6554 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -137,8 +137,7 @@ def validate(self, node, options=None): # Check that the PSyIR of the routine/kernel can be retrieved. try: - _, kernels, _ = ( - KernelModuleInlineTrans._get_psyir_to_inline(node)) + kernels, _ = KernelModuleInlineTrans._get_psyir_to_inline(node) except Exception as error: raise TransformationError( f"{self.name} failed to retrieve PSyIR for {kern_or_call} " @@ -333,21 +332,21 @@ def _prepare_code_to_inline(routines_to_inline): @staticmethod def _get_psyir_to_inline(node): ''' - Wrapper that gets the name and PSyIR of the routine or kernel - corresponding to the call described by `node`. + Wrapper that gets the PSyIR of the routine or kernel + corresponding to the call described by `node`. This supports calls to + routines or kernels which are polymorphic by returning a list of + Routine objects, as well as the associated interface symbol. :param node: the Call or CodedKern to resolve. :type node: :py:class:`psyclone.psyir.nodes.Call` | :py:class:`psyclone.psyGen.CodedKern` - :returns: the name of the routine as seen by the caller and the - PSyIR of the routine implementation. - :rtype: Tuple(str, list[:py:class:`psyclone.psyir.nodes.Routine`], - :py:class:`psyclone.psyir.symbols.Symbol`) + :returns: the PSyIR of the routine implementation(s) and the associated + interface symbol if it is polymorphic. + :rtype: tuple[ + list[:py:class:`psyclone.psyir.nodes.Routine`], + :py:class:`psyclone.psyir.symbols.GenericInterfaceSymbol`)] - :raises TransformationError: if we have a call to a language-level - Routine that maps to an Interface block as this is not yet - supported (TODO #924). ''' # TODO #2054 - once CodedKern has been migrated so that it subclasses # Call then this if/else (and thus this whole routine) can be removed. @@ -357,16 +356,16 @@ def _get_psyir_to_inline(node): # call to get_kernel_schedule() will return the one which has an # interface matching the arguments in the call. interface_sym, routines = node.get_kernel_schedule() - caller_name = node.name.lower() - else: - # We have a generic routine call. - routines = node.get_callees() - caller_name = node.routine.name.lower() - interface_sym = None - if len(routines) > 1: - interface_sym = routines[0].symbol_table.lookup(caller_name) + return (routines, interface_sym) + + # We have a generic routine call. + routines = node.get_callees() + caller_name = node.routine.name.lower() + interface_sym = None + if len(routines) > 1: + interface_sym = routines[0].symbol_table.lookup(caller_name) - return (caller_name, routines, interface_sym) + return (routines, interface_sym) @staticmethod def _rm_imported_symbol(name, table): @@ -436,9 +435,7 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - # TODO - get rid of 'caller name' return value from - # _get_psyir_to_inline? - _, codes_to_inline, interface_sym = ( + codes_to_inline, interface_sym = ( KernelModuleInlineTrans._get_psyir_to_inline(node)) container = node.ancestor(Container) @@ -472,7 +469,7 @@ def apply(self, node, options=None): if isinstance(node, CodedKern): # TODO - add setter for these properties to Kern? # pylint: disable=protected-access - node._kern_schedule = updated_routines + node._kern_schedules = updated_routines if interface_sym: node._interface_symbol = ( updated_routines[0].scope.symbol_table.lookup( diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 3acb82ad3f..9d04c44245 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -714,7 +714,7 @@ class creates the PSyIR schedule on first invocation which is PSyIR Schedule using LFRic-specific PSyIR where possible. :returns: the Symbol defining the interface to this kernel (if it is - polymorphic and a list of the Schedule(s) representing the kernel + polymorphic) and a list of the Schedule(s) representing the kernel code. :rtype: tuple[Optional[:py:class:`psyclone.psyir.symbols.Symbol`], list[:py:class:`psyclone.psyGen.KernelSchedule`]] @@ -723,8 +723,8 @@ class creates the PSyIR schedule on first invocation which is be found in the parse tree of the associated source code. ''' - if self._kern_schedule: - return self._interface_symbol, self._kern_schedule + if self._kern_schedules: + return self._interface_symbol, self._kern_schedules # Check for a local implementation of this kernel first. container = self.ancestor(Container) @@ -782,9 +782,9 @@ class creates the PSyIR schedule on first invocation which is sym = None self._interface_symbol = sym - self._kern_schedule = routines + self._kern_schedules = routines - return self._interface_symbol, self._kern_schedule + return self._interface_symbol, self._kern_schedules def validate_kernel_code_args(self, table): '''Check that the arguments in the kernel code match the expected diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index fbedf1275c..b9d7797f70 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1248,8 +1248,8 @@ def get_kernel_schedule(self): :raises GenerationError: if there is a problem raising the language- level PSyIR of this kernel to GOcean PSyIR. ''' - if self._kern_schedule: - return None, self._kern_schedule + if self._kern_schedules: + return None, self._kern_schedules # Construct the PSyIR of the Fortran parse tree. astp = Fparser2Reader() @@ -1270,9 +1270,9 @@ def get_kernel_schedule(self): # We know the above loop will find the named routine because the # previous raising transformation would have failed otherwise. # pylint: disable=undefined-loop-variable - self._kern_schedule = [routine] + self._kern_schedules = [routine] - return None, self._kern_schedule + return None, self._kern_schedules class GOKernelArguments(Arguments): diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 047c987662..42161207ff 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1352,13 +1352,13 @@ def __init__(self, KernelArguments, call, parent=None, check=True): KernelArguments, check) self._module_code = call.ktype._ast self._kernel_code = call.ktype.procedure - self._fp2_ast = None # The fparser2 AST for the kernel - self._kern_schedule = None # PSyIR schedules for the kernel + self._fp2_ast = None #: The fparser2 AST for the kernel + self._kern_schedules = None #: PSyIR schedule(s) for the kernel self._interface_symbol = None - # Whether or not this kernel has been transformed + #: Whether or not this kernel has been transformed self._modified = False - # Whether or not to in-line this kernel into the module containing - # the PSy layer + #: Whether or not to in-line this kernel into the module containing + #: the PSy layer self._module_inline = False self._opencl_options = {'local_size': 64, 'queue_number': 1} self.arg_descriptors = call.ktype.arg_descriptors diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 93f0a35218..47a2c37276 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -258,7 +258,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) + monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) # and try to apply the transformation inline_trans = KernelModuleInlineTrans() @@ -281,7 +281,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) + monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) # and try to apply the transformation with pytest.raises(TransformationError) as err: @@ -303,7 +303,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedule", [routine]) + monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) container = kern_call.ancestor(Container) assert "compute_cv_code" not in container.symbol_table diff --git a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py index b88525c68a..779f8028de 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py @@ -99,7 +99,7 @@ def test_gok_get_kernel_schedule(): psy = PSyFactory(API, distributed_memory=False).create(invoke_info) schedule = psy.invokes.invoke_list[0].schedule kern = schedule.walk(GOKern)[0] - assert kern._kern_schedule is None + assert kern._kern_schedules is None sym, scheds = kern.get_kernel_schedule() assert sym is None assert isinstance(scheds, list) @@ -111,7 +111,7 @@ def test_gok_get_kernel_schedule(): assert scheds2[0] is sched # Check that the expected error is raised if the subroutine that # implements the kernel cannot be found. - kern._kern_schedule = None + kern._kern_schedules = None # Remove the subroutine that implements the kernel from the Fortran # parse tree. subs = walk(kern.ast, Fortran2003.Subroutine_Subprogram) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index ecc96bf3df..4c433245ab 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -149,13 +149,13 @@ def test_get_kernel_schedule(): # matrix vector kernel kernel = schedule[2].loop_body[0] - assert kernel._kern_schedule is None + assert kernel._kern_schedules is None sym, kernel_schedules = kernel.get_kernel_schedule() assert sym is None assert len(kernel_schedules) == 1 assert isinstance(kernel_schedules[0], KernelSchedule) - assert kernel._kern_schedule[0] is kernel_schedules[0] + assert kernel._kern_schedules[0] is kernel_schedules[0] _, kernel_schedules_2 = kernel.get_kernel_schedule() assert kernel_schedules[0] is kernel_schedules_2[0] From 7cdb37343259215a0b4713fc8aaeb58adb908f7d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 19 Nov 2024 17:28:33 +0000 Subject: [PATCH 037/100] #2716 add xfail for failure to compile case where only one kernel is module-inlined --- .../transformations/kernel_module_inline_trans_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 47a2c37276..869fe915df 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -513,7 +513,9 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): use mixed_kernel_mod, only: mixed_code use quadrature""" in output) - assert LFRicBuild(tmpdir).code_compiles(psy) + success = LFRicBuild(tmpdir).code_compiles(psy) + if not success: + pytest.xfail("nvfortran can't build this code") # Module inline kernel in invoke 2 schedule2 = psy.invokes.invoke_list[1].schedule @@ -521,11 +523,13 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): if coded_kern.name == "mixed_code": inline_trans.apply(coded_kern) + output2 = str(psy.gen) assert ("""SUBROUTINE invoke_1(scalar_r_phys, field_r_phys, \ operator_r_def, f1, f2, m1, a, m2, istp, qr) USE testkern_qr_mod, ONLY: testkern_qr_code - USE quadrature""" in str(psy.gen)) + USE quadrature""" in output2) + assert "mixed_kernel_mod" not in output2 assert LFRicBuild(tmpdir).code_compiles(psy) From d18730f7bc89b4db2454c935789890623b4ff640 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 20 Nov 2024 11:29:07 +0000 Subject: [PATCH 038/100] #2716 improve xfailing test to be more specific --- .../transformations/kernel_module_inline_trans_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 869fe915df..801c3e3645 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -515,7 +515,13 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): success = LFRicBuild(tmpdir).code_compiles(psy) if not success: - pytest.xfail("nvfortran can't build this code") + if LFRicBuild.F90 == "nvfortran": + pytest.xfail( + reason="nvfortran has a bug when a local import of a generic " + "interface overrides an interface of the same name in an " + "outer scope.") + else: + assert False # Module inline kernel in invoke 2 schedule2 = psy.invokes.invoke_list[1].schedule From be9d61ac43d22abdc1e342317b2cc6b60b590e1f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 22 Nov 2024 10:33:47 +0000 Subject: [PATCH 039/100] #2716 improve comment --- .../common/transformations/kernel_module_inline_trans.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 4cf4bb6554..1749975f68 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -393,7 +393,8 @@ def _rm_imported_symbol(name, table): symbol.name not in table else table) remove_csym = actual_table.symbols_imported_from(csym) == [symbol] # We have to force the removal as there will be calls that reference - # this Symbol. + # this Symbol. (These calls will subsequently be updated to refer to + # the Symbol of the inlined routine.) # pylint:disable-next=protected-access actual_table._symbols.pop(symbol.name) if remove_csym: From ee64d54c0c4832c365e890923ef50fc60e1cd9ac Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 28 Nov 2024 10:52:48 +0000 Subject: [PATCH 040/100] #2732 mv fix for interface symbols into new lfric_psy.py file --- src/psyclone/domain/lfric/lfric_psy.py | 13 +++++++++++-- src/psyclone/dynamo0p3.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_psy.py b/src/psyclone/domain/lfric/lfric_psy.py index b7b2b91008..e45ae225c8 100644 --- a/src/psyclone/domain/lfric/lfric_psy.py +++ b/src/psyclone/domain/lfric/lfric_psy.py @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BSD 3-Clause License # -# Copyright (c) 2017-2023, Science and Technology Facilities Council. +# Copyright (c) 2017-2024, Science and Technology Facilities Council. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -46,9 +46,10 @@ from psyclone.configuration import Config from psyclone.domain.lfric import (LFRicConstants, LFRicSymbolTable, LFRicInvokes) -from psyclone.f2pygen import ModuleGen, UseGen, PSyIRGen +from psyclone.f2pygen import InterfaceDeclGen, ModuleGen, UseGen, PSyIRGen from psyclone.psyGen import PSy, InvokeSchedule from psyclone.psyir.nodes import ScopingNode +from psyclone.psyir.symbols import GenericInterfaceSymbol class LFRicPSy(PSy): @@ -156,6 +157,14 @@ def gen(self): if not isinstance(routine, InvokeSchedule): psy_module.add(PSyIRGen(psy_module, routine)) + # Similarly, we have to take care of any Interface symbols (which + # we may have if we've inlined a polymorphic Kernel). + for sym in self.container.symbol_table.symbols: + if isinstance(sym, GenericInterfaceSymbol): + names = [rt.symbol.name for rt in sym.routines] + psy_module.add( + InterfaceDeclGen(psy_module, sym.name, names)) + # Add all invoke-specific information self.invokes.gen_code(psy_module) diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py index 19584fd49a..9621104a2b 100644 --- a/src/psyclone/dynamo0p3.py +++ b/src/psyclone/dynamo0p3.py @@ -60,8 +60,8 @@ from psyclone.domain.lfric.lfric_invoke_schedule import LFRicInvokeSchedule from psyclone.errors import GenerationError, InternalError, FieldNotFoundError from psyclone.f2pygen import (AllocateGen, AssignGen, CallGen, CommentGen, - DeallocateGen, DeclGen, DoGen, InterfaceDeclGen, - ModuleGen, PSyIRGen, TypeDeclGen, UseGen) + DeallocateGen, DeclGen, DoGen, + PSyIRGen, TypeDeclGen, UseGen) from psyclone.parse.kernel import getkerneldescriptors from psyclone.parse.utils import ParseError from psyclone.psyGen import (InvokeSchedule, Arguments, From 5289791947c2e8c492e004a0f46c027c30da783d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 28 Nov 2024 11:57:44 +0000 Subject: [PATCH 041/100] #2716 extend get_callees() to allow for private routines --- src/psyclone/psyir/nodes/call.py | 136 ++++++++++++-------- src/psyclone/tests/psyir/nodes/call_test.py | 52 ++++++++ 2 files changed, 134 insertions(+), 54 deletions(-) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 039a2698d7..3045b88682 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -47,7 +47,8 @@ from psyclone.psyir.nodes.reference import Reference from psyclone.psyir.nodes.routine import Routine from psyclone.psyir.symbols import ( - RoutineSymbol, Symbol, SymbolError, UnsupportedFortranType) + GenericInterfaceSymbol, RoutineSymbol, Symbol, SymbolError, + UnsupportedFortranType) class Call(Statement, DataNode): @@ -442,6 +443,80 @@ def copy(self): return new_copy + def _get_unresolved_callee(self): + ''' + Searches for the implementation(s) of the target routine for this Call + when it is unresolved. + + :returns: the Routine(s) that this call targets. + :rtype: list[:py:class:`psyclone.psyir.nodes.Routine`] + + :raises NotImplementedError: if the routine is not found + in any containers in scope at the call site. + ''' + rsym = self.routine.symbol + # Check for any "raw" Routines, i.e. any that are not in a Container. + # Such Routines would exist in the PSyIR as a child of a FileContainer + # (if the PSyIR contains a FileContainer). Note, if the PSyIR does + # contain a FileContainer, it will be the root node of the PSyIR. + for routine in self.root.children: + if (isinstance(routine, Routine) and + routine.name.lower() == rsym.name.lower()): + return [routine] + + # Now check for any wildcard imports and see if they can + # be used to resolve the symbol. + wildcard_names = [] + containers_not_found = [] + current_table = self.scope.symbol_table + while current_table: + for container_symbol in current_table.containersymbols: + if container_symbol.wildcard_import: + wildcard_names.append(container_symbol.name) + try: + container = container_symbol.find_container_psyir( + local_node=self) + except SymbolError: + container = None + if not container: + # Failed to find/process this Container. + containers_not_found.append(container_symbol.name) + continue + routines = [] + target_sym = container.symbol_table.lookup(rsym.name, + otherwise=None) + if target_sym: + # If the target of the call turns out to be an + # interface then the routines themselves are allowed + # to be private. + can_be_private = isinstance(target_sym, + GenericInterfaceSymbol) + for name in container.resolve_routine(rsym.name): + psyir = container.find_routine_psyir( + name, + allow_private=can_be_private) + if psyir: + routines.append(psyir) + if routines: + return routines + current_table = current_table.parent_symbol_table() + if not wildcard_names: + wc_text = "there are no wildcard imports" + else: + if containers_not_found: + wc_text = ( + f"attempted to resolve the wildcard imports from" + f" {wildcard_names}. However, failed to find the " + f"source for {containers_not_found}. The module search" + f" path is set to {Config.get().include_paths}") + else: + wc_text = (f"wildcard imports from {wildcard_names}") + raise NotImplementedError( + f"Failed to find the source code of the unresolved routine " + f"'{rsym.name}' - looked at any routines in the same source " + f"file and {wc_text}. Searching for external routines " + f"that are only resolved at link time is not supported.") + def get_callees(self): ''' Searches for the implementation(s) of the target routine for this Call. @@ -474,59 +549,7 @@ def _location_txt(node): rsym = self.routine.symbol if rsym.is_unresolved: - - # Check for any "raw" Routines, i.e. ones that are not - # in a Container. Such Routines would exist in the PSyIR - # as a child of a FileContainer (if the PSyIR contains a - # FileContainer). Note, if the PSyIR does contain a - # FileContainer, it will be the root node of the PSyIR. - for routine in self.root.children: - if (isinstance(routine, Routine) and - routine.name.lower() == rsym.name.lower()): - return [routine] - - # Now check for any wildcard imports and see if they can - # be used to resolve the symbol. - wildcard_names = [] - containers_not_found = [] - current_table = self.scope.symbol_table - while current_table: - for container_symbol in current_table.containersymbols: - if container_symbol.wildcard_import: - wildcard_names.append(container_symbol.name) - try: - container = container_symbol.find_container_psyir( - local_node=self) - except SymbolError: - container = None - if not container: - # Failed to find/process this Container. - containers_not_found.append(container_symbol.name) - continue - routines = [] - for name in container.resolve_routine(rsym.name): - psyir = container.find_routine_psyir(name) - if psyir: - routines.append(psyir) - if routines: - return routines - current_table = current_table.parent_symbol_table() - if not wildcard_names: - wc_text = "there are no wildcard imports" - else: - if containers_not_found: - wc_text = ( - f"attempted to resolve the wildcard imports from" - f" {wildcard_names}. However, failed to find the " - f"source for {containers_not_found}. The module search" - f" path is set to {Config.get().include_paths}") - else: - wc_text = (f"wildcard imports from {wildcard_names}") - raise NotImplementedError( - f"Failed to find the source code of the unresolved routine " - f"'{rsym.name}' - looked at any routines in the same source " - f"file and {wc_text}. Searching for external routines " - f"that are only resolved at link time is not supported.") + return self._get_unresolved_callee() root_node = self.ancestor(Container) if not root_node: @@ -535,6 +558,7 @@ def _location_txt(node): can_be_private = True if rsym.is_import: + # Chase down the Container from which the symbol is imported. cursor = rsym # A Routine imported from another Container must be public in that # Container. @@ -582,6 +606,10 @@ def _location_txt(node): if isinstance(container, Container): routines = [] all_names = container.resolve_routine(rsym.name) + if isinstance(rsym, GenericInterfaceSymbol): + # Although the interface must be public, the routines to which + # it points may themselves be private. + can_be_private = True for name in all_names: psyir = container.find_routine_psyir( name, allow_private=can_be_private) diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index cba9d7ed2e..9629d81406 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -932,6 +932,58 @@ def test_call_get_callees_wildcard_import_container(fortran_reader, assert routines[0].name == "just_do_it" +@pytest.mark.usefixtures("clear_module_manager_instance") +def test_call_get_callees_wildcard_import_interface_container( + fortran_reader, tmpdir, monkeypatch): + ''' + Check that get_callees() works successfully for a routine accessed via + a wildcard import of an interface from a module in another file. This + includes the case where the procedures pointed to by that interface are + themselves private. + + ''' + code = ''' +module other_mod + use some_mod +contains + subroutine run_it() + call just_do_it() + end subroutine run_it +end module other_mod +''' + psyir = fortran_reader.psyir_from_source(code) + call = psyir.walk(Call)[0] + # Create the module containing the subroutine definition, + # write it to file and set the search path so that PSyclone can find it. + path = str(tmpdir) + monkeypatch.setattr(Config.get(), '_include_paths', [path]) + + with open(os.path.join(path, "some_mod.f90"), + "w", encoding="utf-8") as mfile: + mfile.write('''\ +module some_mod + interface just_do_it + module procedure :: just_now, just_then + end interface + ! The procedures themselves are declared as private to this module + ! although the interface is public. + private :: just_now, just_then +contains + subroutine just_now() + write(*,*) "hello" + end subroutine just_now + subroutine just_then(arg) + integer :: arg + write(*,*) "goodbye" + end subroutine just_then +end module some_mod''') + routines = call.get_callees() + assert len(routines) == 2 + assert all(isinstance(node, Routine) for node in routines) + assert routines[0].name == "just_now" + assert routines[1].name == "just_then" + + def test_fn_call_get_callees(fortran_reader): ''' Test that get_callees() works for a function call. From ce6f6a90430626213b04f0118ca6e3713e9a6d7f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 28 Nov 2024 11:58:49 +0000 Subject: [PATCH 042/100] #2716 fix linting --- src/psyclone/dynamo0p3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py index 9621104a2b..7fdc69a8a1 100644 --- a/src/psyclone/dynamo0p3.py +++ b/src/psyclone/dynamo0p3.py @@ -73,9 +73,9 @@ Reference, Schedule, StructureReference, Literal, IfBlock, Call, BinaryOperation, IntrinsicCall, Container) from psyclone.psyir.symbols import ( - GenericInterfaceSymbol, INTEGER_TYPE, DataSymbol, DataTypeSymbol, + ArrayType, INTEGER_TYPE, DataSymbol, DataTypeSymbol, ScalarType, UnresolvedType, ContainerSymbol, ImportInterface, - ArrayType, UnsupportedFortranType) + UnsupportedFortranType) # pylint: disable=too-many-lines From 54b9a7f6f252ed6c7c6d50228d371cb0aa21ae1b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 28 Nov 2024 17:15:43 +0000 Subject: [PATCH 043/100] #2716 experiment with recursive inlining --- examples/lfric/scripts/gpu_offloading.py | 50 ++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index dd0618ace5..1b98da7b96 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -45,9 +45,11 @@ import sys from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import LFRicConstants -from psyclone.psyir.nodes import Directive, Loop, Routine +from psyclone.psyGen import CodedKern +from psyclone.psyir.nodes import ( + Call, Directive, IntrinsicCall, Loop, Routine, Schedule) from psyclone.psyir.transformations import ( - ACCKernelsTrans, TransformationError, OMPTargetTrans) + ACCKernelsTrans, InlineTrans, TransformationError, OMPTargetTrans) from psyclone.transformations import ( Dynamo0p3ColourTrans, Dynamo0p3OMPLoopTrans, Dynamo0p3RedundantComputationTrans, OMPParallelTrans, @@ -62,6 +64,38 @@ OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") +def _inline_calls(kern): + ''' + Recursively inline all calls. + + ''' + mod_inline_trans = KernelModuleInlineTrans() + intrans = InlineTrans() + + if isinstance(kern, CodedKern): + _, scheds = kern.get_kernel_schedule() + else: + scheds = [kern] + for sched in scheds: + sched: Schedule + for call in sched.walk(Call): + call: Call + if isinstance(call, IntrinsicCall): + continue + try: + for inner_call in call.get_callees(): + _inline_calls(inner_call) + mod_inline_trans.apply(call) + try: + intrans.apply(call) + except TransformationError as err: + print(f"Failed to inline call {call.debug_string()}:\n{err}") + pass + except TransformationError as err: + print(f"Failed to module-inline routine {call.routine.name}:\n{err}") + pass + + def trans(psyir): '''Applies PSyclone colouring and GPU offloading transformations. Any kernels that cannot be offloaded to GPU are parallelised using OpenMP @@ -77,7 +111,8 @@ def trans(psyir): otrans = Dynamo0p3OMPLoopTrans() const = LFRicConstants() cpu_parallel = OMPParallelTrans() - intrans = KernelModuleInlineTrans() + mod_inline_trans = KernelModuleInlineTrans() + intrans = InlineTrans() if OFFLOAD_DIRECTIVES == "omp": # Use OpenMP offloading @@ -141,7 +176,14 @@ def trans(psyir): if offload: for kern in loop.kernels(): try: - intrans.apply(kern) + mod_inline_trans.apply(kern) + _inline_calls(kern) + #try: + # kern.lower_to_language_level() + # intrans.apply(kern) + #except TransformationError as err: + # print(f"Failed to inline kernel '{kern.name}' " + # f"due to:\n{err.value}") except TransformationError as err: failed_inline.add(kern.name.lower()) print(f"Failed to module-inline kernel " From c73946885f78d5f93a4f897ed42669b22b804a1b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 29 Nov 2024 21:13:09 +0000 Subject: [PATCH 044/100] #2716 fix bug in get_callees() when no Container is created --- src/psyclone/psyir/nodes/call.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 3045b88682..31b238fef0 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -573,6 +573,11 @@ def _location_txt(node): f"Container '{csym.name}' but the source defining " f"that container could not be found. The module search" f" path is set to {Config.get().include_paths}") + if not container: + raise NotImplementedError( + f"RoutineSymbol '{rsym.name}' is imported from " + f"Container '{csym.name}' but the PSyIR for that " + f"container could not be generated.") imported_sym = container.symbol_table.lookup(cursor.name) if imported_sym.visibility != Symbol.Visibility.PUBLIC: # The required Symbol must be shadowed with a PRIVATE From 622e0155a174278b70ebecc07f8f629b711d6dfa Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 29 Nov 2024 21:13:55 +0000 Subject: [PATCH 045/100] #2716 improve robustness of Matmul2CodeTrans.validate() --- .../intrinsics/matmul2code_trans.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py b/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py index 29fe2f8363..616bca3d19 100644 --- a/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py +++ b/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py @@ -47,7 +47,8 @@ BinaryOperation, Assignment, Reference, Loop, Literal, ArrayReference, Range, IntrinsicCall) from psyclone.psyir.symbols import ( - DataSymbol, INTEGER_TYPE, REAL_TYPE, ArrayType, UnsupportedType) + ArrayType, DataSymbol, INTEGER_TYPE, REAL_TYPE, TypedSymbol, + UnsupportedType) from psyclone.psyir.transformations.intrinsics.intrinsic2code_trans import ( Intrinsic2CodeTrans) @@ -318,12 +319,13 @@ def validate(self, node, options=None): f"be references, but found: '{node.parent.debug_string()}'.") # The arguments of matvec should be References to arrays - if any(isinstance(var.symbol.datatype, UnsupportedType) for var - in [matrix1, matrix2, result]): - raise TransformationError( - f"Must have full type information for result and operands of " - f"MATMUL IntrinsicCall but found '{result.symbol}', " - f"'{matrix1.symbol}' and '{matrix2.symbol}'.") + for var in [matrix1, matrix2, result]: + if (not isinstance(var.symbol, TypedSymbol) or + isinstance(var.symbol.datatype, UnsupportedType)): + raise TransformationError( + f"Must have full type information for result and operands " + f"of MATMUL IntrinsicCall but found '{result.symbol}', " + f"'{matrix1.symbol}' and '{matrix2.symbol}'.") if (len(matrix1.symbol.shape) == 0 or len(matrix2.symbol.shape) == 0 or len(result.symbol.shape) == 0): raise TransformationError( From 6f45c3637d33a2990798783ac4479fcc9384214f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 29 Nov 2024 21:14:45 +0000 Subject: [PATCH 046/100] #2716 add Matmul2CodeTrans to gpu_offloading.py --- examples/lfric/scripts/gpu_offloading.py | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 1b98da7b96..6965a879ac 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -49,7 +49,8 @@ from psyclone.psyir.nodes import ( Call, Directive, IntrinsicCall, Loop, Routine, Schedule) from psyclone.psyir.transformations import ( - ACCKernelsTrans, InlineTrans, TransformationError, OMPTargetTrans) + ACCKernelsTrans, InlineTrans, Matmul2CodeTrans, OMPTargetTrans, + TransformationError) from psyclone.transformations import ( Dynamo0p3ColourTrans, Dynamo0p3OMPLoopTrans, Dynamo0p3RedundantComputationTrans, OMPParallelTrans, @@ -61,6 +62,10 @@ INVOKE_EXCLUSIONS = [ ] +# We won't attempt to inline calls to routines with names that contain +# these strings. +INLINE_EXCLUSIONS = ["abort", "logging"] + OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") @@ -68,9 +73,12 @@ def _inline_calls(kern): ''' Recursively inline all calls. + :param kern: the Kernel or Routine to inline any Calls into. + ''' mod_inline_trans = KernelModuleInlineTrans() intrans = InlineTrans() + matrans = Matmul2CodeTrans() if isinstance(kern, CodedKern): _, scheds = kern.get_kernel_schedule() @@ -81,6 +89,12 @@ def _inline_calls(kern): for call in sched.walk(Call): call: Call if isinstance(call, IntrinsicCall): + try: + matrans.apply(call) + except TransformationError: + pass + continue + if any(name in call.routine.name for name in INLINE_EXCLUSIONS): continue try: for inner_call in call.get_callees(): @@ -89,11 +103,11 @@ def _inline_calls(kern): try: intrans.apply(call) except TransformationError as err: - print(f"Failed to inline call {call.debug_string()}:\n{err}") - pass - except TransformationError as err: - print(f"Failed to module-inline routine {call.routine.name}:\n{err}") - pass + print(f"Failed to inline call {call.debug_string()}:\n" + f"{err}") + except (TransformationError, NotImplementedError) as err: + print(f"Failed to module-inline routine {call.routine.name}:\n" + f"{err}") def trans(psyir): From 5fdf36d8a702e413e52d7744e6fd8570f21d7506 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 29 Nov 2024 21:15:38 +0000 Subject: [PATCH 047/100] #2716 fixes to KernelModuleInlineTrans to allow for previously-inlined routine and precision symbols --- .../kernel_module_inline_trans.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 1749975f68..0bcdb81561 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -270,15 +270,20 @@ def _prepare_code_to_inline(routines_to_inline): copied_routines.append(code_to_inline) # First make a set with all symbols used inside the subroutine all_symbols = set() + init_exprns = [] for scope in code_to_inline.walk(ScopingNode): for symbol in scope.symbol_table.symbols: all_symbols.add(symbol) - for reference in code_to_inline.walk(Reference): - all_symbols.add(reference.symbol) - for literal in code_to_inline.walk(Literal): - # Literals may reference symbols in their precision - if isinstance(literal.datatype.precision, Symbol): - all_symbols.add(literal.datatype.precision) + for symbol in scope.symbol_table.datasymbols: + if symbol.initial_value: + init_exprns.append(symbol.initial_value) + for tree in init_exprns + [code_to_inline]: + for reference in tree.walk(Reference): + all_symbols.add(reference.symbol) + for literal in tree.walk(Literal): + # Literals may reference symbols in their precision + if isinstance(literal.datatype.precision, Symbol): + all_symbols.add(literal.datatype.precision) for caller in code_to_inline.walk(Call): all_symbols.add(caller.routine.symbol) for cblock in code_to_inline.walk(CodeBlock): @@ -465,6 +470,19 @@ def apply(self, node, options=None): node.module_inline = True return + if local_sym and local_sym.is_import: + # Double check that this import is not shadowing a routine we've + # already module-inlined. + table = local_sym.find_symbol_table(node) + outer_sym = table.node.scope.symbol_table.lookup(local_sym.name, + otherwise=None) + if outer_sym: + # It is shadowing an outer symbol so we need to remove this + # local symbol and update the call to point to the outer one. + self._rm_imported_symbol(local_sym.name, table) + node.routine.symbol = outer_sym + return + updated_routines = self._prepare_code_to_inline(codes_to_inline) # Update the Kernel to point to the updated PSyIR. if isinstance(node, CodedKern): From bc31ad77ef74c1544887128d184d9f3785aeabd1 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 2 Dec 2024 16:51:38 +0000 Subject: [PATCH 048/100] #2716 tidy after merge --- src/psyclone/psyir/nodes/call.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 9089a1a648..32be56606b 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -37,10 +37,11 @@ ''' This module contains the Call node implementation.''' from collections.abc import Iterable +from typing import List from psyclone.configuration import Config from psyclone.core import AccessType -from psyclone.errors import GenerationError +from psyclone.errors import GenerationError, PSycloneError from psyclone.psyir.nodes.container import Container from psyclone.psyir.nodes.statement import Statement from psyclone.psyir.nodes.datanode import DataNode @@ -54,8 +55,6 @@ UnsupportedFortranType, DataSymbol, ) -from typing import List -from psyclone.errors import PSycloneError class CallMatchingArgumentsNotFound(PSycloneError): From 6b1af690328c74414ed5b1c01fff1d37f02a22c5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 2 Dec 2024 17:15:29 +0000 Subject: [PATCH 049/100] #2716 fix test failures after merge --- .../common/transformations/kernel_module_inline_trans.py | 7 +++++-- .../psyir/transformations/kernel_transformation_test.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 0bcdb81561..f245f26b3d 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -474,8 +474,11 @@ def apply(self, node, options=None): # Double check that this import is not shadowing a routine we've # already module-inlined. table = local_sym.find_symbol_table(node) - outer_sym = table.node.scope.symbol_table.lookup(local_sym.name, - otherwise=None) + outer_sym = None + if table.node.parent: + outer_table = table.node.parent.scope.symbol_table + outer_sym = outer_table.lookup(local_sym.name, + otherwise=None) if outer_sym: # It is shadowing an outer symbol so we need to remove this # local symbol and update the call to point to the outer one. diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index a6b0bebe44..bc51f7e27d 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -297,10 +297,10 @@ def test_transform_kern_with_interface(kernel_outputdir): assert "subroutine mixed_code_32" in contents assert "subroutine mixed_code_64" in contents # But they have been transformed. - assert ('''real*4, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op + assert ('''real*4, dimension(op_ncell_3d,ndf_w0,ndf_w0), intent(in) :: op !$acc routine seq''' in contents) - assert ('''real*8, dimension(ndf_w0,ndf_w0,op_ncell_3d), intent(in) :: op + assert ('''real*8, dimension(op_ncell_3d,ndf_w0,ndf_w0), intent(in) :: op !$acc routine seq''' in contents) assert LFRicBuild(kernel_outputdir).code_compiles(psy) From f1dc4470719b26ab3993e64f8a87a00d603fc548 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 10 Dec 2024 17:21:52 +0000 Subject: [PATCH 050/100] #2716 tidy updated gpu_offloading.py to fix linting errors --- examples/lfric/scripts/gpu_offloading.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 6965a879ac..f6021c22c1 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -71,7 +71,7 @@ def _inline_calls(kern): ''' - Recursively inline all calls. + Recursively inline all calls within the supplied Kernel or Routine. :param kern: the Kernel or Routine to inline any Calls into. @@ -126,7 +126,6 @@ def trans(psyir): const = LFRicConstants() cpu_parallel = OMPParallelTrans() mod_inline_trans = KernelModuleInlineTrans() - intrans = InlineTrans() if OFFLOAD_DIRECTIVES == "omp": # Use OpenMP offloading @@ -192,12 +191,13 @@ def trans(psyir): try: mod_inline_trans.apply(kern) _inline_calls(kern) - #try: - # kern.lower_to_language_level() - # intrans.apply(kern) - #except TransformationError as err: - # print(f"Failed to inline kernel '{kern.name}' " - # f"due to:\n{err.value}") + # At this point we would like to fully inline the + # kernel but InlineTrans does not accept a + # CodedKern. If we lower this kernel first then we + # get errors later (at code-generation time). + # Hopefully this will be resolved when we move + # LFRic to use the PSyIR backend for code + # generation. except TransformationError as err: failed_inline.add(kern.name.lower()) print(f"Failed to module-inline kernel " From 953931a27f950814e173abe604ecd94d34172df6 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 13 Mar 2025 10:27:44 +0000 Subject: [PATCH 051/100] #2716 fix merging errors --- .../transformations/kernel_module_inline_trans.py | 4 +--- src/psyclone/psyir/nodes/intrinsic_call.py | 1 + .../transformations/kernel_module_inline_trans_test.py | 10 ++++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index eb3751bc45..f4f2cca30e 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -44,12 +44,10 @@ ''' from psyclone.core import VariablesAccessInfo -from psyclone.errors import InternalError from psyclone.psyGen import Transformation, CodedKern from psyclone.psyir.transformations import TransformationError from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, DefaultModuleInterface, - RoutineSymbol, Symbol) + ContainerSymbol, DataSymbol, DataTypeSymbol, RoutineSymbol, Symbol) from psyclone.psyir.nodes import ( Container, Reference, Routine, ScopingNode, Literal, CodeBlock, Call, IntrinsicCall) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index bf925c829b..9e4b54a8f0 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -807,6 +807,7 @@ def is_available_on_device(self): IntrinsicCall.Intrinsic.PRODUCT, IntrinsicCall.Intrinsic.SIZE, IntrinsicCall.Intrinsic.SUM, IntrinsicCall.Intrinsic.LBOUND, IntrinsicCall.Intrinsic.MAXVAL, IntrinsicCall.Intrinsic.MINVAL, + IntrinsicCall.Intrinsic.MATMUL, IntrinsicCall.Intrinsic.TINY, IntrinsicCall.Intrinsic.HUGE) @classmethod diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index fa887a58ad..d75762734b 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -180,9 +180,11 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - kernels[0].get_kernel_schedule().pop_all_children() - kernels[0].get_kernel_schedule().addchild(block) - table = kernels[0].get_kernel_schedule().symbol_table + _, kschedules = kernels[0].get_kernel_schedule() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild(block) + table = ksched.symbol_table # Remove symbols that refer to 'go_wp' in outer scope. table._symbols.pop("field_old") table._symbols.pop("field_new") @@ -435,7 +437,7 @@ def test_validate_nested_scopes(fortran_reader, monkeypatch): # Put a new, different symbol (with the same name) into the table of the # parent Container. routine.parent.scope.symbol_table.add(DataSymbol("a", REAL_TYPE)) - monkeypatch.setattr(kern_call, "_kern_schedule", routine) + monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) # The transformation should succeed (because the symbol named 'a' is # actually local to the routine. However, the dependence analysis thinks it From 49b4850dde92f3ecb1a655c7fdcf480d33b7e9ef Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 13 Mar 2025 15:18:37 +0000 Subject: [PATCH 052/100] #2716 improve symbol search in validate() to allow for nested scopes --- .../kernel_module_inline_trans.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index f4f2cca30e..1395062aa2 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -47,7 +47,8 @@ from psyclone.psyGen import Transformation, CodedKern from psyclone.psyir.transformations import TransformationError from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, RoutineSymbol, Symbol) + ContainerSymbol, DataSymbol, DataTypeSymbol, RoutineSymbol, Symbol, + SymbolError) from psyclone.psyir.nodes import ( Container, Reference, Routine, ScopingNode, Literal, CodeBlock, Call, IntrinsicCall) @@ -175,19 +176,33 @@ def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): # different symbols with the same name but declared in different, # nested scopes will be assumed to be the same symbol). vai = VariablesAccessInfo(kernel_schedule) - table = kernel_schedule.symbol_table + rt_table = kernel_schedule.symbol_table for sig in vai.all_signatures: - symbol = table.lookup(sig.var_name, otherwise=None) + access = vai[sig].all_accesses[0] + try: + # The 'node' associated with an access may be a Symbol (if + # the access is part of a symbol definition) or an + # orphaned Node (e.g. within an initialisation expression). + table = access.node.scope.symbol_table + except (SymbolError, AttributeError): + table = rt_table + symbol = table.lookup(sig.var_name, otherwise=None, + scope_limit=kernel_schedule) if not symbol: + # The corresponding Symbol was not found within the scope + # of the target Routine. + outer_sym = kernel_schedule.symbol_table.lookup(sig.var_name, + otherwise=None) + if outer_sym and outer_sym.is_modulevar: + raise TransformationError( + f"{kern_or_call} '{kname}' contains accesses to " + f"'{symbol.name}' which is declared in the callee " + f"module scope. Cannot inline such a {kern_or_call}.") + raise TransformationError( f"{kern_or_call} '{kname}' contains accesses to " f"'{sig.var_name}' but the origin of this signature is " f"unknown.") - if not symbol.is_import and symbol.name not in table: - raise TransformationError( - f"{kern_or_call} '{kname}' contains accesses to " - f"'{symbol.name}' which is declared in the callee " - f"module scope. Cannot inline such a {kern_or_call}.") # We can't transform subroutines that shadow top-level symbol module # names, because we won't be able to bring this into the subroutine From d7177a2d3e8e9e0db3f7ae74d3c2c54bd450401d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 13 Mar 2025 17:57:01 +0000 Subject: [PATCH 053/100] #2716 fix error in error message --- .../domain/common/transformations/kernel_module_inline_trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 1395062aa2..4ecba24b7f 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -196,7 +196,7 @@ def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): if outer_sym and outer_sym.is_modulevar: raise TransformationError( f"{kern_or_call} '{kname}' contains accesses to " - f"'{symbol.name}' which is declared in the callee " + f"'{outer_sym.name}' which is declared in the callee " f"module scope. Cannot inline such a {kern_or_call}.") raise TransformationError( From 421f20d8974648ef48492188c567fcbedb1ab031 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 08:28:40 +0000 Subject: [PATCH 054/100] #2716 WIP fixing tests [skip ci] --- .../transformations/kernel_module_inline_trans.py | 3 +-- .../transformations/kernel_module_inline_trans_test.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 659cd457a1..ea9b8eb676 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -119,8 +119,6 @@ def validate(self, node, options=None): f"psyGen.CodedKern or psyir.nodes.Call but got " f"'{type(node).__name__}'") - parent_container = node.ancestor(Container) - # Check that the PSyIR of the routine/kernel can be retrieved. try: kernels, _ = KernelModuleInlineTrans._get_psyir_to_inline(node) @@ -190,6 +188,7 @@ def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): # any existing Routine matches that required by the Call but for now # we live with the possibility of a false positive resulting in a # refusal to module inline. + parent_container = node.ancestor(Container) for routine in parent_container.walk(Routine, stop_type=Routine): if routine.name.lower() == kname.lower(): # Compare the routine to be inlined with the one that diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index dec04bdf54..c297f5f189 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -39,7 +39,6 @@ ''' Tests of the KernelModuleInlineTrans PSyIR transformation. ''' -import os import pytest from fparser.common.readfortran import FortranStringReader from psyclone.configuration import Config @@ -51,7 +50,7 @@ ContainerSymbol, DataSymbol, ImportInterface, RoutineSymbol, REAL_TYPE, Symbol, SymbolError, SymbolTable) from psyclone.psyir.transformations import TransformationError -from psyclone.transformations import OMPDeclareTargetTrans +from psyclone.transformations import ACCRoutineTrans, OMPDeclareTargetTrans from psyclone.tests.gocean_build import GOceanBuild from psyclone.tests.lfric_build import LFRicBuild from psyclone.tests.utilities import (Compile, count_lines, get_invoke, @@ -75,8 +74,9 @@ def test_check_data_accesses(config_instance): schedule = invoke.schedule kcall = schedule.walk(CodedKern)[1] config_instance.include_paths = [] + _, kschedules = kcall.get_kernel_schedule() with pytest.raises(TransformationError) as err: - trans.check_data_accesses(kcall, kcall.get_kernel_schedule(), "Kernel") + trans.check_data_accesses(kcall, kschedules[0], "Kernel") assert ("Kernel 'kernel_with_use2_code' contains accesses to 'go_wp' which" " is unresolved. It is being brought into scope from one of " "['argument_mod', 'grid_mod', 'kernel_mod', 'kind_params_mod']" @@ -84,7 +84,7 @@ def test_check_data_accesses(config_instance): # Now try where there's only a single wildcard import so we know the origin # of the symbol. kcall0 = schedule.walk(CodedKern)[0] - ksched = kcall0.get_kernel_schedule() + _, (ksched,) = kcall0.get_kernel_schedule() ctable = ksched.ancestor(Container).symbol_table # To do this, we manually remove all ContainerSymbols apart from the one # from which 'go_wp' is imported. @@ -108,7 +108,7 @@ def test_check_data_accesses_indirect_import(monkeypatch): "gocean", idx=0, dist_mem=False) schedule = invoke.schedule kcall = schedule.walk(CodedKern)[1] - ksched = kcall.get_kernel_schedule() + _, (ksched,) = kcall.get_kernel_schedule() # Monkeypatch SymbolTable.resolve_imports() so that it does nothing. This # then exercises the code path where we quietly fail to resolve a symbol. monkeypatch.setattr(ksched.symbol_table, "resolve_imports", From ba4a2f2fa06ee375951ccbfefd3f3a8a76f2ac37 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 08:33:59 +0000 Subject: [PATCH 055/100] #2716 fix linting [skip ci] --- .../domain/common/transformations/kernel_module_inline_trans.py | 1 - src/psyclone/transformations.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index ea9b8eb676..f6409339a3 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -519,7 +519,6 @@ def _rename_import(table, csym, name): interface=ImportInterface( csym, orig_name=name)) - def apply(self, node, options=None): ''' Bring the kernel/subroutine into this Container. diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 96be8c5c25..e92a8bebf8 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -59,7 +59,7 @@ from psyclone.psyir.nodes import ( ACCDataDirective, ACCDirective, ACCEnterDataDirective, ACCKernelsDirective, ACCLoopDirective, ACCParallelDirective, ACCRoutineDirective, - Call, CodeBlock, Container, Directive, Literal, Loop, Node, + Call, CodeBlock, Directive, Literal, Loop, Node, OMPDeclareTargetDirective, OMPDirective, OMPMasterDirective, OMPParallelDirective, OMPParallelDoDirective, OMPSerialDirective, OMPSingleDirective, OMPTaskloopDirective, PSyDataNode, Reference, From 7192011d36870c94e47c1f4e658247d26b4d49a4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 12:25:14 +0000 Subject: [PATCH 056/100] #2716 WIP fixing merge [skip ci] --- .../kernel_module_inline_trans.py | 121 +++++++++--------- .../kernel_module_inline_trans_test.py | 55 +------- 2 files changed, 63 insertions(+), 113 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index f6409339a3..86302fc3c2 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -174,9 +174,7 @@ def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): # to a Routine existing_symbol = node.scope.symbol_table.lookup(kernel_schedule.name, otherwise=None) - if not existing_symbol: - return - if not isinstance(existing_symbol, RoutineSymbol): + if existing_symbol and not isinstance(existing_symbol, RoutineSymbol): raise TransformationError( f"Cannot module-inline {kern_or_call} '{kname}' because " f"symbol '{existing_symbol}' with the same name already " @@ -417,39 +415,6 @@ def _get_psyir_to_inline(node): return (routines, interface_sym) - @staticmethod - def _rm_imported_symbol(name, table): - ''' - If the named symbol is in the supplied table (or an ancestor) *and* is - an import then it is removed. If the Container from which it was being - imported no longer has any imports associated with it then the - ContainerSymbol is also removed. - - :param str name: the name of the symbol to remove. - :param table: the symbol table from which to search for the symbol. - :type table: :py:class:`psyclone.psyir.symbols.SymbolTable` - - ''' - symbol = table.lookup(name, otherwise=None) - if not symbol or not symbol.is_import: - return - - # The RoutineSymbol is in the table (or an outer scope) and is - # imported. We therefore remove it and potentially the ContainerSymbol - # from which it is imported. - csym = symbol.interface.container_symbol - - actual_table = (symbol.find_symbol_table(table.node) if - symbol.name not in table else table) - remove_csym = actual_table.symbols_imported_from(csym) == [symbol] - # We have to force the removal as there will be calls that reference - # this Symbol. (These calls will subsequently be updated to refer to - # the Symbol of the inlined routine.) - # pylint:disable-next=protected-access - actual_table._symbols.pop(symbol.name) - if remove_csym: - actual_table.remove(csym) - @staticmethod def _rm_imported_routine_symbol(name, table): ''' @@ -483,6 +448,10 @@ def _rm_imported_routine_symbol(name, table): # The Routine is brought into scope via a wildcard import. We have # to rename it on import to avoid a clash with the newly inlined # Routine. + # TODO #2846 - if the same Routine is also brought into scope + # through some other wildcard import then this renaming doesn't + # help. The only solution to this is to rename the module-inlined + # Routine (or proceed to fully inline it). KernelModuleInlineTrans._rename_import(ctable, csym, symbol.name) # pylint:disable-next=protected-access actual_table._symbols.pop(symbol.name) @@ -533,12 +502,6 @@ def apply(self, node, options=None): :param options: a dictionary with options for transformations. :type options: Optional[Dict[str, Any]] - :raises TransformationError: if the called Routine cannot be brought - into this Container because of a name clash with another Routine. - :raises NotImplementedError: if node is a Call (rather than a - CodedKern) and the name of the called routine does not match that - of the caller. - ''' if isinstance(node, CodedKern) and node.module_inline: # This PSyKal Kernel is already module inlined. @@ -571,6 +534,8 @@ def apply(self, node, options=None): return else: for routine in codes_to_inline: + # N.B.in a PSyKAl DSL, we won't have a RoutineSymbol for the + # Kernel that is being called. local_sym = local_table.lookup(routine.symbol.name, otherwise=None) if (not local_sym or local_sym is not routine.symbol or @@ -580,24 +545,49 @@ def apply(self, node, options=None): else: # All routines are module-inlined so there's nothing to do. # TODO #11 - log this. + if isinstance(node, CodedKern): + node.module_inline = True return + # Deal with the RoutineSymbol that is in scope at the call site. + sym_in_ctr = None if local_sym and (local_sym.is_import or local_sym.is_unresolved): + local_table = local_sym.find_symbol_table(node) + # It may be that the RoutineSymbol is in fact only declared + # in the Container. If that's the case then we need to keep a + # reference to it so that we can update other Calls to it at the + # end of this method. + if isinstance(local_table.node, Container): + sym_in_ctr = local_sym + if local_sym.is_unresolved: + # If it's currently unresolved then we first update its + # interface prior to removing it. + cntr = codes_to_inline[0].ancestor(Container, + excluding=FileContainer) + if cntr: + cntr_name = cntr.name + cntr_sym = local_table.lookup(cntr_name) + # Now we have a ContainerSymbol, we can change the + # interface of sym_in_ctr and proceed in exactly the + # same way as if it had been resolved originally. + local_sym.interface = ImportInterface(cntr_sym) + + # We need to remove this local symbol. + self._rm_imported_routine_symbol(local_sym.name, local_table) # Double check that this import is not shadowing a routine we've # already module-inlined. - table = local_sym.find_symbol_table(node) outer_sym = None - if table.node.parent: - outer_table = table.node.parent.scope.symbol_table + if local_table.node.parent: + outer_table = local_table.node.parent.scope.symbol_table outer_sym = outer_table.lookup(local_sym.name, otherwise=None) if outer_sym: - outer_table = outer_sym.find_symbol_table(table.node.parent) + outer_table = outer_sym.find_symbol_table( + local_table.node.parent) if not isinstance(outer_table.node, FileContainer): # It is shadowing an outer symbol that is in a Container - # (not a FileContainer) so we need to remove this local - # symbol and update the call to point to the outer one. - self._rm_imported_routine_symbol(local_sym.name, table) + # (not a FileContainer) so we just need to update the call + # to point to the outer symbol. node.routine.symbol = outer_sym if not (outer_sym.is_import or outer_sym.is_unresolved): # The outer symbol is local to this Container so @@ -607,7 +597,6 @@ def apply(self, node, options=None): updated_routines = self._prepare_code_to_inline(codes_to_inline) # Update the Kernel to point to the updated PSyIR. if isinstance(node, CodedKern): - # TODO - add setter for these properties to Kern? # pylint: disable=protected-access node._kern_schedules = updated_routines if interface_sym: @@ -621,19 +610,24 @@ def apply(self, node, options=None): for code_to_inline in updated_routines: # Does the Container already have this Routine? - sym_in_ctr = container.symbol_table.lookup(code_to_inline.name, - scope_limit=container, - otherwise=None) + if not sym_in_ctr: + # We only update sym_in_ctr if it hasn't already been set + # earlier when updating the 'local' symbol. + sym_in_ctr = container.symbol_table.lookup( + code_to_inline.name, + scope_limit=container, + otherwise=None) + if not sym_in_ctr: # If it doesn't exist already, module-inline the subroutine by # inserting the relevant code into the tree. # We need to set the visibility of the routine's symbol to # be private. - code_to_inline.symbol.visibility = Symbol.Visibility.PRIVATE + sym = code_to_inline.symbol + sym.visibility = Symbol.Visibility.PRIVATE container.addchild(code_to_inline.detach()) - continue - if sym_in_ctr.is_import: + elif sym_in_ctr.is_import: # The RoutineSymbol is imported into the table. We must # therefore update its interface and potentially remove the # ContainerSymbol (from which it is imported) altogether. @@ -652,6 +646,8 @@ def apply(self, node, options=None): cntr = code_to_inline.ancestor(Container, excluding=FileContainer) if cntr: + # The symbol comes from a Container (not a FileContainer) + # and so needs an import interface. cntr_name = cntr.name cntr_sym = container.symbol_table.lookup(cntr_name) # Now we have a ContainerSymbol, we can change the @@ -666,15 +662,11 @@ def apply(self, node, options=None): container.addchild(code_to_inline) sym = container.symbol_table.lookup(code_to_inline.name) sym.visibility = Symbol.Visibility.PRIVATE - else: - # The routine is in the FileContainer containing - # the callsite so is not imported from a Container. - pass else: # The Routine is present in the Container. sym = sym_in_ctr - # All Calls in the same scope to a routine of the same - # name must refer to the same Symbol. + # All Calls to a routine of the same name in the same scope as the + # target node must refer to the same Symbol. target_name = sym.name.lower() for call in node.ancestor(Routine).walk(Call): name = call.routine.symbol.name.lower() @@ -686,7 +678,7 @@ def apply(self, node, options=None): call.routine.symbol = sym if interface_sym: - self._rm_imported_symbol(interface_sym.name, local_table) + self._rm_imported_routine_symbol(interface_sym.name, local_table) if interface_sym.name not in container.symbol_table: container.symbol_table.add(interface_sym) interface_sym.replace_symbols_using(container.symbol_table) @@ -695,4 +687,5 @@ def apply(self, node, options=None): # TODO #1823. If the kernel imports were generated at PSy-layer # creation time, we could just remove it here instead of setting a # flag. - node.module_inline = True + if isinstance(node, CodedKern): + node.module_inline = True diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index c297f5f189..22b69fab06 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -476,28 +476,6 @@ def test_validate_fail_to_get_psyir(fortran_reader, config_instance): in str(err.value)) -def test_rm_imported_symbol(): - ''' - Tests for the _rm_imported_symbol() utility method. - - ''' - table = SymbolTable() - csym = ContainerSymbol("ankh") - table.add(csym) - moist_sym = DataSymbol("moist", REAL_TYPE, - interface=ImportInterface(csym)) - table.add(moist_sym) - local_sym = DataSymbol("local", REAL_TYPE) - table.add(local_sym) - KernelModuleInlineTrans._rm_imported_symbol("moist", table) - assert "moist" not in table - # Container has been removed too. - assert "ankh" not in table - # If the symbol is not imported then it is left unchanged. - KernelModuleInlineTrans._rm_imported_symbol("local", table) - assert "local" in table - - def test_validate_nested_scopes(fortran_reader, monkeypatch): ''' Test that validate() works correctly when two symbols in nested scopes @@ -994,28 +972,6 @@ def test_module_inline_with_interfaces(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_get_psyir_to_inline(monkeypatch): - ''' - Test that _get_psyir_to_inline() raises the expected error if more than - one potential routine implementation is found. - - ''' - sym = RoutineSymbol("my_sym") - rout = Routine.create("my_sym", SymbolTable(), []) - node = Call.create(sym) - # For simplicity we just monkeypatch Call.get_callees() so that it appears - # to return more than one Routine. - monkeypatch.setattr(node, "get_callees", lambda: [rout, rout]) - with pytest.raises(TransformationError) as err: - KernelModuleInlineTrans._get_psyir_to_inline(node) - # The duplicated symbol name below is purely a result of the monkeypatch - # - in reality these names will come from a generic interface and be - # different. - assert ("The target of the call to 'my_sym' cannot be inserted because " - "multiple implementations were found: ['my_sym', 'my_sym']." in - str(err.value)) - - def test_rm_imported_routine_symbol(): ''' Tests for the _rm_imported_routine_symbol() utility method. @@ -1052,11 +1008,12 @@ def test_rm_imported_routine_symbol(): assert len(table.symbols_imported_from(csym)) == 1 -@pytest.mark.parametrize("mod_use, sub_use", - [("use my_mod, only: my_sub, my_other_sub", ""), - ("", "use my_mod, only: my_sub, my_other_sub"), - ("use my_mod, only: my_sub, my_other_sub", - "use my_mod, only: my_sub, my_other_sub")]) +@pytest.mark.parametrize( + "mod_use, sub_use", + [("use my_mod, only: my_sub, my_other_sub, my_interface", ""), + ("", "use my_mod, only: my_sub, my_other_sub, my_interface"), + ("use my_mod, only: my_sub, my_other_sub, my_interface", + "use my_mod, only: my_sub, my_other_sub, my_interface")]) @pytest.mark.usefixtures("clear_module_manager_instance") def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, monkeypatch, mod_use, sub_use): From 1bcd8d7754874d1c0ff8613613b79ff0874d72d5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 12:46:16 +0000 Subject: [PATCH 057/100] #2716 fix KernelModuleInlineTrans tests [skip ci] --- .../common/transformations/kernel_module_inline_trans.py | 7 ++++--- .../transformations/kernel_module_inline_trans_test.py | 9 ++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 86302fc3c2..6e1002006c 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -561,7 +561,7 @@ def apply(self, node, options=None): sym_in_ctr = local_sym if local_sym.is_unresolved: # If it's currently unresolved then we first update its - # interface prior to removing it. + # interface to simplify removing it. cntr = codes_to_inline[0].ancestor(Container, excluding=FileContainer) if cntr: @@ -610,9 +610,10 @@ def apply(self, node, options=None): for code_to_inline in updated_routines: # Does the Container already have this Routine? - if not sym_in_ctr: + if not sym_in_ctr or interface_sym: # We only update sym_in_ctr if it hasn't already been set - # earlier when updating the 'local' symbol. + # earlier when updating the 'local' symbol or if that symbol + # was in fact an interface. sym_in_ctr = container.symbol_table.lookup( code_to_inline.name, scope_limit=container, diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 22b69fab06..c4723b2573 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -1071,7 +1071,14 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, assert "subroutine a_sub" in output assert "subroutine my_sub" in output assert "use my_mod, only : my_interface, my_other_sub\n" in output - # We can't test the compilation of this code because of the 'use my_mod.' + + # Module inline the target of the second call. + intrans.apply(calls[1]) + # Local copy of routine must be private and in Container symbol table. + rsym = container.symbol_table.lookup("my_other_sub") + assert rsym.visibility == Symbol.Visibility.PRIVATE + output = fortran_writer(psyir) + assert "use my_mod, only : my_interface\n" in output # Finally, inline the call to the interface. This should then remove all # imports from 'my_mod'. From 770ced28b065335407be65273deb3ac5aac01daf Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 15:05:02 +0000 Subject: [PATCH 058/100] #2716 fix all tests --- src/psyclone/psyir/nodes/call.py | 50 +++++++++++++------ src/psyclone/psyir/nodes/container.py | 11 ++-- .../psyir/transformations/omp_task_trans.py | 4 +- src/psyclone/tests/psyir/nodes/call_test.py | 49 ++---------------- 4 files changed, 52 insertions(+), 62 deletions(-) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 8761b90a75..94cf9fc808 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -567,22 +567,47 @@ def _location_txt(node): return f"code:\n'{out_lines[0]}\n...\n{last_line}'" rsym = self.routine.symbol - if rsym.is_unresolved: + if rsym.is_unresolved or rsym.is_modulevar: # Check for any "raw" Routines, i.e. ones that are not # in a Container. Such Routines would exist in the PSyIR # as a child of a FileContainer (if the PSyIR contains a # FileContainer). Note, if the PSyIR does contain a # FileContainer, it will be the root node of the PSyIR. - psyir = self.root.find_routine_psyir(rsym.name) - if psyir: - return [psyir] + container = None + if rsym.is_modulevar: + # Take care here in case the Routine is an orphan. + table = rsym.find_symbol_table(self) + if table: + container = table.node + else: + container = self.root + if container: + routines = [] + for name in container.resolve_routine(rsym.name): + # Since we're looking in the local Container, the target + # is permitted to be private. + psyir = container.find_routine_psyir(name, + allow_private=True) + if psyir: + routines.append(psyir) + if routines: + return routines + + if rsym.is_modulevar: + root_node = container if container else self.root + raise SymbolError( + f"The RoutineSymbol for Routine '{rsym.name}' is " + f"marked as being local but failed to find the " + f"corresponding implementation in " + f"{_location_txt(root_node)}") # Now check for any wildcard imports and see if they can # be used to resolve the symbol. wildcard_names = [] containers_not_found = [] current_table = self.scope.symbol_table + callee_name = rsym.name.lower() while current_table: for container_symbol in current_table.containersymbols: if container_symbol.wildcard_import: @@ -598,7 +623,13 @@ def _location_txt(node): continue routines = [] for name in container.resolve_routine(rsym.name): - psyir = container.find_routine_psyir(name) + # If 'name' doesn't match callee_name then we are + # dealing with an interface. Routines referenced + # by an interface may be private to the host + # container. + psyir = container.find_routine_psyir( + name, + allow_private=(name.lower() != callee_name)) if psyir: routines.append(psyir) if routines: @@ -669,15 +700,6 @@ def _location_txt(node): rsym = cursor root_node = container - if isinstance(rsym.datatype, UnsupportedFortranType): - # TODO #924 - an UnsupportedFortranType here typically indicates - # that the target is actually an interface. - raise NotImplementedError( - f"RoutineSymbol '{rsym.name}' exists in " - f"{_location_txt(root_node)} but is of " - f"UnsupportedFortranType:\n{rsym.datatype.declaration}\n" - f"Cannot get the PSyIR of such a routine.") - # At this point, we should have found the PSyIR tree containing the # routine - we just need to locate it. It may be in a Container or # it may be in the parent FileContainer. diff --git a/src/psyclone/psyir/nodes/container.py b/src/psyclone/psyir/nodes/container.py index 60d6efcd82..e9891d2afc 100644 --- a/src/psyclone/psyir/nodes/container.py +++ b/src/psyclone/psyir/nodes/container.py @@ -302,9 +302,14 @@ def resolve_routine(self, name): :raises TypeError: if the Symbol with the supplied name is not a RoutineSymbol, GenericInterfaceSymbol or imported Symbol. ''' - try: - rsym = self.symbol_table.lookup(name) - except KeyError: + rsym = self.symbol_table.lookup(name, otherwise=None) + if not rsym: + # TODO for some reason, a module-inlined KernelSchedule does not + # have a corresponding entry in a symbol table so we double check + # here... + for routine in self.walk(Routine): + if routine.name.lower() == name.lower(): + return [name] return [] if isinstance(rsym, GenericInterfaceSymbol): return [rt.symbol.name.lower() for rt in rsym.routines] diff --git a/src/psyclone/psyir/transformations/omp_task_trans.py b/src/psyclone/psyir/transformations/omp_task_trans.py index 1e67ea4081..e483752eb7 100644 --- a/src/psyclone/psyir/transformations/omp_task_trans.py +++ b/src/psyclone/psyir/transformations/omp_task_trans.py @@ -161,7 +161,9 @@ def _inline_kernels(self, node): intrans = InlineTrans() for kern in kerns: kintrans.apply(kern) - cond_trans.apply(kern.get_kernel_schedule()) + _, schedules = kern.get_kernel_schedule() + for sched in schedules: + cond_trans.apply(sched) kern.lower_to_language_level() calls = node.walk(Call) diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index bc7e3a0135..a80e95c459 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -1458,47 +1458,6 @@ def test_call_get_callees_interface(fortran_reader): assert callees[1].name == "ibottom" -def test_call_get_callees_unsupported_type(fortran_reader): - ''' - Check that get_callees() raises the expected error when the called routine - is of UnsupportedFortranType. This is hard to achieve so we have to - manually construct some aspects of the test case. - - ''' - code = ''' -module my_mod - integer, target :: value -contains - subroutine top() - integer :: luggage - luggage = bottom() - end subroutine top - function bottom() result(fval) - integer, pointer :: fval - fval => value - end function bottom -end module my_mod -''' - psyir = fortran_reader.psyir_from_source(code) - container = psyir.children[0] - routine = container.find_routine_psyir("bottom") - rsym = container.symbol_table.lookup(routine.name) - # Ensure the type of this RoutineSymbol is UnsupportedFortranType. - rsym.datatype = UnsupportedFortranType("integer, pointer :: fval") - assign = container.walk(Assignment)[0] - # Currently `bottom()` gets matched by fparser2 as a structure constructor - # and the fparser2 frontend leaves this as a CodeBlock (TODO #2429) so - # replace it with a Call. Once #2429 is fixed the next two lines can be - # removed. - assert isinstance(assign.rhs, CodeBlock) - assign.rhs.replace_with(Call.create(rsym)) - call = psyir.walk(Call)[0] - with pytest.raises(NotImplementedError) as err: - _ = call.get_callees() - assert ("RoutineSymbol 'bottom' exists in Container 'my_mod' but is of " - "UnsupportedFortranType" in str(err.value)) - - def test_call_get_callees_file_container(fortran_reader): ''' Check that get_callees works if the called routine happens to be in file @@ -1554,8 +1513,9 @@ def test_call_get_callees_no_container(fortran_reader): call = top_routine.walk(Call)[0] with pytest.raises(SymbolError) as err: _ = call.get_callees() - assert ("Failed to find a Routine named 'bottom' in code:\n'subroutine " - "top()" in str(err.value)) + assert ("The RoutineSymbol for Routine 'bottom' is marked as being local " + "but failed to find the corresponding implementation in code:\n'" + "subroutine top()" in str(err.value)) def test_call_get_callees_wildcard_import_local_container(fortran_reader): @@ -1758,7 +1718,8 @@ def test_get_callees_code_block(fortran_reader): call = psyir.walk(Call)[1] with pytest.raises(SymbolError) as err: _ = call.get_callees() - assert ("Failed to find a Routine named 'my_func' in Container " + assert ("The RoutineSymbol for Routine 'my_func' is marked as being local " + "but failed to find the corresponding implementation in Container " "'some_mod'" in str(err.value)) From 1c9bf81ce238b0ac6f357634be69bd934ebb9bbf Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 16:41:32 +0000 Subject: [PATCH 059/100] #2716 fix linting --- src/psyclone/tests/psyir/nodes/call_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index a80e95c459..ca3575626c 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -43,13 +43,13 @@ from psyclone.errors import GenerationError from psyclone.parse import ModuleManager from psyclone.psyir.nodes import ( - ArrayReference, Assignment, BinaryOperation, Call, CodeBlock, Literal, + ArrayReference, BinaryOperation, Call, Literal, Node, Reference, Routine, Schedule) from psyclone.psyir.nodes.call import CallMatchingArgumentsNotFound from psyclone.psyir.nodes.node import colored from psyclone.psyir.symbols import ( ArrayType, INTEGER_TYPE, DataSymbol, NoType, RoutineSymbol, REAL_TYPE, - SymbolError, UnsupportedFortranType) + SymbolError) class SpecialCall(Call): From d8cac6f4ea314b93a95140e5a64319e9ef74b013 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 14 Mar 2025 20:56:44 +0000 Subject: [PATCH 060/100] #2716 fix all tests --- .../transformations/kernel_module_inline_trans.py | 11 ++++++++--- src/psyclone/psyir/nodes/routine.py | 3 ++- .../tests/psyir/transformations/inline_trans_test.py | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 6e1002006c..3abaa5cd28 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -557,7 +557,8 @@ def apply(self, node, options=None): # in the Container. If that's the case then we need to keep a # reference to it so that we can update other Calls to it at the # end of this method. - if isinstance(local_table.node, Container): + if (isinstance(local_table.node, Container) and + not isinstance(local_table.node, FileContainer)): sym_in_ctr = local_sym if local_sym.is_unresolved: # If it's currently unresolved then we first update its @@ -571,8 +572,12 @@ def apply(self, node, options=None): # interface of sym_in_ctr and proceed in exactly the # same way as if it had been resolved originally. local_sym.interface = ImportInterface(cntr_sym) - - # We need to remove this local symbol. + else: + # The routine to be inlined must be in the outer + # FileContainer so we just need to remove the local + # symbol. + local_table._symbols.pop(local_sym.name) + # We need to remove this (imported) local symbol. self._rm_imported_routine_symbol(local_sym.name, local_table) # Double check that this import is not shadowing a routine we've # already module-inlined. diff --git a/src/psyclone/psyir/nodes/routine.py b/src/psyclone/psyir/nodes/routine.py index 522c70c3cd..f3ec6197a0 100644 --- a/src/psyclone/psyir/nodes/routine.py +++ b/src/psyclone/psyir/nodes/routine.py @@ -268,7 +268,8 @@ def update_parent_symbol_table(self, new_parent): # replace_with, which is handled here. if sym is self._symbol: try: - new_parent.symbol_table.lookup(self._symbol.name) + new_parent.symbol_table.lookup(self._symbol.name, + scope_limit=new_parent) except KeyError: new_parent.symbol_table.add(self._symbol) elif self.symbol_table: diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py index e721b6930d..1eef311f58 100644 --- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py @@ -1549,6 +1549,7 @@ def test_apply_raw_subroutine( if start: modinline_trans = KernelModuleInlineTrans() modinline_trans.apply(call) + assert "sub" in psyir.children[0].symbol_table inline_trans = InlineTrans() inline_trans.apply(call) output = fortran_writer(psyir) From 294aea636ae9d8740cb56cb962b97892db027f13 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 9 Apr 2025 17:09:48 +0100 Subject: [PATCH 061/100] #2716 WIP fixing merged files [skip ci] --- .../kernel_module_inline_trans.py | 15 +- src/psyclone/domain/lfric/lfric_kern.py | 126 +++++++--------- src/psyclone/psyir/nodes/call.py | 17 ++- src/psyclone/psyir/nodes/container.py | 6 +- .../kernel_module_inline_trans_test.py | 22 --- src/psyclone/transformations.py | 137 +++++++++--------- 6 files changed, 146 insertions(+), 177 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index f965f5e183..a0b244c09f 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -457,8 +457,9 @@ def apply(self, node, options=None): callsite_table = node.scope.symbol_table if interface_sym: - local_sym = local_table.lookup(interface_sym.name, otherwise=None) - if interface_sym is local_sym and not local_sym.is_import: + called_sym = callsite_table.lookup(interface_sym.name, + otherwise=None) + if interface_sym is called_sym and not called_sym.is_import: # Interface symbol is already local so nothing to do. if isinstance(node, CodedKern): node.module_inline = True @@ -470,8 +471,8 @@ def apply(self, node, options=None): # N.B.in a PSyKAl DSL, we won't have a RoutineSymbol for the # Kernel that is being called, so we look it up instead of # using node.symbol. - called_sym = local_table.lookup(caller_name, - otherwise=None) + called_sym = callsite_table.lookup(caller_name, + otherwise=None) if (not called_sym or called_sym is not routine.symbol or (called_sym.is_import or called_sym.is_unresolved)): # This routine is not module-inlined. @@ -494,7 +495,7 @@ def apply(self, node, options=None): # update any other Calls to it (at the end of this method). sym_in_ctr = called_sym - self._rm_imported_routine_symbol(called_sym, code_to_inline, + self._rm_imported_routine_symbol(called_sym, codes_to_inline[0], table) # Double check that this import is not shadowing a routine we've @@ -595,7 +596,9 @@ def apply(self, node, options=None): call.routine.symbol = target_sym if interface_sym: - self._rm_imported_routine_symbol(interface_sym.name, local_table) + self._rm_imported_routine_symbol(interface_sym, + codes_to_inline[0], + callsite_table) if interface_sym.name not in container.symbol_table: container.symbol_table.add(interface_sym) interface_sym.replace_symbols_using(container.symbol_table) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index ae4a68bf3c..36aefe9325 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -56,8 +56,8 @@ from psyclone.psyGen import InvokeSchedule, CodedKern, args_filter from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.frontend.fparser2 import Fparser2Reader -from psyclone.psyir.nodes import (Container, Loop, Literal, Reference, - KernelSchedule) +from psyclone.psyir.nodes import (Container, KernelSchedule, + Loop, Literal, Reference) from psyclone.psyir.symbols import (DataSymbol, ScalarType, ArrayType, INTEGER_TYPE) @@ -733,79 +733,59 @@ class creates the PSyIR schedule on first invocation which is # Otherwise, get the PSyIR Kernel Schedule(s) from the original # parse tree. if not routines: - routines = Fparser2Reader().get_routine_schedules(self.name, - self.ast) - new_schedules = [] - for routine in routines[:]: - # If one of the symbols is not declared in a routine then - # this is only picked up when writing out the routine - # (raising a VisitorError), so we check here so that - # invalid code is not inlined. We use debug_string() to - # minimise the overhead. - - # TODO #2271 could potentially avoid the need for - # debug_string() within. Sergi suggests that we may be - # missing the traversal of the declaration init - # expressions and that might solve the problem. I'm not so - # sure as we are talking about unknown symbols that will - # only be resolved in the back-end (or not). If I am right - # then one option would be to use the FortranWriter, but - # that would be bigger overhead, or perhaps just the - # declarations part of FortranWriter if that is possible. - # Also see TODO issue #2336 which captures the specific - # problem in LFRic that this fixes. - routine.debug_string() - - # TODO #935 - replace the PSyIR argument data symbols with - # LFRic data symbols. For the moment we just return the - # unmodified PSyIR schedule but this should use - # RaisePSyIR2LFRicKernTrans once KernelInterface is fully - # functional (#928). - ksched = KernelSchedule( - routine.symbol, symbol_table=routine.symbol_table.detach()) - for child in routine.pop_all_children(): - ksched.addchild(child) - routine.replace_with(ksched) - new_schedules.append(ksched) - routines = new_schedules + orig_psyir = Fparser2Reader().generate_psyir(self.ast) + for container in orig_psyir.walk(Container): + names = container.resolve_routine(self.name) + routines = [] + can_be_private = len(names) > 1 + for name in names: + rt_psyir = container.find_routine_psyir( + name, allow_private=can_be_private) + if rt_psyir: + routines.append(rt_psyir) + if routines: + break + if not routines: + raise InternalError("oh dear") + self._interface_symbol = None if len(routines) > 1: - table = routines[0].scope.symbol_table - sym = table.lookup(self.name) - else: - # The kernel name corresponds to an interface block. Find which - # of the routines matches the precision of the arguments. - matched_routines = [] - for routine in routines: - try: - # The validity check for the kernel arguments will raise - # an exception if the precisions don't match. - self.validate_kernel_code_args(routine.symbol_table) - # TODO #2716 - this code will be reworked. - matched_routines.append(routine) - except GenerationError: - pass - if not matched_routines: - raise GenerationError( - f"Failed to find a kernel implementation with an interface" - f" that matches the invoke of '{self.name}'. (Tried " - f"routines {[item.name for item in routines]}.)") - if len(matched_routines) > 1: - raise GenerationError( - f"Found multiple kernel implementations (" - f"{[rt.name for rt in matched_routines]}) that apparently " - f"match the interface of this call to '{self.name}'. This " - f"is a known bug - TODO #2716.") - sched = matched_routines[0] - # TODO #935 - replace the PSyIR argument data symbols with LFRic data - # symbols. For the moment we just return the unmodified PSyIR schedule - # but this should use RaisePSyIR2LFRicKernTrans once KernelInterface - # is fully functional (#928). - ksched = KernelSchedule(sched.symbol, - symbol_table=sched.symbol_table.detach()) - for child in sched.pop_all_children(): - ksched.addchild(child) - sched.replace_with(ksched) + self._interface_symbol = container.symbol_table.lookup(self.name) + + new_schedules = [] + for routine in routines[:]: + # If one of the symbols is not declared in a routine then + # this is only picked up when writing out the routine + # (raising a VisitorError), so we check here so that + # invalid code is not inlined. We use debug_string() to + # minimise the overhead. + + # TODO #2271 could potentially avoid the need for + # debug_string() within. Sergi suggests that we may be + # missing the traversal of the declaration init + # expressions and that might solve the problem. I'm not so + # sure as we are talking about unknown symbols that will + # only be resolved in the back-end (or not). If I am right + # then one option would be to use the FortranWriter, but + # that would be bigger overhead, or perhaps just the + # declarations part of FortranWriter if that is possible. + # Also see TODO issue #2336 which captures the specific + # problem in LFRic that this fixes. + routine.debug_string() + + # TODO #935 - replace the PSyIR argument data symbols with + # LFRic data symbols. For the moment we just return the + # unmodified PSyIR schedule but this should use + # RaisePSyIR2LFRicKernTrans once KernelInterface is fully + # functional (#928). + ksched = KernelSchedule( + routine.symbol, symbol_table=routine.symbol_table.detach()) + for child in routine.pop_all_children(): + ksched.addchild(child) + routine.replace_with(ksched) + new_schedules.append(ksched) + + self._kern_schedules = new_schedules return self._interface_symbol, self._kern_schedules diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 7c175c3c0b..97ef5075b9 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -41,7 +41,7 @@ from psyclone.configuration import Config from psyclone.core import AccessType -from psyclone.errors import GenerationError +from psyclone.errors import GenerationError, PSycloneError from psyclone.psyir.nodes.codeblock import CodeBlock from psyclone.psyir.nodes.container import Container from psyclone.psyir.nodes.statement import Statement @@ -603,16 +603,17 @@ def _location_txt(node): # bringing it into scope so we stop searching (the # alternative is to resolve every wildcard import we # encounter and that is very costly). - msg = (f"Failed to find the source code of the unresolved " - f"routine '{rsym.name}'. It may be being brought " - f"into scope from one of {wildcard_names}") + msg = (f"Failed to find the source code of the " + f"unresolved routine '{rsym.name}'. It may be " + f"being brought into scope from one of " + f"{wildcard_names}") if have_codeblock: - msg += (" or it may be within a CodeBlock. If it isn't" - ", you ") + msg += (" or it may be within a CodeBlock. If it " + "isn't, you ") else: msg += ". You " - msg += ("may wish to add the appropriate module name to " - "the `RESOLVE_IMPORTS` variable in the " + msg += ("may wish to add the appropriate module name " + "to the `RESOLVE_IMPORTS` variable in the " "transformation script.") raise NotImplementedError(msg) parent = cursor.parent diff --git a/src/psyclone/psyir/nodes/container.py b/src/psyclone/psyir/nodes/container.py index f362c8205a..a1921c5d72 100644 --- a/src/psyclone/psyir/nodes/container.py +++ b/src/psyclone/psyir/nodes/container.py @@ -237,8 +237,10 @@ def resolve_routine(self, name): if not rsym: # TODO for some reason, a module-inlined KernelSchedule does not # have a corresponding entry in a symbol table so we double check - # here... - for routine in self.walk(Routine): + # here. We have stop_type=Container so that if this method is + # called for a FileContainer, it does *not* walk down into child + # Containers. + for routine in self.walk(Routine, stop_type=Container): if routine.name.lower() == name.lower(): return [name] return [] diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index d27ea77965..d80d484815 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -871,28 +871,6 @@ def test_module_inline_with_interfaces(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_get_psyir_to_inline(monkeypatch): - ''' - Test that _get_psyir_to_inline() raises the expected error if more than - one potential routine implementation is found. - - ''' - sym = RoutineSymbol("my_sym") - rout = Routine.create("my_sym", SymbolTable(), []) - node = Call.create(sym) - # For simplicity we just monkeypatch Call.get_callees() so that it appears - # to return more than one Routine. - monkeypatch.setattr(node, "get_callees", lambda: [rout, rout]) - with pytest.raises(TransformationError) as err: - KernelModuleInlineTrans._get_psyir_to_inline(node) - # The duplicated symbol name below is purely a result of the monkeypatch - # - in reality these names will come from a generic interface and be - # different. - assert ("The target of the call to 'my_sym' cannot be inserted because " - "multiple implementations were found: ['my_sym', 'my_sym']." in - str(err.value)) - - def test_rm_imported_routine_symbol(fortran_reader): ''' Tests for the _rm_imported_routine_symbol() utility method. diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 773819e226..49f484a8c1 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -422,66 +422,68 @@ def validate_it_can_run_on_gpu(self, node, options): kernel_schedules = [node] k_or_r = "routine" - # Check that the routine does not access any data that is imported via - # a 'use' statement. - vai = VariablesAccessInfo() - kernel_schedule.reference_accesses(vai) - ktable = kernel_schedule.symbol_table - for sig in vai.all_signatures: - name = sig.var_name - first = vai[sig].all_accesses[0].node - if isinstance(first, Symbol): - table = ktable - else: - try: - table = first.scope.symbol_table - except SymbolError: - # The node associated with this access is not within a - # scoping region. + # Check that the routine(s) do(oes) not access any data that is + # imported via a 'use' statement. + for sched in kernel_schedules: + vai = VariablesAccessInfo() + sched.reference_accesses(vai) + ktable = sched.symbol_table + for sig in vai.all_signatures: + name = sig.var_name + first = vai[sig].all_accesses[0].node + if isinstance(first, Symbol): table = ktable - symbol = table.lookup(name) - if symbol.is_import: - # resolve_type does nothing if the Symbol type is known. - try: - symbol.resolve_type() - except (SymbolError, FileNotFoundError): - # TODO #11 - log that we failed to resolve this Symbol. - pass - if (isinstance(symbol, DataSymbol) and symbol.is_constant): - # An import of a compile-time constant is fine. - continue - raise TransformationError( - f"{k_or_r} '{node.name}' accesses the symbol " - f"'{symbol}' which is imported. If this symbol " - f"represents data then it must first be converted to a " - f"{k_or_r} argument using the KernelImportsToArguments " - f"transformation.") - - # We forbid CodeBlocks because we can't be certain that what they - # contain can be executed on a GPU. However, we do permit the user - # to override this check. - cblocks = kernel_schedule.walk(CodeBlock) - if not force: - if cblocks: - cblock_txt = ("\n " + "\n ".join(str(node) for node in - cblocks[0].get_ast_nodes) - + "\n") - option_txt = "options={'force': True}" - raise TransformationError( - f"Cannot safely apply {type(self).__name__} to {k_or_r} " - f"'{node.name}' because its PSyIR contains one or more " - f"CodeBlocks:{cblock_txt}You may use '{option_txt}' to " - f"override this check.") - - calls = ksched.walk(Call) - for call in calls: - if not call.is_available_on_device(): - call_str = call.debug_string().rstrip("\n") + else: + try: + table = first.scope.symbol_table + except SymbolError: + # The node associated with this access is not within a + # scoping region. + table = ktable + symbol = table.lookup(name) + if symbol.is_import: + # resolve_type does nothing if the Symbol type is known. + try: + symbol.resolve_type() + except (SymbolError, FileNotFoundError): + # TODO #11 - log that we failed to resolve this Symbol. + pass + if (isinstance(symbol, DataSymbol) and symbol.is_constant): + # An import of a compile-time constant is fine. + continue + raise TransformationError( + f"{k_or_r} '{node.name}' accesses the symbol " + f"'{symbol}' which is imported. If this symbol " + f"represents data then it must first be converted to a" + f" {k_or_r} argument using the " + f"KernelImportsToArguments transformation.") + + # We forbid CodeBlocks because we can't be certain that what they + # contain can be executed on a GPU. However, we do permit the user + # to override this check. + cblocks = sched.walk(CodeBlock) + if not force: + if cblocks: + cblock_txt = ("\n " + "\n ".join( + str(node) for node in cblocks[0].get_ast_nodes) + + "\n") + option_txt = "options={'force': True}" raise TransformationError( - f"{k_or_r} '{node.name}' calls another routine " - f"'{call_str}' which is not available on the " - f"accelerator device and therefore cannot have " - f"{type(self).__name__} applied to it (TODO #342).") + f"Cannot safely apply {type(self).__name__} to " + f"{k_or_r} '{node.name}' because its PSyIR contains " + f"one or more CodeBlocks:{cblock_txt}You may use " + f"'{option_txt}' to override this check.") + + calls = sched.walk(Call) + for call in calls: + if not call.is_available_on_device(): + call_str = call.debug_string().rstrip("\n") + raise TransformationError( + f"{k_or_r} '{node.name}' calls another routine " + f"'{call_str}' which is not available on the " + f"accelerator device and therefore cannot have " + f"{type(self).__name__} applied to it (TODO " + f"#342).") class OMPDeclareTargetTrans(Transformation, MarkRoutineForGPUMixin): @@ -2903,14 +2905,17 @@ def validate(self, node, options=None): f"Kernel '{node.name}' contains undeclared symbol: " f"{err.value}") from err - try: - kernel.check_outer_scope_accesses(node, "Kernel", - permit_unresolved=False, - ignore_non_data_accesses=True) - except SymbolError as err: - raise TransformationError( - f"Cannot apply {self.name} to Kernel '{node.name}' because it " - f"accesses data from its outer scope: {err.value}") from err + for kernel in kernels: + try: + kernel.check_outer_scope_accesses( + node, "Kernel", + permit_unresolved=False, + ignore_non_data_accesses=True) + except SymbolError as err: + raise TransformationError( + f"Cannot apply {self.name} to Kernel '{node.name}' " + f"because it accesses data from its outer scope: " + f"{err.value}") from err def apply(self, node, options=None): ''' From af91edc9075342d3f7428273154c662a2bf56bea Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Apr 2025 10:43:45 +0100 Subject: [PATCH 062/100] #2716 finish fixing tests --- .../kernel_module_inline_trans.py | 26 ++-- src/psyclone/psyir/nodes/call.py | 130 ++++-------------- src/psyclone/psyir/nodes/container.py | 3 +- .../transformations/globalstoargs_test.py | 21 --- .../tests/domain/lfric/lfric_kern_test.py | 24 ---- src/psyclone/tests/psyir/nodes/call_test.py | 3 +- .../tests/psyir/nodes/routine_test.py | 11 +- 7 files changed, 53 insertions(+), 165 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index a0b244c09f..c97c372aa0 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -374,6 +374,8 @@ def _rm_imported_routine_symbol(symbol: Symbol, symbol.name not in table else table) # Find the table containing the ContainerSymbol from which # the symbol is imported. + # TODO #1734 - this *should* always be the same as `actual_table` but + # this is not currently guaranteed. ctable = (csym.find_symbol_table(table.node) if csym.name not in table else table) remove_csym = (ctable.symbols_imported_from(csym) == [symbol] and @@ -486,6 +488,7 @@ def apply(self, node, options=None): # Deal with the RoutineSymbol that is in scope at the call site. sym_in_ctr = None + shadowed_sym = None if called_sym and (called_sym.is_import or called_sym.is_unresolved): table = called_sym.find_symbol_table(node) @@ -506,18 +509,18 @@ def apply(self, node, options=None): caller_cntr_table = table.node.parent.scope.symbol_table # Look to see whether it also contains a symbol matching # the name of the called routine. - caller_cntr_sym = caller_cntr_table.lookup(called_sym.name, - otherwise=None) - if caller_cntr_sym: - caller_cntr_table = caller_cntr_sym.find_symbol_table( + shadowed_sym = caller_cntr_table.lookup(called_sym.name, + otherwise=None) + if shadowed_sym: + caller_cntr_table = shadowed_sym.find_symbol_table( table.node.parent) if not isinstance(caller_cntr_table.node, FileContainer): # It is shadowing an outer symbol that is in a # Container (not a FileContainer) so we just need to # update the call to point to the outer symbol. - node.routine.symbol = caller_cntr_sym - if not (caller_cntr_sym.is_import or - caller_cntr_sym.is_unresolved): + node.routine.symbol = shadowed_sym + if not (shadowed_sym.is_import or + shadowed_sym.is_unresolved): # The outer symbol is local to this Container so # there's nothing else to do. return @@ -599,9 +602,12 @@ def apply(self, node, options=None): self._rm_imported_routine_symbol(interface_sym, codes_to_inline[0], callsite_table) - if interface_sym.name not in container.symbol_table: - container.symbol_table.add(interface_sym) - interface_sym.replace_symbols_using(container.symbol_table) + if shadowed_sym: + self._rm_imported_routine_symbol(shadowed_sym, + codes_to_inline[0], + caller_cntr_table) + container.symbol_table.add(interface_sym) + interface_sym.replace_symbols_using(container.symbol_table) # Set the module-inline flag to avoid generating the kernel imports # TODO #1823. If the kernel imports were generated at PSy-layer diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 97ef5075b9..c984ed7038 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -463,80 +463,6 @@ def copy(self): return new_copy - def _get_unresolved_callee(self): - ''' - Searches for the implementation(s) of the target routine for this Call - when it is unresolved. - - :returns: the Routine(s) that this call targets. - :rtype: list[:py:class:`psyclone.psyir.nodes.Routine`] - - :raises NotImplementedError: if the routine is not found - in any containers in scope at the call site. - ''' - rsym = self.routine.symbol - # Check for any "raw" Routines, i.e. any that are not in a Container. - # Such Routines would exist in the PSyIR as a child of a FileContainer - # (if the PSyIR contains a FileContainer). Note, if the PSyIR does - # contain a FileContainer, it will be the root node of the PSyIR. - for routine in self.root.children: - if (isinstance(routine, Routine) and - routine.name.lower() == rsym.name.lower()): - return [routine] - - # Now check for any wildcard imports and see if they can - # be used to resolve the symbol. - wildcard_names = [] - containers_not_found = [] - current_table = self.scope.symbol_table - while current_table: - for container_symbol in current_table.containersymbols: - if container_symbol.wildcard_import: - wildcard_names.append(container_symbol.name) - try: - container = container_symbol.find_container_psyir( - local_node=self) - except SymbolError: - container = None - if not container: - # Failed to find/process this Container. - containers_not_found.append(container_symbol.name) - continue - routines = [] - target_sym = container.symbol_table.lookup(rsym.name, - otherwise=None) - if target_sym: - # If the target of the call turns out to be an - # interface then the routines themselves are allowed - # to be private. - can_be_private = isinstance(target_sym, - GenericInterfaceSymbol) - for name in container.resolve_routine(rsym.name): - psyir = container.find_routine_psyir( - name, - allow_private=can_be_private) - if psyir: - routines.append(psyir) - if routines: - return routines - current_table = current_table.parent_symbol_table() - if not wildcard_names: - wc_text = "there are no wildcard imports" - else: - if containers_not_found: - wc_text = ( - f"attempted to resolve the wildcard imports from" - f" {wildcard_names}. However, failed to find the " - f"source for {containers_not_found}. The module search" - f" path is set to {Config.get().include_paths}") - else: - wc_text = (f"wildcard imports from {wildcard_names}") - raise NotImplementedError( - f"Failed to find the source code of the unresolved routine " - f"'{rsym.name}' - looked at any routines in the same source " - f"file and {wc_text}. Searching for external routines " - f"that are only resolved at link time is not supported.") - def get_callees(self): ''' Searches for the implementation(s) of all potential target routines @@ -591,31 +517,31 @@ def _location_txt(node): if routines: rsym.interface = DefaultModuleInterface() return routines - if not have_codeblock: - have_codeblock = any(isinstance(child, CodeBlock) for - child in cursor.children) - wildcard_names = [csym.name for csym in - cursor.symbol_table.wildcard_imports( - scope_limit=cursor)] - if wildcard_names: - # We haven't yet found an implementation of the Routine - # but we have found a wildcard import and that could be - # bringing it into scope so we stop searching (the - # alternative is to resolve every wildcard import we - # encounter and that is very costly). - msg = (f"Failed to find the source code of the " - f"unresolved routine '{rsym.name}'. It may be " - f"being brought into scope from one of " - f"{wildcard_names}") - if have_codeblock: - msg += (" or it may be within a CodeBlock. If it " - "isn't, you ") - else: - msg += ". You " - msg += ("may wish to add the appropriate module name " - "to the `RESOLVE_IMPORTS` variable in the " - "transformation script.") - raise NotImplementedError(msg) + if not have_codeblock: + have_codeblock = any(isinstance(child, CodeBlock) for + child in cursor.children) + wildcard_names = [csym.name for csym in + cursor.symbol_table.wildcard_imports( + scope_limit=cursor)] + if wildcard_names: + # We haven't yet found an implementation of the Routine + # but we have found a wildcard import and that could be + # bringing it into scope so we stop searching (the + # alternative is to resolve every wildcard import we + # encounter and that is very costly). + msg = (f"Failed to find the source code of the " + f"unresolved routine '{rsym.name}'. It may be " + f"being brought into scope from one of " + f"{wildcard_names}") + if have_codeblock: + msg += (" or it may be within a CodeBlock. If it " + "isn't, you ") + else: + msg += ". You " + msg += ("may wish to add the appropriate module name " + "to the `RESOLVE_IMPORTS` variable in the " + "transformation script.") + raise NotImplementedError(msg) parent = cursor.parent cursor = parent.scope if parent else None @@ -688,17 +614,17 @@ def _location_txt(node): cursor = container while cursor and isinstance(cursor, Container): routines = [] - all_names = container.resolve_routine(rsym.name) + all_names = cursor.resolve_routine(rsym.name) if isinstance(rsym, GenericInterfaceSymbol): # Although the interface must be public, the routines to which # it points may themselves be private. can_be_private = True for name in all_names: - psyir = container.find_routine_psyir( + psyir = cursor.find_routine_psyir( name, allow_private=can_be_private) if psyir: routines.append(psyir) - if len(routines) == len(all_names): + if all_names and len(routines) == len(all_names): # We've resolved everything. return routines cursor = cursor.parent diff --git a/src/psyclone/psyir/nodes/container.py b/src/psyclone/psyir/nodes/container.py index a1921c5d72..d5e3075f8c 100644 --- a/src/psyclone/psyir/nodes/container.py +++ b/src/psyclone/psyir/nodes/container.py @@ -233,7 +233,8 @@ def resolve_routine(self, name): :raises TypeError: if the Symbol with the supplied name is not a RoutineSymbol, GenericInterfaceSymbol or imported Symbol. ''' - rsym = self.symbol_table.lookup(name, otherwise=None) + rsym = self.symbol_table.lookup(name, otherwise=None, + scope_limit=self) if not rsym: # TODO for some reason, a module-inlined KernelSchedule does not # have a corresponding entry in a symbol table so we double check diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 16fed103be..74534c11af 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -38,7 +38,6 @@ import os import pytest -from psyclone.gocean1p0 import GOInvokeSchedule from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory, InvokeSchedule from psyclone.psyir.symbols import (DataSymbol, REAL_TYPE, INTEGER_TYPE, @@ -101,26 +100,6 @@ def test_kernelimportstoargumentstrans_no_wildcard_import(): "unresolved" in str(err.value)) -def test_kernelimportstoargumentstrans_no_polymorphic(monkeypatch): - ''' - Check that the transformation rejects polymorphic kernels. - - ''' - trans = KernelImportsToArguments() - _, invoke = get_invoke("26.8_mixed_precision_args.f90", api="lfric", idx=0) - kernel = invoke.schedule.coded_kernels()[0] - invsched = kernel.ancestor(InvokeSchedule) - # Currently this transformation will only work for the GOcean API so - # monkeypatch the class of the parent InvokeSchedule. - monkeypatch.setattr(invsched, "__class__", GOInvokeSchedule) - with pytest.raises(TransformationError) as err: - trans.validate(kernel) - assert ("KernelImportsToArguments transformation does not support " - "polymorphic kernels but found the following implementations for " - "kernel 'mixed_code': ['mixed_code_32', 'mixed_code_64']" - in str(err.value)) - - @pytest.mark.xfail(reason="Transformation does not set modified property " "of kernel - #663") @pytest.mark.usefixtures("kernel_outputdir") diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index f46757ec54..f34c1bea43 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -218,30 +218,6 @@ def fake_validate(_1, _2): "['mixed_code_32', 'mixed_code_64'].)" in str(err.value)) -def test_get_kernel_sched_mixed_precision_multiple_match(monkeypatch): - ''' - Test that we get the expected error if there appears to be more than - one matching implementation for a mixed-precision kernel. - - TODO #2716 will fix this and then this test can be removed. - - ''' - _, invoke = get_invoke("26.8_mixed_precision_args.f90", TEST_API, - name="invoke_0", dist_mem=False) - sched = invoke.schedule - kernels = sched.walk(LFRicKern, stop_type=LFRicKern) - - # To simplify things we just monkeypatch the 'validate_kernel_code_args' - # method so that it always succeeds. - monkeypatch.setattr(LFRicKern, "validate_kernel_code_args", - lambda _1, _2: None) - with pytest.raises(GenerationError) as err: - _ = kernels[0].get_kernel_schedule() - assert ("Found multiple kernel implementations (['mixed_code_32', " - "'mixed_code_64']) that apparently match the interface of this " - "call to 'mixed_code'" in str(err.value)) - - def test_validate_kernel_code_args(monkeypatch): '''Test that a coded kernel that conforms to the expected kernel metadadata is validated successfully. Also check that the diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index 797fb862c6..db6fa45195 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -1525,8 +1525,7 @@ def test_call_get_callees_no_container(fortran_reader): call = top_routine.walk(Call)[0] with pytest.raises(SymbolError) as err: _ = call.get_callees() - assert ("The RoutineSymbol for Routine 'bottom' is marked as being local " - "but failed to find the corresponding implementation in code:\n'" + assert ("Failed to find a Routine named 'bottom' in code:\n'" "subroutine top()" in str(err.value)) diff --git a/src/psyclone/tests/psyir/nodes/routine_test.py b/src/psyclone/tests/psyir/nodes/routine_test.py index 6255a4db93..ad2e7d0290 100644 --- a/src/psyclone/tests/psyir/nodes/routine_test.py +++ b/src/psyclone/tests/psyir/nodes/routine_test.py @@ -485,20 +485,21 @@ def test_check_outer_scope_accesses(config_instance): config_instance.include_paths = [] # Multiple wildcard imports are handled by bringing them into the routine # and so aren't a problem. - kcall.get_kernel_schedule().check_outer_scope_accesses(kcall, "Kernel") + _, kschedules = kcall.get_kernel_schedule() + kschedules[0].check_outer_scope_accesses(kcall, "Kernel") # Now try where there's only a single wildcard import so we know the origin # of the symbol. kcall0 = schedule.walk(CodedKern)[0] - ksched = kcall0.get_kernel_schedule() - ctable = ksched.ancestor(Container).symbol_table + _, kscheds = kcall0.get_kernel_schedule() + ctable = kscheds[0].ancestor(Container).symbol_table # To do this, we manually remove all ContainerSymbols apart from the one # from which 'go_wp' is imported. for sym in ctable.wildcard_imports(): if sym.name != "kind_params_mod": ctable._symbols.pop(sym.name) - ksched.check_outer_scope_accesses(kcall0, "Kernel") - table = ksched.symbol_table + kscheds[0].check_outer_scope_accesses(kcall0, "Kernel") + table = kscheds[0].symbol_table assert (table.lookup("go_wp").interface.container_symbol.name == "kind_params_mod") From c431bc96558dd9e9874968ae0ac72a9357a8ac29 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Apr 2025 13:21:13 +0100 Subject: [PATCH 063/100] #2716 fix cov for Call and LFRicKern --- src/psyclone/domain/lfric/lfric_kern.py | 6 ++-- src/psyclone/psyir/nodes/call.py | 35 ++++++++----------- .../tests/domain/lfric/lfric_kern_test.py | 15 ++++++-- src/psyclone/tests/psyir/nodes/call_test.py | 19 ++++++++-- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 36aefe9325..74abb60695 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -745,8 +745,10 @@ class creates the PSyIR schedule on first invocation which is routines.append(rt_psyir) if routines: break - if not routines: - raise InternalError("oh dear") + else: + raise InternalError( + f"Failed to find any routines for Kernel '{self.name}'. " + f"Source of Kernel is:\n{self.ast}") self._interface_symbol = None if len(routines) > 1: diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index c984ed7038..7c1e5944cf 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -476,25 +476,6 @@ def get_callees(self): limitation prevents definite determination of the target routine. ''' - def _location_txt(node): - ''' - Utility to generate meaningful location text. - - :param node: a PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.Node` - - :returns: description of location of node. - :rtype: str - ''' - if isinstance(node, Container): - return f"Container '{node.name}'" - out_lines = node.debug_string().split("\n") - idx = -1 - while not out_lines[idx]: - idx -= 1 - last_line = out_lines[idx] - return f"code:\n'{out_lines[0]}\n...\n{last_line}'" - rsym = self.routine.symbol if rsym.is_unresolved: # Search for the Routine in the current file. This search is @@ -629,9 +610,19 @@ def _location_txt(node): return routines cursor = cursor.parent + if isinstance(root_node, Container): + location_txt = f"Container '{root_node.name}'" + else: + out_lines = root_node.debug_string().split("\n") + idx = -1 + while not out_lines[idx]: + idx -= 1 + last_line = out_lines[idx] + location_txt = f"code:\n'{out_lines[0]}\n...\n{last_line}'" + raise SymbolError( f"Failed to find a Routine named '{rsym.name}' in " - f"{_location_txt(root_node)}. This is normally because the routine" + f"{location_txt}. This is normally because the routine" f" is within a CodeBlock.") def _check_argument_type_matches( @@ -820,3 +811,7 @@ def get_callee( f"No matching routine found for '{self.debug_string()}':" "\n" + error_msg ) + + +# For AutoAPI auto-documentation generation. +__all__ = ["Call"] diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index f34c1bea43..908d78e5ff 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -53,7 +53,8 @@ from psyclone.errors import InternalError, GenerationError from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory -from psyclone.psyir.nodes import Reference, KernelSchedule +from psyclone.psyir.frontend.fparser2 import Fparser2Reader +from psyclone.psyir.nodes import Container, Reference, KernelSchedule from psyclone.psyir.symbols import ArgumentInterface, DataSymbol, REAL_TYPE, \ INTEGER_TYPE, ArrayType from psyclone.tests.utilities import get_invoke @@ -136,7 +137,7 @@ def test_kern_ncolours(monkeypatch): in str(err.value)) -def test_get_kernel_schedule(): +def test_get_kernel_schedule(monkeypatch): '''Test that a PSyIR kernel schedule is created by get_kernel_schedule if one does not exist and that the same kernel schedule is returned if one has already been created. @@ -159,6 +160,16 @@ def test_get_kernel_schedule(): _, kernel_schedules_2 = kernel.get_kernel_schedule() assert kernel_schedules[0] is kernel_schedules_2[0] + # Check the internal error for the case where we fail to get any + # implementation for the kernel. + kernel._kern_schedules = None + # Monkeypatch the frontend so that it just returns an empty Container. + monkeypatch.setattr(Fparser2Reader, "generate_psyir", + lambda _1, _2: Container("dummy_mod")) + with pytest.raises(InternalError) as err: + kernel.get_kernel_schedule() + assert ("Failed to find any routines for Kernel 'matrix_vector_code'" + in str(err.value)) @pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index db6fa45195..35225ceedc 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -47,8 +47,8 @@ from psyclone.psyir.nodes.call import CallMatchingArgumentsNotFound from psyclone.psyir.nodes.node import colored from psyclone.psyir.symbols import ( - ArrayType, INTEGER_TYPE, DataSymbol, NoType, RoutineSymbol, REAL_TYPE, - SymbolError) + ArrayType, INTEGER_TYPE, ContainerSymbol, DataSymbol, NoType, + RoutineSymbol, REAL_TYPE, SymbolError) class SpecialCall(Call): @@ -1410,7 +1410,7 @@ def test_call_get_callees_unresolved(fortran_reader, tmpdir, monkeypatch, @pytest.mark.usefixtures("clear_module_manager_instance") -def test_call_get_callees_resolved_not_found(fortran_reader): +def test_call_get_callees_resolved_not_found(fortran_reader, monkeypatch): ''' Test get_callees() when the RoutineSymbol is resolved (i.e. we know which Container it comes from) but we can't find the source of the Container. @@ -1428,6 +1428,13 @@ def test_call_get_callees_resolved_not_found(fortran_reader): assert ("RoutineSymbol 'this_one' is imported from Container 'another_mod'" " but the source defining that container could not be found. The " "module search path is set to [" in str(err.value)) + monkeypatch.setattr(ContainerSymbol, "find_container_psyir", + lambda _1, local_node=None: None) + with pytest.raises(NotImplementedError) as err: + _ = call.get_callees() + assert ("RoutineSymbol 'this_one' is imported from Container 'another_mod'" + " but the PSyIR for that container could not be generated." + in str(err.value)) def test_call_get_callees_interface(fortran_reader): @@ -1520,6 +1527,12 @@ def test_call_get_callees_no_container(fortran_reader): ''' psyir = fortran_reader.psyir_from_source(code) top_routine = psyir.walk(Routine)[0] + new_call = Call.create(RoutineSymbol("missing"), []) + top_routine.addchild(new_call) + with pytest.raises(SymbolError) as err: + _ = new_call.get_callees() + assert ("Failed to find a Routine named 'missing' in Container 'my_mod'" + in str(err.value)) # Deliberately make the Routine node an orphan so there's no Container. top_routine.detach() call = top_routine.walk(Call)[0] From adf12587083c215cb3d25252cae198ac49ca282d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Apr 2025 14:03:29 +0100 Subject: [PATCH 064/100] #2716 get full cov of Container --- src/psyclone/psyir/nodes/container.py | 8 -------- src/psyclone/tests/psyir/nodes/container_test.py | 14 ++++++++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/psyclone/psyir/nodes/container.py b/src/psyclone/psyir/nodes/container.py index d5e3075f8c..4dd15a0ae1 100644 --- a/src/psyclone/psyir/nodes/container.py +++ b/src/psyclone/psyir/nodes/container.py @@ -236,14 +236,6 @@ def resolve_routine(self, name): rsym = self.symbol_table.lookup(name, otherwise=None, scope_limit=self) if not rsym: - # TODO for some reason, a module-inlined KernelSchedule does not - # have a corresponding entry in a symbol table so we double check - # here. We have stop_type=Container so that if this method is - # called for a FileContainer, it does *not* walk down into child - # Containers. - for routine in self.walk(Routine, stop_type=Container): - if routine.name.lower() == name.lower(): - return [name] return [] if isinstance(rsym, GenericInterfaceSymbol): return [rt.symbol.name.lower() for rt in rsym.routines] diff --git a/src/psyclone/tests/psyir/nodes/container_test.py b/src/psyclone/tests/psyir/nodes/container_test.py index c420100c39..d11dc80a74 100644 --- a/src/psyclone/tests/psyir/nodes/container_test.py +++ b/src/psyclone/tests/psyir/nodes/container_test.py @@ -42,7 +42,7 @@ from psyclone.errors import GenerationError from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.nodes import (Call, colored, Container, FileContainer, - KernelSchedule, Return) + KernelSchedule, Return, Routine) from psyclone.psyir.symbols import DataSymbol, REAL_SINGLE_TYPE, SymbolTable from psyclone.tests.utilities import check_links @@ -223,7 +223,7 @@ def test_find_routine_psyir_routine_not_found(fortran_reader): assert result is None -def test_get_routine_missing_container(fortran_reader): +def test_find_routine_psyir_missing_container(fortran_reader): '''Test that None is returned when we cannot find the container from which the required Routine is imported. @@ -238,7 +238,7 @@ def test_get_routine_missing_container(fortran_reader): assert result is None -def test_get_routine_missing_container_wildcard(fortran_reader): +def test_find_routine_psyir_missing_container_wildcard(fortran_reader): '''Test that None is returned when we cannot find the container from which a wildcard import is performed. @@ -256,9 +256,7 @@ def test_get_routine_missing_container_wildcard(fortran_reader): def test_find_routine_in_container_private_routine_not_found(fortran_reader): '''Test that None is returned when the required Routine is not found in the Container associated with the supplied container symbol, as - it is private. This situation should not arise as it is invalid to - try to import a private routine. However, there are currrently no - checks for this when creating PSyIR. + it is private. ''' private_sub_in_module = SUB_IN_MODULE.replace( @@ -270,6 +268,10 @@ def test_find_routine_in_container_private_routine_not_found(fortran_reader): container = csym.find_container_psyir(local_node=call_node) result = container.find_routine_psyir(call_node.routine.name) assert result is None + # If we permit the Routine to be private then it is returned. + result = container.find_routine_psyir(call_node.routine.name, + allow_private=True) + assert isinstance(result, Routine) assert container.find_routine_psyir("doesnotexist") is None From 16978af1fba193c827896de3d0eb464d66d8fbe8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Apr 2025 14:34:27 +0100 Subject: [PATCH 065/100] #2716 rm __all__ from Call because of doc warning --- src/psyclone/psyir/nodes/call.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 7c1e5944cf..529cab5a4b 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -811,7 +811,3 @@ def get_callee( f"No matching routine found for '{self.debug_string()}':" "\n" + error_msg ) - - -# For AutoAPI auto-documentation generation. -__all__ = ["Call"] From 3318f2e5a4288619195211ee4eda2470b7c606f4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 10 Apr 2025 15:27:47 +0100 Subject: [PATCH 066/100] #2716 allow for missing symbol_table in Routine.name setter --- src/psyclone/psyir/nodes/routine.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/psyclone/psyir/nodes/routine.py b/src/psyclone/psyir/nodes/routine.py index 92ea37f47b..47f0c3c85f 100644 --- a/src/psyclone/psyir/nodes/routine.py +++ b/src/psyclone/psyir/nodes/routine.py @@ -415,13 +415,18 @@ def name(self, new_name): self.parent.symbol_table.rename_symbol(symbol, new_name) else: # Check if the symbol in our own symbol table is the symbol - try: - sym = self.symbol_table.lookup(symbol.name) - if sym is self._symbol: - self.symbol_table.rename_symbol(symbol, new_name) - except KeyError: - # Symbol isn't in a symbol table so we can modify its - # name freely + # During a copy it is possible for a Routine to not yet + # have a SymbolTable so allow for that. + if self.symbol_table: + try: + sym = self.symbol_table.lookup(symbol.name) + if sym is self._symbol: + self.symbol_table.rename_symbol(symbol, new_name) + except KeyError: + # Symbol isn't in a symbol table so we can modify its + # name freely + symbol._name = new_name + else: symbol._name = new_name def __str__(self): From d84b829201254cddcb480e6f0627913e9290cebf Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 14 Apr 2025 11:44:06 +0100 Subject: [PATCH 067/100] #2716 update KernelModuleInlineTrans so that all calls of a given CodedKern point to inlined version --- .../kernel_module_inline_trans.py | 30 +++++++++------ .../kernel_module_inline_trans_test.py | 37 ++++--------------- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index c97c372aa0..480f5f3622 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -527,15 +527,6 @@ def apply(self, node, options=None): updated_routines = self._prepare_code_to_inline(codes_to_inline) - # Update the Kernel to point to the updated PSyIR. - if isinstance(node, CodedKern): - # pylint: disable=protected-access - node._kern_schedules = updated_routines - if interface_sym: - node._interface_symbol = ( - updated_routines[0].scope.symbol_table.lookup( - interface_sym.name)) - # The Container into which we will inline the Routine(s). container = node.ancestor(Container) @@ -609,9 +600,26 @@ def apply(self, node, options=None): container.symbol_table.add(interface_sym) interface_sym.replace_symbols_using(container.symbol_table) - # Set the module-inline flag to avoid generating the kernel imports + # Update the Kernel to point to the updated PSyIR and set + # the module-inline flag to avoid generating the kernel imports # TODO #1823. If the kernel imports were generated at PSy-layer # creation time, we could just remove it here instead of setting a # flag. if isinstance(node, CodedKern): - node.module_inline = True + cntr = node.ancestor(Container) + # TODO #2846 - since we do not currently rename module-inlined + # routines, inlining just one instance of a Kernel call and + # subsequently transforming it (e.g. by adding ACC ROUTINE) would + # inhibit all further transformations of any other calls to that + # kernel (that relied upon inlining). Therefore, for now, we update + # *all* calls to this particular kernel in the current module to + # point to the module-inlined version. + for kern in cntr.walk(CodedKern, stop_type=CodedKern): + if kern.name == node.name: + kern.module_inline = True + # pylint: disable=protected-access + kern._kern_schedules = updated_routines + if interface_sym: + kern._interface_symbol = ( + updated_routines[0].scope.symbol_table.lookup( + interface_sym.name)) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index d80d484815..cc9799eacd 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -491,13 +491,11 @@ def test_module_inline_apply_kernel_in_multiple_invokes(tmpdir): inline_trans.apply(coded_kern) artrans.apply(coded_kern) gen = str(psy.gen) - - # After this, one invoke uses the inlined top-level subroutine - # and the other imports it (shadowing the top-level symbol) - assert gen.count("USE testkern_qr_mod, ONLY: testkern_qr_code") == 1 + assert gen.count("USE testkern_qr_mod, ONLY: testkern_qr_code") == 0 assert gen.count("END SUBROUTINE testkern_qr_code") == 1 - # Module inline kernel in invoke 2 + # After this, both invokes use the inlined top-level subroutine. + # Module-inlining kernel in invoke 2 should have no effect. schedule1 = psy.invokes.invoke_list[1].schedule for coded_kern in schedule1.walk(CodedKern): if coded_kern.name == "testkern_qr_code": @@ -533,35 +531,14 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): assert "subroutine mixed_code_32" in output assert "!$acc routine seq" in output assert "subroutine mixed_code_64" in output + # Since we don't currently rename module-inlined kernels, module-inlining + # just one instance means that call to that same Kernel the whole module + # uses the newly-inlined version. assert ("""subroutine invoke_1(scalar_r_phys, field_r_phys, \ operator_r_def, f1, f2, m1, a, m2, istp, qr) use testkern_qr_mod, only: testkern_qr_code - use mixed_kernel_mod, only: mixed_code use quadrature""" in output) - - success = LFRicBuild(tmpdir).code_compiles(psy) - if not success: - if LFRicBuild.F90 == "nvfortran": - pytest.xfail( - reason="nvfortran has a bug when a local import of a generic " - "interface overrides an interface of the same name in an " - "outer scope.") - else: - assert False - - # Module inline kernel in invoke 2 - schedule2 = psy.invokes.invoke_list[1].schedule - for coded_kern in schedule2.walk(CodedKern): - if coded_kern.name == "mixed_code": - inline_trans.apply(coded_kern) - - output2 = str(psy.gen) - assert ("""SUBROUTINE invoke_1(scalar_r_phys, field_r_phys, \ -operator_r_def, f1, f2, m1, a, m2, istp, qr) - USE testkern_qr_mod, ONLY: testkern_qr_code - USE quadrature""" in output2) - - assert "mixed_kernel_mod" not in output2 + assert "mixed_kernel_mod" not in output assert LFRicBuild(tmpdir).code_compiles(psy) From fa787a29a6718e50814fff6b5e86a521a8ffedfa Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 15 Apr 2025 10:13:21 +0100 Subject: [PATCH 068/100] #2716 allow for interface symbol when updating calls after mod-inlining --- .../common/transformations/kernel_module_inline_trans.py | 5 +++-- .../transformations/kernel_module_inline_trans_test.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 480f5f3622..406a15d4e6 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -583,8 +583,9 @@ def apply(self, node, options=None): name = call.routine.symbol.name.lower() if name == target_name: call.routine.symbol = target_sym - # All Calls that referred to this Symbol must also be updated. - if sym_in_ctr: + # All Calls that referred to this Symbol must also be updated. Take + # care that the name matches as sym_in_ctr might be an interface. + if sym_in_ctr and sym_in_ctr.name == target_sym.name: for call in container.walk(Call): if call.routine.symbol is sym_in_ctr: call.routine.symbol = target_sym diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index cc9799eacd..cbd74e16cc 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -970,6 +970,10 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, assert "use my_mod" not in output assert "subroutine my_other_sub" in output assert "interface my_interface" in output + # Check that the calls themselves are unaffected. + assert "call my_sub(a)" in output + assert "call my_other_sub(b)" in output + assert "call my_interface(b)" in output assert Compile(tmpdir).string_compiles(output) From 27b1e3a22bd3b0263b3f969de881612272911af1 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 23 Apr 2025 15:22:41 +0100 Subject: [PATCH 069/100] #2716 update gpu_offloading script to exclude MATMULs --- examples/lfric/scripts/gpu_offloading.py | 25 +++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index e2b244e0ea..85093257ed 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -73,6 +73,9 @@ def _inline_calls(kern): ''' Recursively inline all calls within the supplied Kernel or Routine. + Currently only attempts to replace MATMUL intrinsic calls with inline + code. +` :param kern: the Kernel or Routine to inline any Calls into. ''' @@ -88,12 +91,23 @@ def _inline_calls(kern): sched: Schedule for call in sched.walk(Call): call: Call - if isinstance(call, IntrinsicCall): - try: - matrans.apply(call) - except TransformationError: - pass + # The NVIDIA compiler (as at 25.3) will sometimes fail to compile + # code with calls to MATMUL with a claim that they are not + # available on the device, e.g.: + # Call to NVHPC runtime function not supported - + # pgf90_matmul_real4_i8 + # Therefore, if we are unable to replace a MATMUL by generic code, + # the resulting TransformationError will signal (to the calling + # routine) that we are not to mark this kernel for offload. + if (isinstance(call, IntrinsicCall) and + call.intrinsic == IntrinsicCall.Intrinsic.MATMUL): + matrans.apply(call) continue + + # For now we only look at MATMUL calls. In future we may want to + # remove this `continue` and attempt to inline more calls. + continue + if any(name in call.routine.name for name in INLINE_EXCLUSIONS): continue try: @@ -196,6 +210,7 @@ def trans(psyir): print(f"Failed to module-inline kernel " f"'{kern.name}' due to:\n{err.value}") try: + _inline_calls(kern) gpu_annotation_trans.apply(kern) print(f"Annotated kernel '{kern.name}'") except TransformationError as err: From 7e8f40ac6090af71c656992e6b3a3d489c953517 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 23 Apr 2025 20:35:00 +0100 Subject: [PATCH 070/100] #2716 tidy comment [skip ci] --- .../common/transformations/kernel_module_inline_trans_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index cbd74e16cc..68716d6dc9 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -532,8 +532,8 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): assert "!$acc routine seq" in output assert "subroutine mixed_code_64" in output # Since we don't currently rename module-inlined kernels, module-inlining - # just one instance means that call to that same Kernel the whole module - # uses the newly-inlined version. + # just one instance means that calls to that same Kernel throughout the + # whole module uses the newly-inlined version. assert ("""subroutine invoke_1(scalar_r_phys, field_r_phys, \ operator_r_def, f1, f2, m1, a, m2, istp, qr) use testkern_qr_mod, only: testkern_qr_code From 1f9a88c7f52ffe967ec6d6ec6f4d4361d73b7379 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 23 Apr 2025 21:40:04 +0100 Subject: [PATCH 071/100] #2716 rm unused code --- .../common/transformations/kernel_module_inline_trans.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 406a15d4e6..50ac542fc8 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -461,12 +461,6 @@ def apply(self, node, options=None): if interface_sym: called_sym = callsite_table.lookup(interface_sym.name, otherwise=None) - if interface_sym is called_sym and not called_sym.is_import: - # Interface symbol is already local so nothing to do. - if isinstance(node, CodedKern): - node.module_inline = True - # TODO #11 - log this. - return else: caller_name = codes_to_inline[0].name for routine in codes_to_inline: @@ -482,8 +476,6 @@ def apply(self, node, options=None): else: # All routines are module-inlined so there's nothing to do. # TODO #11 - log this. - if isinstance(node, CodedKern): - node.module_inline = True return # Deal with the RoutineSymbol that is in scope at the call site. From 82dfcb8e14af94d25d9a50917f460b1f57cf3a82 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 23 Apr 2025 22:00:46 +0100 Subject: [PATCH 072/100] #2716 fix lfric_kern coverage --- .../tests/domain/lfric/lfric_kern_test.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index 908d78e5ff..0ce1d4a71f 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -48,13 +48,14 @@ import psyclone from psyclone.configuration import Config from psyclone.core import AccessType +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import (LFRicConstants, LFRicTypes, LFRicKern, LFRicKernMetadata, LFRicLoop) from psyclone.errors import InternalError, GenerationError from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory from psyclone.psyir.frontend.fparser2 import Fparser2Reader -from psyclone.psyir.nodes import Container, Reference, KernelSchedule +from psyclone.psyir.nodes import Container, KernelSchedule, Reference, Routine from psyclone.psyir.symbols import ArgumentInterface, DataSymbol, REAL_TYPE, \ INTEGER_TYPE, ArrayType from psyclone.tests.utilities import get_invoke @@ -172,6 +173,26 @@ def test_get_kernel_schedule(monkeypatch): in str(err.value)) +def test_get_kernel_schedule_same_container(monkeypatch): + ''' + Check that get_kernel_schedule() first examines all routines in the same + Container. + + ''' + _, invoke = get_invoke("12_kernel_specific.f90", TEST_API, idx=0) + sched = invoke.schedule + # Module-inline the kernels so that they are in the same Container as the + # call site. + mod_inline_trans = KernelModuleInlineTrans() + for kern in sched.walk(LFRicKern): + mod_inline_trans.apply(kern) + # Remove the cached schedule to force get_kernel_schedule() to search. + monkeypatch.setattr(kern, "_kern_schedules", None) + _, schedules = kern.get_kernel_schedule() + # The returned schedule should be the one in the local Container. + assert schedules[0] in sched.ancestor(Container).walk(Routine) + + @pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" " implementations of a polymorphic kernel. We need to " "put back (and fix) the ability to resolve which " From 7834fb0155fa1f0e3bc8128c7f38c980e379015a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 24 Apr 2025 16:23:25 +0100 Subject: [PATCH 073/100] #2716 ensure inserted interface symbol is private --- .../common/transformations/kernel_module_inline_trans.py | 3 +++ .../transformations/kernel_module_inline_trans_test.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 50ac542fc8..a03ec409d8 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -583,6 +583,8 @@ def apply(self, node, options=None): call.routine.symbol = target_sym if interface_sym: + # Deal with the interface symbol - remove any existing import and + # then make sure the local symbol is private. self._rm_imported_routine_symbol(interface_sym, codes_to_inline[0], callsite_table) @@ -591,6 +593,7 @@ def apply(self, node, options=None): codes_to_inline[0], caller_cntr_table) container.symbol_table.add(interface_sym) + interface_sym.visibility = Symbol.Visibility.PRIVATE interface_sym.replace_symbols_using(container.symbol_table) # Update the Kernel to point to the updated PSyIR and set diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 68716d6dc9..89acdcf515 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -47,8 +47,9 @@ from psyclone.psyir.nodes import ( Container, Routine, CodeBlock, Call, IntrinsicCall) from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, ImportInterface, RoutineSymbol, REAL_TYPE, - Symbol, SymbolError, SymbolTable, UnresolvedInterface) + ContainerSymbol, DataSymbol, GenericInterfaceSymbol, ImportInterface, + RoutineSymbol, REAL_TYPE, Symbol, SymbolError, SymbolTable, + UnresolvedInterface) from psyclone.psyir.transformations import TransformationError from psyclone.transformations import ACCRoutineTrans, OMPDeclareTargetTrans from psyclone.tests.gocean_build import GOceanBuild @@ -833,6 +834,10 @@ def test_module_inline_with_interfaces(tmpdir): kern_calls = invoke.schedule.walk(CodedKern) inline_trans = KernelModuleInlineTrans() inline_trans.apply(kern_calls[0]) + sym = kern_calls[0].scope.symbol_table.lookup("mixed_code") + # Check that the inteface symbol is declared and is private. + assert isinstance(sym, GenericInterfaceSymbol) + assert sym.visibility == Symbol.Visibility.PRIVATE # Check that module-inlining the second kernel call (which is to the # same interface) doesn't break anything. inline_trans.apply(kern_calls[1]) From 4d1e02de5de3ace85a1fe832b5fd8e2dd2be0b76 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 24 Apr 2025 18:58:10 +0100 Subject: [PATCH 074/100] #2716 rm un-needed check from LFRicKern --- src/psyclone/domain/lfric/lfric_kern.py | 28 ++++--------------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 74abb60695..647eb3f0c3 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -756,30 +756,10 @@ class creates the PSyIR schedule on first invocation which is new_schedules = [] for routine in routines[:]: - # If one of the symbols is not declared in a routine then - # this is only picked up when writing out the routine - # (raising a VisitorError), so we check here so that - # invalid code is not inlined. We use debug_string() to - # minimise the overhead. - - # TODO #2271 could potentially avoid the need for - # debug_string() within. Sergi suggests that we may be - # missing the traversal of the declaration init - # expressions and that might solve the problem. I'm not so - # sure as we are talking about unknown symbols that will - # only be resolved in the back-end (or not). If I am right - # then one option would be to use the FortranWriter, but - # that would be bigger overhead, or perhaps just the - # declarations part of FortranWriter if that is possible. - # Also see TODO issue #2336 which captures the specific - # problem in LFRic that this fixes. - routine.debug_string() - - # TODO #935 - replace the PSyIR argument data symbols with - # LFRic data symbols. For the moment we just return the - # unmodified PSyIR schedule but this should use - # RaisePSyIR2LFRicKernTrans once KernelInterface is fully - # functional (#928). + # TODO #935 - replace the PSyIR argument data symbols with LFRic + # data symbols. For the moment we just return the unmodified PSyIR + # schedule but this should use RaisePSyIR2LFRicKernTrans once + # KernelInterface is fully functional (#928). ksched = KernelSchedule( routine.symbol, symbol_table=routine.symbol_table.detach()) for child in routine.pop_all_children(): From 9c8615d181f19aa50ef476af9215e0a020f1b7a2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 28 May 2025 09:35:42 +0100 Subject: [PATCH 075/100] #2716 fix tests after merge --- .../kernel_module_inline_trans_test.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index b123544d14..c1548c091a 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -487,11 +487,6 @@ def test_module_inline_apply_kernel_in_multiple_invokes(tmpdir): artrans.apply(coded_kern) gen = str(psy.gen) - # After this, one invoke uses the inlined top-level subroutine - # and the other imports it (shadowing the top-level symbol) - assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 1 - assert gen.count("end subroutine testkern_qr_code") == 1 - # After this, both invokes use the inlined top-level subroutine. # Module-inlining kernel in invoke 2 should have no effect. schedule1 = psy.invokes.invoke_list[1].schedule @@ -534,8 +529,10 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): # whole module uses the newly-inlined version. assert ("""subroutine invoke_1(scalar_r_phys, field_r_phys, \ operator_r_def, f1, f2, m1, a, m2, istp, qr) - use testkern_qr_mod, only: testkern_qr_code - use quadrature""" in output) + use function_space_mod, only : basis, diff_basis + use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, \ +quadrature_xyoz_type + use testkern_qr_mod, only : testkern_qr_code""" in output) assert "mixed_kernel_mod" not in output assert LFRicBuild(tmpdir).code_compiles(psy) From fa0491bb8e0d06169458d0c7a0f48882cda2d9bb Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 28 May 2025 15:59:07 +0100 Subject: [PATCH 076/100] #2719 change get_kernel_schedule() to only return Schedules --- .../kernel_module_inline_trans.py | 9 +--- ...teration_boundaries_inside_kernel_trans.py | 17 +++++-- .../transformations/gocean_opencl_trans.py | 44 +++++++++------- src/psyclone/domain/lfric/lfric_kern.py | 51 ++++++++++--------- src/psyclone/gocean1p0.py | 11 ++-- src/psyclone/psyGen.py | 30 +++++++---- .../psyir/transformations/omp_task_trans.py | 9 ++-- .../kernel_module_inline_trans_test.py | 16 +++--- .../tests/domain/gocean/kernel/gokern_test.py | 9 ++-- .../transformations/globalstoargs_test.py | 12 ++--- .../gocean1p0_transformations_test.py | 4 +- ...ion_boundaries_inside_kernel_trans_test.py | 5 +- .../tests/domain/lfric/lfric_kern_test.py | 17 +++---- .../lfric_transformations_test.py | 6 +-- src/psyclone/tests/psyGen_test.py | 3 +- .../fparser2_find_or_create_symbol_test.py | 2 +- .../tests/psyir/nodes/routine_test.py | 4 +- .../kernel_transformation_test.py | 5 +- src/psyclone/transformations.py | 14 ++--- 19 files changed, 142 insertions(+), 126 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index a03ec409d8..9909e335e8 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -316,8 +316,7 @@ def _get_psyir_to_inline(node): # Where mixed-precision kernels are supported (e.g. in LFRic) the # call to get_kernel_schedule() will return the one which has an # interface matching the arguments in the call. - interface_sym, routines = node.get_kernel_schedule() - return (routines, interface_sym) + return (node.get_kernel_schedule(), node.get_interface_symbol()) # We have a generic routine call. routines = node.get_callees() @@ -614,8 +613,4 @@ def apply(self, node, options=None): if kern.name == node.name: kern.module_inline = True # pylint: disable=protected-access - kern._kern_schedules = updated_routines - if interface_sym: - kern._interface_symbol = ( - updated_routines[0].scope.symbol_table.lookup( - interface_sym.name)) + kern._schedules = updated_routines diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index b3414d40ee..fe815ae1ac 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -100,7 +100,8 @@ def validate(self, node, options=None): :param options: a dictionary with options for transformations. :type options: Optional[Dict[str, Any]] - :raises TransformationError: if the node is not a GOKern. + :raises TransformationError: if the node is not a GOKern or has more + than one implementation. ''' if not isinstance(node, GOKern): @@ -109,6 +110,13 @@ def validate(self, node, options=None): f"can only be applied to 'GOKern' nodes, but found " f"'{type(node).__name__}'.") + kschedules = node.get_kernel_schedule() + # GOcean Kernels must have a single implementation. + if len(kschedules) != 1: + raise TransformationError( + f"GOcean kernels must have a single implementation but " + f"'{node.name}' corresponds to an interface.") + def apply(self, node, options=None): '''Apply this transformation to the supplied node. @@ -179,10 +187,9 @@ def apply(self, node, options=None): inner_loop.iteration_space = "go_all_pts" outer_loop.iteration_space = "go_all_pts" - # Update Kernel - _, kschedules = node.get_kernel_schedule() - # GOcean Kernels must have a single implementation. - kschedule = kschedules[0] + # Update Kernel. Validate has checked that it's not polymorphic. + kschedule = node.get_kernel_schedule()[0] + kernel_st = kschedule.symbol_table iteration_indices = kernel_st.iteration_indices data_arguments = kernel_st.data_arguments diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index 2945b4f707..08121e4451 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -113,27 +113,27 @@ def validate(self, node, options=None): :type node: :py:class:`psyclone.psyGen.InvokeSchedule` :param options: a dictionary with options for transformations. :type options: dict of str:value or None - :param bool options["enable_profiling"]: whether or not to set up the \ + :param bool options["enable_profiling"]: whether or not to set up the OpenCL environment with the profiling option enabled. - :param bool options["out_of_order"]: whether or not to set up the \ + :param bool options["out_of_order"]: whether or not to set up the OpenCL environment with the out_of_order option enabled. - :param bool options["end_barrier"]: whether or not to add an OpenCL \ + :param bool options["end_barrier"]: whether or not to add an OpenCL barrier at the end of the transformed invoke. - :raises TransformationError: if the InvokeSchedule is not for the \ - GOcean1.0 API. - :raises TransformationError: if any of the kernels have arguments \ - which are passed as a literal. + :raises TransformationError: if the InvokeSchedule is not for the + GOcean API. + :raises TransformationError: if any of the kernels have arguments + which are passed as a literal. :raises TransformationError: if any of the provided options is invalid. - :raises TransformationError: if any of the provided options is not \ - compatible with a previous OpenCL - environment. - :raises TransformationError: if any kernel in this invoke has a \ - global variable used by an import. - :raises TransformationError: if any kernel does not iterate over \ - the whole grid. + :raises TransformationError: if any of the provided options is not + compatible with a previous OpenCL environment. + :raises TransformationError: if any kernel in this invoke has more than + one implementation (corresponds to an interface). + :raises TransformationError: if any kernel in this invoke has a + global variable used by an import. + :raises TransformationError: if any kernel does not iterate over + the whole grid. ''' - if isinstance(node, InvokeSchedule): if not isinstance(node, GOInvokeSchedule): raise TransformationError( @@ -195,9 +195,15 @@ def validate(self, node, options=None): # type information). for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) - _, kschedules = kern.get_kernel_schedule() + + kscheds = kern.get_kernel_schedule() # GOcean Kernels must have a single implementation. - ksched = kschedules[0] + if len(kscheds) > 1: + raise TransformationError( + f"GOcean kernels must have a single implementation but " + f"'{node.name}' corresponds to an interface.") + ksched = kscheds[0] + global_variables = set(ksched.symbol_table.imported_symbols) prec_symbols = set(ksched.symbol_table.precision_datasymbols) if global_variables.difference(prec_symbols): @@ -757,9 +763,9 @@ def _insert_kernel_code_in_opencl_file(self, kernel): # Create a copy of the kernel and remove precision symbols since they # are not supported in the OpenCL backend. - _, schedules = kernel.get_kernel_schedule() # validate() has checked that the kernel is not polymorphic. - kernel_copy = schedules[0].copy() + schedule = kernel.get_kernel_schedule()[0] + kernel_copy = schedule.copy() symtab = kernel_copy.symbol_table # TODO #898: Removing symbols is not properly supported by PSyIR diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index f5ca0210a1..bb4dc6257e 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -41,7 +41,7 @@ from collections import OrderedDict from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Union from psyclone.configuration import Config from psyclone.core import AccessType @@ -59,8 +59,9 @@ from psyclone.psyir.nodes import ( Loop, Literal, Reference, KernelSchedule, Container, Routine) from psyclone.psyir.symbols import ( - DataSymbol, ScalarType, ArrayType, UnsupportedFortranType, DataTypeSymbol, - UnresolvedType, ContainerSymbol, INTEGER_TYPE, UnresolvedInterface) + DataSymbol, GenericInterfaceSymbol, ScalarType, ArrayType, DataTypeSymbol, + UnresolvedType, ContainerSymbol, INTEGER_TYPE, UnresolvedInterface, + UnsupportedFortranType) class LFRicKern(CodedKern): @@ -780,28 +781,36 @@ def gen_stub(self) -> Container: return stub_module + def get_interface_symbol(self) -> Union[GenericInterfaceSymbol, None]: + ''' + :returns: the interface symbol for this kernel if it is polymorphic, + None otherwise. + ''' + kscheds = self.get_kernel_schedule() + if len(kscheds) == 1: + return None + cntr = kscheds[0].ancestor(Container) + return cntr.symbol_table.lookup(self.name) + def get_kernel_schedule(self): - '''Returns a PSyIR Schedule representing the kernel code. The base - class creates the PSyIR schedule on first invocation which is - then checked for consistency with the kernel metadata - here. The Schedule is just generated on first invocation, this - allows us to retain transformations that may subsequently be - applied to the Schedule. + '''Returns the PSyIR Schedule(s) representing the kernel code. The base + class creates the PSyIR schedule(s) on first invocation which is then + checked for consistency with the kernel metadata here. The Schedule is + just generated on first invocation, this allows us to retain + transformations that may subsequently be applied to the Schedule(s). Once issue #935 is implemented, this routine will return the PSyIR Schedule using LFRic-specific PSyIR where possible. - :returns: the Symbol defining the interface to this kernel (if it is - polymorphic) and a list of the Schedule(s) representing the kernel - code. - :rtype: tuple[Optional[:py:class:`psyclone.psyir.symbols.Symbol`], - list[:py:class:`psyclone.psyGen.KernelSchedule`]] + :returns: the Schedule(s) representing the kernel implementation. + :rtype: list[:py:class:`psyclone.psyGen.KernelSchedule`] - :raises GenerationError: if 0 or >1 subroutines matching this kernel + :raises InternalError: if no subroutines matching this kernel can be found in the parse tree of the associated source code. + ''' - if self._kern_schedules: - return self._interface_symbol, self._kern_schedules + if self._schedules: + return self._schedules # Check for a local implementation of this kernel first. container = self.ancestor(Container) @@ -833,10 +842,6 @@ class creates the PSyIR schedule on first invocation which is f"Failed to find any routines for Kernel '{self.name}'. " f"Source of Kernel is:\n{self.ast}") - self._interface_symbol = None - if len(routines) > 1: - self._interface_symbol = container.symbol_table.lookup(self.name) - new_schedules = [] for routine in routines[:]: # TODO #935 - replace the PSyIR argument data symbols with LFRic @@ -850,9 +855,9 @@ class creates the PSyIR schedule on first invocation which is routine.replace_with(ksched) new_schedules.append(ksched) - self._kern_schedules = new_schedules + self._schedules = new_schedules - return self._interface_symbol, self._kern_schedules + return self._schedules def validate_kernel_code_args(self, table): '''Check that the arguments in the kernel code match the expected diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index 0e8f1a839c..302bb8636c 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1084,14 +1084,13 @@ def get_kernel_schedule(self): list comprising just one Schedule. :returns: a schedule representing the GOcean kernel code. - :rtype: tuple[NoneType, - list[:py:class:`psyclone.gocean1p0.GOKernelSchedule`]] + :rtype: list[:py:class:`psyclone.gocean1p0.GOKernelSchedule`] :raises GenerationError: if there is a problem raising the language- level PSyIR of this kernel to GOcean PSyIR. ''' - if self._kern_schedules: - return None, self._kern_schedules + if self._schedules: + return self._schedules # Construct the PSyIR of the Fortran parse tree. astp = Fparser2Reader() @@ -1112,9 +1111,9 @@ def get_kernel_schedule(self): # We know the above loop will find the named routine because the # previous raising transformation would have failed otherwise. # pylint: disable=undefined-loop-variable - self._kern_schedules = [routine] + self._schedules = [routine] - return None, self._kern_schedules + return self._schedules class GOKernelArguments(Arguments): diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 75a796f013..e98c503a57 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1340,8 +1340,8 @@ def __init__(self, KernelArguments, call, parent=None, check=True): self._module_code = call.ktype._ast self._kernel_code = call.ktype.procedure self._fp2_ast = None #: The fparser2 AST for the kernel - self._kern_schedules = None #: PSyIR schedule(s) for the kernel - self._interface_symbol = None + #: PSyIR schedule(s) for the kernel + self._schedules = None #: Whether or not this kernel has been transformed self._modified = False #: Whether or not to in-line this kernel into the module containing @@ -1350,16 +1350,23 @@ def __init__(self, KernelArguments, call, parent=None, check=True): self._opencl_options = {'local_size': 64, 'queue_number': 1} self.arg_descriptors = call.ktype.arg_descriptors + def get_interface_symbol(self) -> None: + ''' + By default, a Kern is not polymorphic and therefore has no interface + symbol. + + ''' + return None + def get_kernel_schedule(self): ''' - Returns a PSyIR Schedule representing the kernel code. The Schedule - is just generated on first invocation, this allows us to retain - transformations that may subsequently be applied to the Schedule. + Returns the PSyIR Schedule(s) representing the kernel code. The + Schedules are just generated on first invocation, this allows us to + retain transformations that may subsequently be applied to the + Schedule(s). - :returns: Interface symbol (if any) and Schedule(s) representing the - kernel code. - :rtype: tuple[:py:class:`psyclone.psyir.symbols.Symbol`, - list[:py:class:`psyclone.psyir.nodes.KernelSchedule`]] + :returns: Schedule(s) representing the kernel code. + :rtype: list[:py:class:`psyclone.psyir.nodes.KernelSchedule`] :raises NotImplementedError: must be overridden in sub-class. @@ -1665,7 +1672,7 @@ def rename_and_write(self): # Start from the root of the schedule as we want to output # any module information surrounding the kernel subroutine # as well as the subroutine itself. - _, schedules = self.get_kernel_schedule() + schedules = self.get_kernel_schedule() new_kern_code = fortran_writer(schedules[0].root) fll = FortLineLength() new_kern_code = fll.process(new_kern_code) @@ -1705,7 +1712,7 @@ def _rename_psyir(self, suffix): ''' # We need to get the kernel schedule before modifying self.name. - interface_sym, kern_schedules = self.get_kernel_schedule() + kern_schedules = self.get_kernel_schedule() container = kern_schedules[0].ancestor(Container) # Use the suffix to create a new kernel name. This will @@ -1715,6 +1722,7 @@ def _rename_psyir(self, suffix): # If the kernel is polymorphic, we can just change the name of # the interface. + interface_sym = self.get_interface_symbol() if interface_sym: orig_kern_name = interface_sym.name new_kern_name = self._new_name(orig_kern_name, suffix, "_code") diff --git a/src/psyclone/psyir/transformations/omp_task_trans.py b/src/psyclone/psyir/transformations/omp_task_trans.py index e26c2b8112..30519c6241 100644 --- a/src/psyclone/psyir/transformations/omp_task_trans.py +++ b/src/psyclone/psyir/transformations/omp_task_trans.py @@ -112,11 +112,14 @@ def validate(self, node, options=None, **kwargs): for kern in kerns: kintrans.validate(kern) - cond_trans.validate(kern.get_kernel_schedule()) + routines = kern.get_kernel_schedule() + for routine in routines: + cond_trans.validate(routine) # We need to apply these transformations to ensure we can # validate the InlineTrans kintrans.apply(kern) - cond_trans.apply(kern.get_kernel_schedule()) + for routine in routines: + cond_trans.apply(routine) kern.lower_to_language_level() calls = node_copy.walk(Call) @@ -172,7 +175,7 @@ def _inline_kernels(self, node): intrans = InlineTrans() for kern in kerns: kintrans.apply(kern) - _, schedules = kern.get_kernel_schedule() + schedules = kern.get_kernel_schedule() for sched in schedules: cond_trans.apply(sched) kern.lower_to_language_level() diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index c1548c091a..98e4b73251 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -103,7 +103,7 @@ def test_validate_with_imported_subroutine_call(): schedule = invoke.schedule kern_call = schedule.walk(CodedKern)[0] # Create a call to made up subroutine and module symbols - _, kern_schedules = kern_call.get_kernel_schedule() + kern_schedules = kern_call.get_kernel_schedule() kern_schedule = kern_schedules[0] mymod = kern_schedule.symbol_table.new_symbol( "mymod", @@ -162,7 +162,7 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - _, kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_kernel_schedule() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild(block) @@ -179,7 +179,7 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - _, kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_kernel_schedule() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild(block) @@ -196,7 +196,7 @@ def test_validate_no_inline_global_var(parser): # But make sure that an IntrinsicCall routine name is not considered # a global symbol, as they are implicitly declared everywhere - _, kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_kernel_schedule() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild( @@ -262,7 +262,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) + monkeypatch.setattr(kern_call, "_schedules", [routine]) # and try to apply the transformation inline_trans = KernelModuleInlineTrans() @@ -285,7 +285,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) + monkeypatch.setattr(kern_call, "_schedules", [routine]) # and try to apply the transformation with pytest.raises(TransformationError) as err: @@ -307,7 +307,7 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): end module my_mod ''') routine = psyir.walk(Routine)[0] - monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) + monkeypatch.setattr(kern_call, "_schedules", [routine]) container = kern_call.ancestor(Container) assert "compute_cv_code" not in container.symbol_table @@ -411,7 +411,7 @@ def test_validate_nested_scopes(fortran_reader, monkeypatch): # Put a new, different symbol (with the same name) into the table of the # parent Container. routine.parent.scope.symbol_table.add(DataSymbol("a", REAL_TYPE)) - monkeypatch.setattr(kern_call, "_kern_schedules", [routine]) + monkeypatch.setattr(kern_call, "_schedules", [routine]) # The transformation should succeed (because the symbol named 'a' is # actually local to the routine. However, the dependence analysis thinks it diff --git a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py index 69c7fbbc50..fe4882cc7f 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py @@ -99,19 +99,18 @@ def test_gok_get_kernel_schedule(): psy = PSyFactory(API, distributed_memory=False).create(invoke_info) schedule = psy.invokes.invoke_list[0].schedule kern = schedule.walk(GOKern)[0] - assert kern._kern_schedules is None - sym, scheds = kern.get_kernel_schedule() - assert sym is None + assert kern._schedules is None + scheds = kern.get_kernel_schedule() assert isinstance(scheds, list) assert len(scheds) == 1 sched = scheds[0] assert isinstance(sched, GOKernelSchedule) # A second call should just return the previously-obtained schedule. - sym, scheds2 = kern.get_kernel_schedule() + scheds2 = kern.get_kernel_schedule() assert scheds2[0] is sched # Check that the expected error is raised if the subroutine that # implements the kernel cannot be found. - kern._kern_schedules = None + kern._schedules = None # Remove the subroutine that implements the kernel from the Fortran # parse tree. subs = walk(kern.ast, Fortran2003.Subroutine_Subprogram) diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 5cb86511fb..87b5d9341b 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -157,16 +157,16 @@ def set_to_real(variable): # 3) Has converted the Kernel Schedule symbol into an argument which is # in also the last position - ksymbol = kernel.get_kernel_schedule().symbol_table.lookup("rdt") + ksymbol = kernel.get_kernel_schedule()[0].symbol_table.lookup("rdt") assert ksymbol.is_argument - assert kernel.get_kernel_schedule().symbol_table.argument_list[-1] == \ + assert kernel.get_kernel_schedule()[0].symbol_table.argument_list[-1] == \ ksymbol - assert len(kernel.get_kernel_schedule().symbol_table.argument_list) == \ + assert len(kernel.get_kernel_schedule()[0].symbol_table.argument_list) == \ len(kernel.args) + 2 # GOcean kernels have 2 implicit arguments # Check the kernel code is generated as expected fwriter = FortranWriter() - kernel_code = fwriter(kernel.get_kernel_schedule()) + kernel_code = fwriter(kernel.get_kernel_schedule()[0]) assert "subroutine kernel_with_use_code(ji,jj,istep,ssha,tmask,rdt)" \ in kernel_code assert "real, intent(inout) :: rdt" in kernel_code @@ -210,7 +210,7 @@ def create_data_symbol(arg): trans.apply(kernel) fwriter = FortranWriter() - _, kernels = kernel.get_kernel_schedule() + kernels = kernel.get_kernel_schedule() kernel_code = fwriter(kernels[0]) assert ("subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " @@ -289,7 +289,7 @@ def create_data_symbol(arg): monkeypatch.setattr(DataSymbol, "resolve_type", create_data_symbol) for num, kernel in enumerate(invoke.schedule.coded_kernels()): - _, kernels = kernel.get_kernel_schedule() + kernels = kernel.get_kernel_schedule() kschedule = kernels[0] trans.apply(kernel) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py index f66581dba7..82cc498829 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -1494,7 +1494,7 @@ def test_accroutinetrans_with_kern(fortran_writer, monkeypatch): assert rtrans.name == "ACCRoutineTrans" rtrans.apply(kern) # Check that there is a acc routine directive in the kernel - _, schedules = kern.get_kernel_schedule() + schedules = kern.get_kernel_schedule() code = fortran_writer(schedules[0]) assert "!$acc routine seq\n" in code @@ -1518,7 +1518,7 @@ def test_accroutinetrans_with_routine(fortran_writer): assert isinstance(kern, GOKern) rtrans = ACCRoutineTrans() assert rtrans.name == "ACCRoutineTrans" - _, routines = kern.get_kernel_schedule() + routines = kern.get_kernel_schedule() rtrans.apply(routines[0]) # Check that there is a acc routine directive in the routine code = fortran_writer(routines[0]) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index d3ef173133..485e6797c5 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -81,7 +81,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): # Add some name conflicting symbols in the Invoke and the Kernel kernel.ancestor(Container).symbol_table.new_symbol("xstop") - _, routines = kernel.get_kernel_schedule() + routines = kernel.get_kernel_schedule() ksched = routines[0] ksched.symbol_table.new_symbol("ystart") @@ -120,8 +120,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): assert kernel.arguments.args[-1].argument_type == "scalar" # Check that the kernel subroutine has been transformed: - _, kschedules = kernel.get_kernel_schedule() - kschedule = kschedules[0] + kschedule = kernel.get_kernel_schedule()[0] # - It has the boundary conditions mask assert isinstance(kschedule.children[0], IfBlock) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index 6e7e6c2aef..6292cd64fe 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -151,19 +151,18 @@ def test_get_kernel_schedule(monkeypatch): # matrix vector kernel kernel = schedule[2].loop_body[0] - assert kernel._kern_schedules is None + assert kernel._schedules is None - sym, kernel_schedules = kernel.get_kernel_schedule() - assert sym is None + kernel_schedules = kernel.get_kernel_schedule() assert len(kernel_schedules) == 1 assert isinstance(kernel_schedules[0], KernelSchedule) - assert kernel._kern_schedules[0] is kernel_schedules[0] + assert kernel._schedules[0] is kernel_schedules[0] - _, kernel_schedules_2 = kernel.get_kernel_schedule() + kernel_schedules_2 = kernel.get_kernel_schedule() assert kernel_schedules[0] is kernel_schedules_2[0] # Check the internal error for the case where we fail to get any # implementation for the kernel. - kernel._kern_schedules = None + kernel._schedules = None # Monkeypatch the frontend so that it just returns an empty Container. monkeypatch.setattr(Fparser2Reader, "generate_psyir", lambda _1, _2: Container("dummy_mod")) @@ -187,8 +186,8 @@ def test_get_kernel_schedule_same_container(monkeypatch): for kern in sched.walk(LFRicKern): mod_inline_trans.apply(kern) # Remove the cached schedule to force get_kernel_schedule() to search. - monkeypatch.setattr(kern, "_kern_schedules", None) - _, schedules = kern.get_kernel_schedule() + monkeypatch.setattr(kern, "_schedules", None) + schedules = kern.get_kernel_schedule() # The returned schedule should be the one in the local Container. assert schedules[0] in sched.ancestor(Container).walk(Routine) @@ -264,7 +263,7 @@ def test_validate_kernel_code_args(monkeypatch): schedule = psy.invokes.invoke_list[0].schedule # matrix vector kernel kernel = schedule[2].loop_body[0] - _, schedules = kernel.get_kernel_schedule() + schedules = kernel.get_kernel_schedule() sched = schedules[0] kernel.validate_kernel_code_args(sched.symbol_table) 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 b34e8bee8b..2574c152de 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -7686,8 +7686,7 @@ def test_kern_const_invalid_make_constant1(): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") - _, kernel_schedules = kernel.get_kernel_schedule() - kernel_schedule = kernel_schedules[0] + kernel_schedule = kernel.get_kernel_schedule()[0] symbol_table = kernel_schedule.symbol_table # Make the symbol table's argument list empty. We have to make sure that # the interface of any existing argument Symbols is set to @@ -7714,8 +7713,7 @@ def test_kern_const_invalid_make_constant2(): kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") kctrans = LFRicKernelConstTrans() - _, kernel_schedules = kernel.get_kernel_schedule() - kernel_schedule = kernel_schedules[0] + kernel_schedule = kernel.get_kernel_schedule()[0] symbol_table = kernel_schedule.symbol_table symbol = symbol_table._argument_list[7] # Expecting scalar integer. Set to array. diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 50468251ee..9e196e63e3 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -599,8 +599,7 @@ def test_kern_get_kernel_schedule(): psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) schedule = psy.invokes.invoke_list[0].schedule kern = schedule.children[0].loop_body[0] - sym, kern_schedules = kern.get_kernel_schedule() - assert sym is None + kern_schedules = kern.get_kernel_schedule() assert len(kern_schedules) == 1 assert isinstance(kern_schedules[0], KernelSchedule) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py index bd9002df61..ea8c063e1b 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py @@ -61,7 +61,7 @@ def test_find_or_create_unresolved_symbol(): api="gocean", idx=0) sched = invoke.schedule kernels = sched.walk(Kern) - _, kernel_schedules = kernels[0].get_kernel_schedule() + kernel_schedules = kernels[0].get_kernel_schedule() kernel_schedule = kernel_schedules[0] references = kernel_schedule.walk(Reference) diff --git a/src/psyclone/tests/psyir/nodes/routine_test.py b/src/psyclone/tests/psyir/nodes/routine_test.py index ad2e7d0290..7a6d494e5b 100644 --- a/src/psyclone/tests/psyir/nodes/routine_test.py +++ b/src/psyclone/tests/psyir/nodes/routine_test.py @@ -485,12 +485,12 @@ def test_check_outer_scope_accesses(config_instance): config_instance.include_paths = [] # Multiple wildcard imports are handled by bringing them into the routine # and so aren't a problem. - _, kschedules = kcall.get_kernel_schedule() + kschedules = kcall.get_kernel_schedule() kschedules[0].check_outer_scope_accesses(kcall, "Kernel") # Now try where there's only a single wildcard import so we know the origin # of the symbol. kcall0 = schedule.walk(CodedKern)[0] - _, kscheds = kcall0.get_kernel_schedule() + kscheds = kcall0.get_kernel_schedule() ctable = kscheds[0].ancestor(Container).symbol_table # To do this, we manually remove all ContainerSymbols apart from the one # from which 'go_wp' is imported. diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 506e2368f5..70115ec576 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -306,7 +306,6 @@ def test_transform_kern_with_interface(kernel_outputdir): assert LFRicBuild(kernel_outputdir).code_compiles(psy) kernels = sched.coded_kernels() rtrans.apply(kernels[1], options={"force": True}) - psy.gen assert LFRicBuild(kernel_outputdir).code_compiles(psy) @@ -443,7 +442,7 @@ def test_gpumixin_validate_no_call(): in str(err.value)) # The same error happens for unsupported GPU intrinsics - _, kschedules = kernel.get_kernel_schedule() + kschedules = kernel.get_kernel_schedule() call = kschedules[0].walk(Call)[0] call.replace_with( IntrinsicCall.create(IntrinsicCall.Intrinsic.GET_COMMAND)) @@ -469,7 +468,7 @@ def test_kernel_gpu_annotation_trans(rtrans, expected_directive, rtrans.apply(kern) # Check that the directive has been added to the kernel code - _, kschedules = kern.get_kernel_schedule() + kschedules = kern.get_kernel_schedule() code = fortran_writer(kschedules[0]) assert expected_directive in code diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index c0fee1e19c..e5e2be7df3 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -410,7 +410,7 @@ def validate_it_can_run_on_gpu(self, node, options): # or that the frontend failed to convert it into PSyIR) reraise it # as a TransformationError try: - _, kernel_schedules = node.get_kernel_schedule() + kernel_schedules = node.get_kernel_schedule() except Exception as error: raise TransformationError( f"Failed to create PSyIR for kernel '{node.name}'. " @@ -549,7 +549,7 @@ def apply(self, node, options=None): node.modified = True # Get the schedule representing the kernel subroutine - _, routines = node.get_kernel_schedule() + routines = node.get_kernel_schedule() else: routines = [node] @@ -2416,7 +2416,7 @@ def make_constant(symbol_table, arg_position, value, arg_list_info = KernCallArgList(kernel) arg_list_info.generate() try: - _, kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_kernel_schedule() except NotImplementedError as excinfo: raise TransformationError( f"Failed to parse kernel '{kernel.name}'. Error reported was " @@ -2766,8 +2766,8 @@ def apply(self, node, options=None): # Flag that the kernel has been modified node.modified = True - # Get the schedule representing the kernel subroutine - sym, routines = node.get_kernel_schedule() + # Get the schedule(s) representing the kernel subroutine + routines = node.get_kernel_schedule() else: routines = [node] @@ -3008,7 +3008,7 @@ def validate(self, node, options=None): # Check that there are no unqualified imports or undeclared symbols try: - _, kernels = node.get_kernel_schedule() + kernels = node.get_kernel_schedule() except SymbolError as err: raise TransformationError( f"Kernel '{node.name}' contains undeclared symbol: " @@ -3040,7 +3040,7 @@ def apply(self, node, options=None): ''' self.validate(node, options) - _, kernels = node.get_kernel_schedule() + kernels = node.get_kernel_schedule() # validate() has ensured that there is only one kernel routine. kernel = kernels[0] symtab = kernel.symbol_table From 68e56bd82938973ad605df02a52ecf866568e7de Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 28 May 2025 16:28:55 +0100 Subject: [PATCH 077/100] #2716 refactor _get_psyir_to_inline --- examples/lfric/scripts/gpu_offloading.py | 2 +- .../kernel_module_inline_trans.py | 39 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 7ed478c458..6869390b62 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -63,7 +63,7 @@ ] # We won't attempt to inline calls to routines with names that contain -# these strings. +# these strings (because they're not computationally important). INLINE_EXCLUSIONS = ["abort", "logging"] OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 9909e335e8..c40a65e52b 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -121,7 +121,7 @@ def validate(self, node, options=None): # Check that the PSyIR of the routine/kernel can be retrieved. try: - kernels, _ = KernelModuleInlineTrans._get_psyir_to_inline(node) + kernels = KernelModuleInlineTrans._get_psyir_to_inline(node) except Exception as error: raise TransformationError( f"{self.name} failed to retrieve PSyIR for {kern_or_call} " @@ -197,8 +197,8 @@ def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): if routine.name.lower() == kname.lower(): # Compare the routine to be inlined with the one that # is already present. - (new_rt, ) = self._prepare_code_to_inline([kernel_schedule]) - if routine == new_rt: + new_rts = self._prepare_code_to_inline([kernel_schedule]) + if len(new_rts) == 1 and routine == new_rts[0]: # It's the same so we can proceed (although all we need to # do is update the RoutineSymbol referenced by the Call.) return @@ -291,7 +291,7 @@ def _prepare_code_to_inline( return copied_routines @staticmethod - def _get_psyir_to_inline(node): + def _get_psyir_to_inline(node) -> list[Routine]: ''' Wrapper that gets the PSyIR of the routine or kernel corresponding to the call described by `node`. This supports calls to @@ -302,11 +302,7 @@ def _get_psyir_to_inline(node): :type node: :py:class:`psyclone.psyir.nodes.Call` | :py:class:`psyclone.psyGen.CodedKern` - :returns: the PSyIR of the routine implementation(s) and the associated - interface symbol if it is polymorphic. - :rtype: tuple[ - list[:py:class:`psyclone.psyir.nodes.Routine`], - :py:class:`psyclone.psyir.symbols.GenericInterfaceSymbol`)] + :returns: the PSyIR of the routine implementation(s) ''' # TODO #2054 - once CodedKern has been migrated so that it subclasses @@ -316,16 +312,10 @@ def _get_psyir_to_inline(node): # Where mixed-precision kernels are supported (e.g. in LFRic) the # call to get_kernel_schedule() will return the one which has an # interface matching the arguments in the call. - return (node.get_kernel_schedule(), node.get_interface_symbol()) + return node.get_kernel_schedule() # We have a generic routine call. - routines = node.get_callees() - caller_name = node.routine.name.lower() - interface_sym = None - if len(routines) > 1: - interface_sym = routines[0].symbol_table.lookup(caller_name) - - return (routines, interface_sym) + return node.get_callees() @staticmethod def _rm_imported_routine_symbol(symbol: Symbol, @@ -445,6 +435,11 @@ def apply(self, node, options=None): self.validate(node, options) + if isinstance(node, CodedKern): + caller_name = node.name + else: + caller_name = node.routine.symbol.name + # Get the PSyIR of the routine to module inline as well as the name # with which it is being called. # Note that we use the resolved callee subroutine name and not the @@ -453,15 +448,15 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - codes_to_inline, interface_sym = ( - KernelModuleInlineTrans._get_psyir_to_inline(node)) + codes_to_inline = KernelModuleInlineTrans._get_psyir_to_inline(node) + interface_sym = codes_to_inline[0].symbol_table.lookup( + caller_name) if len(codes_to_inline) > 1 else None + callsite_table = node.scope.symbol_table if interface_sym: - called_sym = callsite_table.lookup(interface_sym.name, - otherwise=None) + called_sym = callsite_table.lookup(caller_name, otherwise=None) else: - caller_name = codes_to_inline[0].name for routine in codes_to_inline: # N.B.in a PSyKAl DSL, we won't have a RoutineSymbol for the # Kernel that is being called, so we look it up instead of From fedbb7df3e4e7566be55dfdfc08b35e4cf81624d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 28 May 2025 17:12:48 +0100 Subject: [PATCH 078/100] #2716 fix bug in get_callees --- src/psyclone/psyir/nodes/call.py | 3 +- src/psyclone/tests/psyir/nodes/call_test.py | 41 ++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 529cab5a4b..ee30b5401d 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -486,12 +486,13 @@ def get_callees(self): cursor = table.node have_codeblock = False while cursor: + # We want to look in both Containers and FileContainers. if isinstance(cursor, Container): routines = [] for name in cursor.resolve_routine(rsym.name): # Since we're looking in the local Container, the # target is permitted to be private. - psyir = cursor.find_routine_psyir(rsym.name, + psyir = cursor.find_routine_psyir(name, allow_private=True) if psyir: routines.append(psyir) diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index 35225ceedc..51d6bb2ccc 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -48,7 +48,7 @@ from psyclone.psyir.nodes.node import colored from psyclone.psyir.symbols import ( ArrayType, INTEGER_TYPE, ContainerSymbol, DataSymbol, NoType, - RoutineSymbol, REAL_TYPE, SymbolError) + RoutineSymbol, REAL_TYPE, SymbolError, UnresolvedInterface) class SpecialCall(Call): @@ -680,6 +680,45 @@ def test_call_get_callees_local(fortran_reader): assert result == [psyir.walk(Routine)[1]] +def test_call_get_callees_local_unresolved_interface(fortran_reader, + monkeypatch): + ''' + Check that get_callees() works as expected when the target of the Call + is an unresolved interface that exists in the same Container as the call + site. This shouldn't ever occur in practise so we use monkeypatch. + + ''' + code = ''' +module some_mod + implicit none + integer :: luggage + interface polymorph + module procedure :: morph1, morph2 + end interface +contains + subroutine top() + luggage = 0 + call polymorph(luggage) + end subroutine top + + subroutine morph1(arg) + integer, intent(inout) :: arg + end subroutine morph1 + + subroutine morph2(arg) + real, intent(inout) :: arg + end subroutine morph2 +end module some_mod''' + psyir = fortran_reader.psyir_from_source(code) + call = psyir.walk(Call)[0] + # Monkeypatch the called symbol so that it appears to be unresolved. + monkeypatch.setattr(call.routine.symbol, "_interface", + UnresolvedInterface()) + result = call.get_callees() + assert len(result) == 2 + assert result == psyir.walk(Routine)[1:] + + def test_call_get_callees_local_file_container(fortran_reader): ''' Test that get_callees() succeeds when the called routine is within From 96b438cbc5ead2aa785c44ff7792429b4eba5a1d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 28 May 2025 17:16:48 +0100 Subject: [PATCH 079/100] #2716 update examples --- examples/gocean/eg3/ocl_trans.py | 2 +- examples/lfric/eg15/matvec_opt.py | 2 +- examples/lfric/scripts/gpu_offloading.py | 2 +- examples/lfric/scripts/kernel_print.py | 2 +- examples/xdsl/backend/xdsl.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index 6aab541d6e..59b496ddd9 100644 --- a/examples/gocean/eg3/ocl_trans.py +++ b/examples/gocean/eg3/ocl_trans.py @@ -62,7 +62,7 @@ def trans(psyir): move_boundaries_trans.apply(kern) # Change the syntax to remove the return statements introduced by the # previous transformation - _, kschedules = kern.get_kernel_schedule() + kschedules = kern.get_kernel_schedule() # NOTE: we assume the kernel is not polymorphic and thus there is # only one schedule associated with it. fold_trans.apply(kschedules[0]) diff --git a/examples/lfric/eg15/matvec_opt.py b/examples/lfric/eg15/matvec_opt.py index 61d1e379a5..83f2bc23a3 100644 --- a/examples/lfric/eg15/matvec_opt.py +++ b/examples/lfric/eg15/matvec_opt.py @@ -91,7 +91,7 @@ def trans(psyir): for kernel in psyir.coded_kernels(): if kernel.name.lower() == "matrix_vector_kernel_code": - _, kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_kernel_schedule() # For simplicity, ASSUME that the kernel is not polymorphic and # thus only has one schedule. kernel_schedule = kernel_schedules[0] diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 6869390b62..96ca43c8b5 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -84,7 +84,7 @@ def _inline_calls(kern): matrans = Matmul2CodeTrans() if isinstance(kern, CodedKern): - _, scheds = kern.get_kernel_schedule() + scheds = kern.get_kernel_schedule() else: scheds = [kern] for sched in scheds: diff --git a/examples/lfric/scripts/kernel_print.py b/examples/lfric/scripts/kernel_print.py index b6e1333509..66556de1b9 100644 --- a/examples/lfric/scripts/kernel_print.py +++ b/examples/lfric/scripts/kernel_print.py @@ -55,7 +55,7 @@ def trans(psyir): # Loop over all of the Kernels Calls for kernel in psyir.coded_kernels(): try: - _, kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_kernel_schedule() for ksched in kernel_schedules: if ksched not in already_printed: kern = fortran_writer(ksched) diff --git a/examples/xdsl/backend/xdsl.py b/examples/xdsl/backend/xdsl.py index 4fcd476818..3d601a2625 100644 --- a/examples/xdsl/backend/xdsl.py +++ b/examples/xdsl/backend/xdsl.py @@ -439,7 +439,7 @@ def checkIfStringIsType(self, string, typ): def nemokern_node(self, node): exec_statements = [] - _, schedules = node.get_kernel_schedule() + schedules = node.get_kernel_schedule() # IGNORE polymorphic routines. schedule = schedules[0] for child in schedule.children: From 4929ac3c7430025039f04854b47b40a4307536ac Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 29 May 2025 12:11:11 +0100 Subject: [PATCH 080/100] #2716 fix typing capitalisation for 3.8 --- .../domain/common/transformations/kernel_module_inline_trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index c40a65e52b..841819006e 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -291,7 +291,7 @@ def _prepare_code_to_inline( return copied_routines @staticmethod - def _get_psyir_to_inline(node) -> list[Routine]: + def _get_psyir_to_inline(node) -> List[Routine]: ''' Wrapper that gets the PSyIR of the routine or kernel corresponding to the call described by `node`. This supports calls to From 5a8514505c2ac78f98b046dcb8455d1ac6e74864 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 29 May 2025 20:35:01 +0100 Subject: [PATCH 081/100] #2716 fix missed lines --- ...teration_boundaries_inside_kernel_trans.py | 124 ++++++++---------- .../transformations/gocean_opencl_trans.py | 37 +++--- 2 files changed, 73 insertions(+), 88 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index fe815ae1ac..f5f9afdade 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -100,8 +100,7 @@ def validate(self, node, options=None): :param options: a dictionary with options for transformations. :type options: Optional[Dict[str, Any]] - :raises TransformationError: if the node is not a GOKern or has more - than one implementation. + :raises TransformationError: if the node is not a GOKern. ''' if not isinstance(node, GOKern): @@ -110,13 +109,6 @@ def validate(self, node, options=None): f"can only be applied to 'GOKern' nodes, but found " f"'{type(node).__name__}'.") - kschedules = node.get_kernel_schedule() - # GOcean Kernels must have a single implementation. - if len(kschedules) != 1: - raise TransformationError( - f"GOcean kernels must have a single implementation but " - f"'{node.name}' corresponds to an interface.") - def apply(self, node, options=None): '''Apply this transformation to the supplied node. @@ -187,64 +179,64 @@ def apply(self, node, options=None): inner_loop.iteration_space = "go_all_pts" outer_loop.iteration_space = "go_all_pts" - # Update Kernel. Validate has checked that it's not polymorphic. - kschedule = node.get_kernel_schedule()[0] - - kernel_st = kschedule.symbol_table - iteration_indices = kernel_st.iteration_indices - data_arguments = kernel_st.data_arguments - - # Create new symbols and insert them as kernel arguments at the end of - # the kernel argument list - xstart_symbol = kernel_st.new_symbol( - "xstart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, - interface=ArgumentInterface(ArgumentInterface.Access.READ)) - xstop_symbol = kernel_st.new_symbol( - "xstop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, - interface=ArgumentInterface(ArgumentInterface.Access.READ)) - ystart_symbol = kernel_st.new_symbol( - "ystart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, - interface=ArgumentInterface(ArgumentInterface.Access.READ)) - ystop_symbol = kernel_st.new_symbol( - "ystop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, - interface=ArgumentInterface(ArgumentInterface.Access.READ)) - kernel_st.specify_argument_list( - iteration_indices + data_arguments + - [xstart_symbol, xstop_symbol, ystart_symbol, ystop_symbol]) - - # Create boundary masking conditions - condition1 = BinaryOperation.create( - BinaryOperation.Operator.LT, - Reference(iteration_indices[0]), - Reference(xstart_symbol)) - condition2 = BinaryOperation.create( - BinaryOperation.Operator.GT, - Reference(iteration_indices[0]), - Reference(xstop_symbol)) - condition3 = BinaryOperation.create( - BinaryOperation.Operator.LT, - Reference(iteration_indices[1]), - Reference(ystart_symbol)) - condition4 = BinaryOperation.create( - BinaryOperation.Operator.GT, - Reference(iteration_indices[1]), - Reference(ystop_symbol)) - - condition = BinaryOperation.create( - BinaryOperation.Operator.OR, - BinaryOperation.create( + # Update Kernel implementation(s). + for kschedule in node.get_kernel_schedule(): + + kernel_st = kschedule.symbol_table + iteration_indices = kernel_st.iteration_indices + data_arguments = kernel_st.data_arguments + + # Create new symbols and insert them as kernel arguments at the + # end of the kernel argument list + xstart_symbol = kernel_st.new_symbol( + "xstart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + interface=ArgumentInterface(ArgumentInterface.Access.READ)) + xstop_symbol = kernel_st.new_symbol( + "xstop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + interface=ArgumentInterface(ArgumentInterface.Access.READ)) + ystart_symbol = kernel_st.new_symbol( + "ystart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + interface=ArgumentInterface(ArgumentInterface.Access.READ)) + ystop_symbol = kernel_st.new_symbol( + "ystop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + interface=ArgumentInterface(ArgumentInterface.Access.READ)) + kernel_st.specify_argument_list( + iteration_indices + data_arguments + + [xstart_symbol, xstop_symbol, ystart_symbol, ystop_symbol]) + + # Create boundary masking conditions + condition1 = BinaryOperation.create( + BinaryOperation.Operator.LT, + Reference(iteration_indices[0]), + Reference(xstart_symbol)) + condition2 = BinaryOperation.create( + BinaryOperation.Operator.GT, + Reference(iteration_indices[0]), + Reference(xstop_symbol)) + condition3 = BinaryOperation.create( + BinaryOperation.Operator.LT, + Reference(iteration_indices[1]), + Reference(ystart_symbol)) + condition4 = BinaryOperation.create( + BinaryOperation.Operator.GT, + Reference(iteration_indices[1]), + Reference(ystop_symbol)) + + condition = BinaryOperation.create( BinaryOperation.Operator.OR, - condition1, - condition2), - BinaryOperation.create( - BinaryOperation.Operator.OR, - condition3, - condition4) - ) - - # Insert the conditional mask as the first statement of the kernel - if_statement = IfBlock.create(condition, [Return()]) - kschedule.children.insert(0, if_statement) + BinaryOperation.create( + BinaryOperation.Operator.OR, + condition1, + condition2), + BinaryOperation.create( + BinaryOperation.Operator.OR, + condition3, + condition4) + ) + + # Insert the conditional mask as the first statement of the kernel + if_statement = IfBlock.create(condition, [Return()]) + kschedule.children.insert(0, if_statement) # For Sphinx AutoAPI documentation generation diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index 08121e4451..bd30dd165a 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -127,12 +127,11 @@ def validate(self, node, options=None): :raises TransformationError: if any of the provided options is invalid. :raises TransformationError: if any of the provided options is not compatible with a previous OpenCL environment. - :raises TransformationError: if any kernel in this invoke has more than - one implementation (corresponds to an interface). :raises TransformationError: if any kernel in this invoke has a global variable used by an import. :raises TransformationError: if any kernel does not iterate over the whole grid. + ''' if isinstance(node, InvokeSchedule): if not isinstance(node, GOInvokeSchedule): @@ -196,26 +195,20 @@ def validate(self, node, options=None): for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) - kscheds = kern.get_kernel_schedule() - # GOcean Kernels must have a single implementation. - if len(kscheds) > 1: - raise TransformationError( - f"GOcean kernels must have a single implementation but " - f"'{node.name}' corresponds to an interface.") - ksched = kscheds[0] - - global_variables = set(ksched.symbol_table.imported_symbols) - prec_symbols = set(ksched.symbol_table.precision_datasymbols) - if global_variables.difference(prec_symbols): - names = sorted([sym.name for sym in - global_variables.difference(prec_symbols)]) - raise TransformationError( - f"The Symbol Table for kernel '{kern.name}' contains the " - f"following symbols with 'global' scope: {names}. An " - f"OpenCL kernel cannot call other kernels and all of the " - f"data it accesses must be passed by argument. Use the " - f"KernelImportsToArguments transformation to convert such " - f"symbols to kernel arguments first.") + for ksched in kern.get_kernel_schedule(): + + global_variables = set(ksched.symbol_table.imported_symbols) + prec_symbols = set(ksched.symbol_table.precision_datasymbols) + if global_variables.difference(prec_symbols): + names = sorted([sym.name for sym in + global_variables.difference(prec_symbols)]) + raise TransformationError( + f"The Symbol Table for kernel '{kern.name}' contains " + f"the following symbols with 'global' scope: {names}. " + f"An OpenCL kernel cannot call other kernels and all " + f"of the data it accesses must be passed by argument. " + f"Use the KernelImportsToArguments transformation to " + f"convert such symbols to kernel arguments first.") # In OpenCL all kernel loops should iterate the whole grid for kernel in node.kernels(): From 35ca76722033a48c8718c58bee467b70f7ff81ae Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 29 May 2025 21:14:30 +0100 Subject: [PATCH 082/100] #2716 improve unit coverage --- .../tests/domain/lfric/lfric_kern_test.py | 25 ++++++++++++++++--- src/psyclone/tests/psyGen_test.py | 4 ++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index 6292cd64fe..b5940831eb 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -56,8 +56,9 @@ from psyclone.psyGen import PSyFactory from psyclone.psyir.frontend.fparser2 import Fparser2Reader from psyclone.psyir.nodes import Container, KernelSchedule, Reference, Routine -from psyclone.psyir.symbols import ArgumentInterface, DataSymbol, REAL_TYPE, \ - INTEGER_TYPE, ArrayType +from psyclone.psyir.symbols import ( + ArgumentInterface, ArrayType, DataSymbol, GenericInterfaceSymbol, + INTEGER_TYPE, REAL_TYPE) from psyclone.tests.utilities import get_invoke from psyclone.transformations import LFRicColourTrans from psyclone.psyir.backend.visitor import VisitorError @@ -157,6 +158,8 @@ def test_get_kernel_schedule(monkeypatch): assert len(kernel_schedules) == 1 assert isinstance(kernel_schedules[0], KernelSchedule) assert kernel._schedules[0] is kernel_schedules[0] + # Not a polymorphic kernel so has no interface symbol + assert kernel.get_interface_symbol() is None kernel_schedules_2 = kernel.get_kernel_schedule() assert kernel_schedules[0] is kernel_schedules_2[0] @@ -192,11 +195,27 @@ def test_get_kernel_schedule_same_container(monkeypatch): assert schedules[0] in sched.ancestor(Container).walk(Routine) +def test_get_kernel_schedule_mixed_precision(): + ''' + Test that get_kernel_schedule() and get_interface_symbol() work for a + mixed-precision kernel. + + ''' + _, invoke = get_invoke("26.8_mixed_precision_args.f90", TEST_API, + name="invoke_0", dist_mem=False) + sched = invoke.schedule + for kern in sched.walk(LFRicKern, stop_type=LFRicKern): + assert len(kern.get_kernel_schedule()) == 2 + isym = kern.get_interface_symbol() + assert isinstance(isym, GenericInterfaceSymbol) + assert isym.name == "mixed_code" + + @pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" " implementations of a polymorphic kernel. We need to " "put back (and fix) the ability to resolve which " "implementation is being called.") -def test_get_kernel_schedule_mixed_precision(): +def test_get_kernel_schedule_mixed_precision_match(): ''' Test that we can get the correct schedule for a mixed-precision kernel. diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 9e196e63e3..a8ca2f37ac 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -838,7 +838,8 @@ def test_kern_children_validation(): def test_codedkern_get_kernel_schedule(monkeypatch): ''' Check that CodedKern.get_kernel_schedule() raises a NotImplementedError - (as it must be implemented by sub-classes). + (as it must be implemented by sub-classes). Also check that + get_interface_symbol() returns None. ''' ast = fpapi.parse(FAKE_KERNEL_METADATA, ignore_comments=False) @@ -850,6 +851,7 @@ def test_codedkern_get_kernel_schedule(monkeypatch): kern.get_kernel_schedule() assert ("get_kernel_schedule() must be overridden in class " in str(err.value)) + assert kern.get_interface_symbol() is None def test_inlinedkern_children_validation(): From 76471fad318623d8105c16384d122ae2df1ef236 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 3 Jun 2025 10:32:44 +0100 Subject: [PATCH 083/100] #2716 fix after merge --- src/psyclone/transformations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 9fee6752dc..f831704f10 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -425,7 +425,7 @@ def validate_it_can_run_on_gpu(self, node, options): # Check that the routine(s) do(oes) not access any data that is # imported via a 'use' statement. for sched in kernel_schedules: - vam = sched.reference_accesses(vai) + vam = sched.reference_accesses() ktable = sched.symbol_table for sig in vam.all_signatures: name = sig.var_name From bb97eba98870c9b08c44c629b0119221350db567 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 11 Jun 2025 21:18:34 +0100 Subject: [PATCH 084/100] #2716 rename get_kernel_schedule to get_callees [skip ci] --- .../kernel_module_inline_trans.py | 31 +-------------- ...teration_boundaries_inside_kernel_trans.py | 2 +- .../transformations/gocean_opencl_trans.py | 4 +- src/psyclone/domain/lfric/lfric_kern.py | 5 +-- src/psyclone/gocean1p0.py | 5 +-- src/psyclone/psyGen.py | 9 ++--- .../psyir/transformations/omp_task_trans.py | 4 +- .../kernel_module_inline_trans_test.py | 12 +++--- .../tests/domain/gocean/kernel/gokern_test.py | 12 +++--- .../transformations/globalstoargs_test.py | 12 +++--- .../gocean1p0_transformations_test.py | 6 +-- ...ion_boundaries_inside_kernel_trans_test.py | 4 +- .../tests/domain/lfric/lfric_kern_test.py | 38 +++++++++---------- .../lfric_transformations_test.py | 6 +-- src/psyclone/tests/psyGen_test.py | 14 +++---- .../fparser2_find_or_create_symbol_test.py | 2 +- .../tests/psyir/nodes/routine_test.py | 4 +- .../kernel_transformation_test.py | 8 ++-- src/psyclone/transformations.py | 14 +++---- 19 files changed, 81 insertions(+), 111 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 3115c9fbe4..a528ceb8b6 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -120,7 +120,7 @@ def validate(self, node, options=None): # Check that the PSyIR of the routine/kernel can be retrieved. try: - kernels = KernelModuleInlineTrans._get_psyir_to_inline(node) + kernels = node.get_callees() except Exception as error: raise TransformationError( f"{self.name} failed to retrieve PSyIR for {kern_or_call} " @@ -289,33 +289,6 @@ def _prepare_code_to_inline( module_symbol.name) return copied_routines - @staticmethod - def _get_psyir_to_inline(node) -> List[Routine]: - ''' - Wrapper that gets the PSyIR of the routine or kernel - corresponding to the call described by `node`. This supports calls to - routines or kernels which are polymorphic by returning a list of - Routine objects, as well as the associated interface symbol. - - :param node: the Call or CodedKern to resolve. - :type node: :py:class:`psyclone.psyir.nodes.Call` | - :py:class:`psyclone.psyGen.CodedKern` - - :returns: the PSyIR of the routine implementation(s) - - ''' - # TODO #2054 - once CodedKern has been migrated so that it subclasses - # Call then this if/else (and thus this whole routine) can be removed. - if isinstance(node, CodedKern): - # We have a call to a Kernel in a PSyKAl API. - # Where mixed-precision kernels are supported (e.g. in LFRic) the - # call to get_kernel_schedule() will return the one which has an - # interface matching the arguments in the call. - return node.get_kernel_schedule() - - # We have a generic routine call. - return node.get_callees() - @staticmethod def _rm_imported_routine_symbol(symbol: Symbol, schedule: Routine, @@ -447,7 +420,7 @@ def apply(self, node, options=None): # may already be in use, but the equality check below guarantees # that if it exists it is only valid when it references the exact same # implementation. - codes_to_inline = KernelModuleInlineTrans._get_psyir_to_inline(node) + codes_to_inline = node.get_callees() interface_sym = codes_to_inline[0].symbol_table.lookup( caller_name) if len(codes_to_inline) > 1 else None diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index f5f9afdade..315655d886 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -180,7 +180,7 @@ def apply(self, node, options=None): outer_loop.iteration_space = "go_all_pts" # Update Kernel implementation(s). - for kschedule in node.get_kernel_schedule(): + for kschedule in node.get_callees(): kernel_st = kschedule.symbol_table iteration_indices = kernel_st.iteration_indices diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index bd30dd165a..9bb2aab857 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -195,7 +195,7 @@ def validate(self, node, options=None): for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) - for ksched in kern.get_kernel_schedule(): + for ksched in kern.get_callees(): global_variables = set(ksched.symbol_table.imported_symbols) prec_symbols = set(ksched.symbol_table.precision_datasymbols) @@ -757,7 +757,7 @@ def _insert_kernel_code_in_opencl_file(self, kernel): # Create a copy of the kernel and remove precision symbols since they # are not supported in the OpenCL backend. # validate() has checked that the kernel is not polymorphic. - schedule = kernel.get_kernel_schedule()[0] + schedule = kernel.get_kernel_callees()[0] kernel_copy = schedule.copy() symtab = kernel_copy.symbol_table diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 02c2b8d099..55521e0b79 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -786,13 +786,13 @@ def get_interface_symbol(self) -> Union[GenericInterfaceSymbol, None]: :returns: the interface symbol for this kernel if it is polymorphic, None otherwise. ''' - kscheds = self.get_kernel_schedule() + kscheds = self.get_callees() if len(kscheds) == 1: return None cntr = kscheds[0].ancestor(Container) return cntr.symbol_table.lookup(self.name) - def get_kernel_schedule(self): + def get_callees(self) -> List[KernelSchedule]: '''Returns the PSyIR Schedule(s) representing the kernel code. The base class creates the PSyIR schedule(s) on first invocation which is then checked for consistency with the kernel metadata here. The Schedule is @@ -803,7 +803,6 @@ class creates the PSyIR schedule(s) on first invocation which is then PSyIR Schedule using LFRic-specific PSyIR where possible. :returns: the Schedule(s) representing the kernel implementation. - :rtype: list[:py:class:`psyclone.psyGen.KernelSchedule`] :raises InternalError: if no subroutines matching this kernel can be found in the parse tree of the associated source code. diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index 0736d4f1c3..b16308804c 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1075,13 +1075,12 @@ def index_offset(self): ''' The grid index-offset convention that this kernel expects ''' return self._index_offset - def get_kernel_schedule(self): + def get_callees(self): ''' Obtains and returns the PSyIR Schedule representing the kernel code. For consistency with LFRic kernels (which may be polymorphic), this - method actually returns a tuple with the second element containing a - list comprising just one Schedule. + method actually returns a list comprising just one Schedule. :returns: a schedule representing the GOcean kernel code. :rtype: list[:py:class:`psyclone.gocean1p0.GOKernelSchedule`] diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 731a773bb0..be9e1345cd 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1357,7 +1357,7 @@ def get_interface_symbol(self) -> None: ''' return None - def get_kernel_schedule(self): + def get_callees(self): ''' Returns the PSyIR Schedule(s) representing the kernel code. The Schedules are just generated on first invocation, this allows us to @@ -1371,8 +1371,7 @@ def get_kernel_schedule(self): ''' raise NotImplementedError( - f"get_kernel_schedule() must be overridden in class " - f"{self.__class__}") + f"get_callees() must be overridden in class {self.__class__}") @property def opencl_options(self): @@ -1671,7 +1670,7 @@ def rename_and_write(self): # Start from the root of the schedule as we want to output # any module information surrounding the kernel subroutine # as well as the subroutine itself. - schedules = self.get_kernel_schedule() + schedules = self.get_callees() new_kern_code = fortran_writer(schedules[0].root) fll = FortLineLength() new_kern_code = fll.process(new_kern_code) @@ -1711,7 +1710,7 @@ def _rename_psyir(self, suffix): ''' # We need to get the kernel schedule before modifying self.name. - kern_schedules = self.get_kernel_schedule() + kern_schedules = self.get_callees() container = kern_schedules[0].ancestor(Container) # Use the suffix to create a new kernel name. This will diff --git a/src/psyclone/psyir/transformations/omp_task_trans.py b/src/psyclone/psyir/transformations/omp_task_trans.py index 30519c6241..d24dfd4fa8 100644 --- a/src/psyclone/psyir/transformations/omp_task_trans.py +++ b/src/psyclone/psyir/transformations/omp_task_trans.py @@ -112,7 +112,7 @@ def validate(self, node, options=None, **kwargs): for kern in kerns: kintrans.validate(kern) - routines = kern.get_kernel_schedule() + routines = kern.get_callees() for routine in routines: cond_trans.validate(routine) # We need to apply these transformations to ensure we can @@ -175,7 +175,7 @@ def _inline_kernels(self, node): intrans = InlineTrans() for kern in kerns: kintrans.apply(kern) - schedules = kern.get_kernel_schedule() + schedules = kern.get_callees() for sched in schedules: cond_trans.apply(sched) kern.lower_to_language_level() diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 276e8dc200..07be9717db 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -103,7 +103,7 @@ def test_validate_with_imported_subroutine_call(): schedule = invoke.schedule kern_call = schedule.walk(CodedKern)[0] # Create a call to made up subroutine and module symbols - kern_schedules = kern_call.get_kernel_schedule() + kern_schedules = kern_call.get_callees() kern_schedule = kern_schedules[0] mymod = kern_schedule.symbol_table.new_symbol( "mymod", @@ -119,7 +119,7 @@ def test_validate_with_imported_subroutine_call(): inline_trans.validate(kern_call) -def test_validate_invalid_get_kernel_schedule(monkeypatch): +def test_validate_invalid_get_callees(monkeypatch): '''Check that the validate method in the class KernelTrans raises an exception if the kernel code can not be retrieved. @@ -134,7 +134,7 @@ def test_validate_invalid_get_kernel_schedule(monkeypatch): def raise_symbol_error(): '''Simple function that raises SymbolError.''' raise SymbolError("error") - monkeypatch.setattr(kernel, "get_kernel_schedule", raise_symbol_error) + monkeypatch.setattr(kernel, "get_callees", raise_symbol_error) with pytest.raises(TransformationError) as err: kernel_trans.apply(kernel) assert ("KernelModuleInlineTrans failed to retrieve PSyIR for Kernel " @@ -162,7 +162,7 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_callees() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild(block) @@ -179,7 +179,7 @@ def test_validate_no_inline_global_var(parser): end subroutine mytest''') stmt = parser(reader).children[0].children[1] block = CodeBlock([stmt], CodeBlock.Structure.STATEMENT) - kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_callees() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild(block) @@ -196,7 +196,7 @@ def test_validate_no_inline_global_var(parser): # But make sure that an IntrinsicCall routine name is not considered # a global symbol, as they are implicitly declared everywhere - kschedules = kernels[0].get_kernel_schedule() + kschedules = kernels[0].get_callees() ksched = kschedules[0] ksched.pop_all_children() ksched.addchild( diff --git a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py index 73944530dc..b861750714 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py @@ -38,7 +38,7 @@ pytest tests for the GOKern class. TODO #1938 - expand the tests to fully cover the class. Currently only the -constructor and the get_kernel_schedule() method are tested. +constructor and the get_callees() method are tested. ''' @@ -89,9 +89,9 @@ def test_gok_construction(): assert kern._index_offset == "go_offset_sw" -def test_gok_get_kernel_schedule(): +def test_gok_get_callees(): ''' - Test the get_kernel_schedule() method of GOKern. + Test the get_callees() method of GOKern. ''' _, invoke_info = parse(os.path.join(BASE_PATH, "single_invoke.f90"), @@ -100,13 +100,13 @@ def test_gok_get_kernel_schedule(): schedule = psy.invokes.invoke_list[0].schedule kern = schedule.walk(GOKern)[0] assert kern._schedules is None - scheds = kern.get_kernel_schedule() + scheds = kern.get_callees() assert isinstance(scheds, list) assert len(scheds) == 1 sched = scheds[0] assert isinstance(sched, GOKernelSchedule) # A second call should just return the previously-obtained schedule. - scheds2 = kern.get_kernel_schedule() + scheds2 = kern.get_callees() assert scheds2[0] is sched # Check that the expected error is raised if the subroutine that # implements the kernel cannot be found. @@ -119,7 +119,7 @@ def test_gok_get_kernel_schedule(): sub.parent.content.remove(sub) break with pytest.raises(GenerationError) as err: - kern.get_kernel_schedule() + kern.get_callees() err_text = str(err.value) assert ("Failed to raise the PSyIR for kernel 'compute_cu_code' to GOcean " "PSyIR" in err_text) diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 87b5d9341b..c9a6bb70ee 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -157,16 +157,16 @@ def set_to_real(variable): # 3) Has converted the Kernel Schedule symbol into an argument which is # in also the last position - ksymbol = kernel.get_kernel_schedule()[0].symbol_table.lookup("rdt") + ksymbol = kernel.get_callees()[0].symbol_table.lookup("rdt") assert ksymbol.is_argument - assert kernel.get_kernel_schedule()[0].symbol_table.argument_list[-1] == \ + assert kernel.get_callees()[0].symbol_table.argument_list[-1] == \ ksymbol - assert len(kernel.get_kernel_schedule()[0].symbol_table.argument_list) == \ + assert len(kernel.get_callees()[0].symbol_table.argument_list) == \ len(kernel.args) + 2 # GOcean kernels have 2 implicit arguments # Check the kernel code is generated as expected fwriter = FortranWriter() - kernel_code = fwriter(kernel.get_kernel_schedule()[0]) + kernel_code = fwriter(kernel.get_callees()[0]) assert "subroutine kernel_with_use_code(ji,jj,istep,ssha,tmask,rdt)" \ in kernel_code assert "real, intent(inout) :: rdt" in kernel_code @@ -210,7 +210,7 @@ def create_data_symbol(arg): trans.apply(kernel) fwriter = FortranWriter() - kernels = kernel.get_kernel_schedule() + kernels = kernel.get_callees() kernel_code = fwriter(kernels[0]) assert ("subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " @@ -289,7 +289,7 @@ def create_data_symbol(arg): monkeypatch.setattr(DataSymbol, "resolve_type", create_data_symbol) for num, kernel in enumerate(invoke.schedule.coded_kernels()): - kernels = kernel.get_kernel_schedule() + kernels = kernel.get_callees() kschedule = kernels[0] trans.apply(kernel) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py index 82cc498829..6fbfd65015 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -1494,7 +1494,7 @@ def test_accroutinetrans_with_kern(fortran_writer, monkeypatch): assert rtrans.name == "ACCRoutineTrans" rtrans.apply(kern) # Check that there is a acc routine directive in the kernel - schedules = kern.get_kernel_schedule() + schedules = kern.get_callees() code = fortran_writer(schedules[0]) assert "!$acc routine seq\n" in code @@ -1502,7 +1502,7 @@ def test_accroutinetrans_with_kern(fortran_writer, monkeypatch): def raise_gen_error(): '''Simple function that raises GenerationError.''' raise GenerationError("error") - monkeypatch.setattr(kern, "get_kernel_schedule", raise_gen_error) + monkeypatch.setattr(kern, "get_callees", raise_gen_error) with pytest.raises(TransformationError) as err: rtrans.apply(kern) assert ("Failed to create PSyIR for kernel 'continuity_code'. Cannot " @@ -1518,7 +1518,7 @@ def test_accroutinetrans_with_routine(fortran_writer): assert isinstance(kern, GOKern) rtrans = ACCRoutineTrans() assert rtrans.name == "ACCRoutineTrans" - routines = kern.get_kernel_schedule() + routines = kern.get_callees() rtrans.apply(routines[0]) # Check that there is a acc routine directive in the routine code = fortran_writer(routines[0]) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index 485e6797c5..d49610add6 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -81,7 +81,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): # Add some name conflicting symbols in the Invoke and the Kernel kernel.ancestor(Container).symbol_table.new_symbol("xstop") - routines = kernel.get_kernel_schedule() + routines = kernel.get_callees() ksched = routines[0] ksched.symbol_table.new_symbol("ystart") @@ -120,7 +120,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): assert kernel.arguments.args[-1].argument_type == "scalar" # Check that the kernel subroutine has been transformed: - kschedule = kernel.get_kernel_schedule()[0] + kschedule = kernel.get_callees()[0] # - It has the boundary conditions mask assert isinstance(kschedule.children[0], IfBlock) diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index b5940831eb..5c87defff6 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -139,8 +139,8 @@ def test_kern_getter_errors(): in str(err.value)) -def test_get_kernel_schedule(monkeypatch): - '''Test that a PSyIR kernel schedule is created by get_kernel_schedule +def test_kern_get_callees(monkeypatch): + '''Test that a PSyIR kernel schedule is created by get_callees if one does not exist and that the same kernel schedule is returned if one has already been created. @@ -154,14 +154,14 @@ def test_get_kernel_schedule(monkeypatch): assert kernel._schedules is None - kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_callees() assert len(kernel_schedules) == 1 assert isinstance(kernel_schedules[0], KernelSchedule) assert kernel._schedules[0] is kernel_schedules[0] # Not a polymorphic kernel so has no interface symbol assert kernel.get_interface_symbol() is None - kernel_schedules_2 = kernel.get_kernel_schedule() + kernel_schedules_2 = kernel.get_callees() assert kernel_schedules[0] is kernel_schedules_2[0] # Check the internal error for the case where we fail to get any # implementation for the kernel. @@ -170,14 +170,14 @@ def test_get_kernel_schedule(monkeypatch): monkeypatch.setattr(Fparser2Reader, "generate_psyir", lambda _1, _2: Container("dummy_mod")) with pytest.raises(InternalError) as err: - kernel.get_kernel_schedule() + kernel.get_callees() assert ("Failed to find any routines for Kernel 'matrix_vector_code'" in str(err.value)) -def test_get_kernel_schedule_same_container(monkeypatch): +def test_get_callees_same_container(monkeypatch): ''' - Check that get_kernel_schedule() first examines all routines in the same + Check that get_callees() first examines all routines in the same Container. ''' @@ -188,16 +188,16 @@ def test_get_kernel_schedule_same_container(monkeypatch): mod_inline_trans = KernelModuleInlineTrans() for kern in sched.walk(LFRicKern): mod_inline_trans.apply(kern) - # Remove the cached schedule to force get_kernel_schedule() to search. + # Remove the cached schedule to force get_callees() to search. monkeypatch.setattr(kern, "_schedules", None) - schedules = kern.get_kernel_schedule() + schedules = kern.get_callees() # The returned schedule should be the one in the local Container. assert schedules[0] in sched.ancestor(Container).walk(Routine) -def test_get_kernel_schedule_mixed_precision(): +def test_get_callees_mixed_precision(): ''' - Test that get_kernel_schedule() and get_interface_symbol() work for a + Test that get_callees() and get_interface_symbol() work for a mixed-precision kernel. ''' @@ -205,17 +205,17 @@ def test_get_kernel_schedule_mixed_precision(): name="invoke_0", dist_mem=False) sched = invoke.schedule for kern in sched.walk(LFRicKern, stop_type=LFRicKern): - assert len(kern.get_kernel_schedule()) == 2 + assert len(kern.get_callees()) == 2 isym = kern.get_interface_symbol() assert isinstance(isym, GenericInterfaceSymbol) assert isym.name == "mixed_code" -@pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" +@pytest.mark.xfail(reason="get_callees has been extended to return all" " implementations of a polymorphic kernel. We need to " "put back (and fix) the ability to resolve which " "implementation is being called.") -def test_get_kernel_schedule_mixed_precision_match(): +def test_get_callees_mixed_precision_match(): ''' Test that we can get the correct schedule for a mixed-precision kernel. @@ -234,16 +234,16 @@ def test_get_kernel_schedule_mixed_precision_match(): # Check that the correct kernel implementation is obtained for each # one in the invoke. for precision, kern in zip(precisions, kernels): - sched = kern.get_kernel_schedule() + sched = kern.get_callees() assert isinstance(sched, KernelSchedule) assert sched.name == f"mixed_code_{8*precision}" -@pytest.mark.xfail(reason="get_kernel_schedule has been extended to return all" +@pytest.mark.xfail(reason="get_callees() has been extended to return all" " implementations of a polymorphic kernel. We need to " "put back (and fix) the ability to resolve which " "implementation is being called.") -def test_get_kernel_sched_mixed_precision_no_match(monkeypatch): +def test_get_callees_mixed_precision_no_match(monkeypatch): ''' Test that we get the expected error if there's no matching implementation for a mixed-precision kernel. @@ -262,7 +262,7 @@ def fake_validate(_1, _2): monkeypatch.setattr(LFRicKern, "validate_kernel_code_args", fake_validate) with pytest.raises(GenerationError) as err: - _ = kernels[0].get_kernel_schedule() + _ = kernels[0].get_callees() assert ("Failed to find a kernel implementation with an interface that " "matches the invoke of 'mixed_code'. (Tried routines " "['mixed_code_32', 'mixed_code_64'].)" in str(err.value)) @@ -282,7 +282,7 @@ def test_validate_kernel_code_args(monkeypatch): schedule = psy.invokes.invoke_list[0].schedule # matrix vector kernel kernel = schedule[2].loop_body[0] - schedules = kernel.get_kernel_schedule() + schedules = kernel.get_callees() sched = schedules[0] kernel.validate_kernel_code_args(sched.symbol_table) 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 8b78aa72aa..297bd259c1 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -7651,7 +7651,7 @@ def test_kern_const_invalid_kern(monkeypatch): def dummy(): '''A dummy function that always raises an exception.''' raise NotImplementedError("Monkeypatch error") - monkeypatch.setattr(kernel, "get_kernel_schedule", dummy) + monkeypatch.setattr(kernel, "get_callees", dummy) with pytest.raises(TransformationError) as excinfo: kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) assert ( @@ -7686,7 +7686,7 @@ def test_kern_const_invalid_make_constant1(): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") - kernel_schedule = kernel.get_kernel_schedule()[0] + kernel_schedule = kernel.get_callees()[0] symbol_table = kernel_schedule.symbol_table # Make the symbol table's argument list empty. We have to make sure that # the interface of any existing argument Symbols is set to @@ -7713,7 +7713,7 @@ def test_kern_const_invalid_make_constant2(): kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") kctrans = LFRicKernelConstTrans() - kernel_schedule = kernel.get_kernel_schedule()[0] + kernel_schedule = kernel.get_callees()[0] symbol_table = kernel_schedule.symbol_table symbol = symbol_table._argument_list[7] # Expecting scalar integer. Set to array. diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index a8ca2f37ac..461b65d070 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -591,15 +591,15 @@ def test_invokeschedule_lowering_with_preexisting_globals(): # Kern class test -def test_kern_get_kernel_schedule(): - ''' Tests the get_kernel_schedule method in the Kern class. +def test_kern_get_callees(): + ''' Tests the get_callees method in the Kern class. ''' _, invoke_info = parse(os.path.join(BASE_PATH, "1_single_invoke.f90"), api="lfric") psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) schedule = psy.invokes.invoke_list[0].schedule kern = schedule.children[0].loop_body[0] - kern_schedules = kern.get_kernel_schedule() + kern_schedules = kern.get_callees() assert len(kern_schedules) == 1 assert isinstance(kern_schedules[0], KernelSchedule) @@ -835,9 +835,9 @@ def test_kern_children_validation(): "is a LeafNode and doesn't accept children.") in str(excinfo.value) -def test_codedkern_get_kernel_schedule(monkeypatch): +def test_codedkern_get_callees(monkeypatch): ''' - Check that CodedKern.get_kernel_schedule() raises a NotImplementedError + Check that CodedKern.get_callees() raises a NotImplementedError (as it must be implemented by sub-classes). Also check that get_interface_symbol() returns None. @@ -848,8 +848,8 @@ def test_codedkern_get_kernel_schedule(monkeypatch): kern.load_meta(metadata) monkeypatch.setattr(kern, "__class__", CodedKern) with pytest.raises(NotImplementedError) as err: - kern.get_kernel_schedule() - assert ("get_kernel_schedule() must be overridden in class " + kern.get_callees() + assert ("get_callees() must be overridden in class " in str(err.value)) assert kern.get_interface_symbol() is None diff --git a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py index ea8c063e1b..f4168f3a85 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py @@ -61,7 +61,7 @@ def test_find_or_create_unresolved_symbol(): api="gocean", idx=0) sched = invoke.schedule kernels = sched.walk(Kern) - kernel_schedules = kernels[0].get_kernel_schedule() + kernel_schedules = kernels[0].get_callees() kernel_schedule = kernel_schedules[0] references = kernel_schedule.walk(Reference) diff --git a/src/psyclone/tests/psyir/nodes/routine_test.py b/src/psyclone/tests/psyir/nodes/routine_test.py index 7a6d494e5b..b6b9aaefe7 100644 --- a/src/psyclone/tests/psyir/nodes/routine_test.py +++ b/src/psyclone/tests/psyir/nodes/routine_test.py @@ -485,12 +485,12 @@ def test_check_outer_scope_accesses(config_instance): config_instance.include_paths = [] # Multiple wildcard imports are handled by bringing them into the routine # and so aren't a problem. - kschedules = kcall.get_kernel_schedule() + kschedules = kcall.get_callees() kschedules[0].check_outer_scope_accesses(kcall, "Kernel") # Now try where there's only a single wildcard import so we know the origin # of the symbol. kcall0 = schedule.walk(CodedKern)[0] - kscheds = kcall0.get_kernel_schedule() + kscheds = kcall0.get_callees() ctable = kscheds[0].ancestor(Container).symbol_table # To do this, we manually remove all ContainerSymbols apart from the one # from which 'go_wp' is imported. diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 70115ec576..b8db59a959 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -335,12 +335,12 @@ def test_gpumixin_validate_no_schedule(monkeypatch): sched = invoke.schedule kernels = sched.walk(Kern) kern = kernels[0] - # We monkeypatch the 'get_kernel_schedule' method of LFRicKern so that it + # We monkeypatch the 'get_callees' method of LFRicKern so that it # just raises an exception. def broken(_1_): raise GenerationError("this is just a test") - monkeypatch.setattr(kern, "get_kernel_schedule", broken) + monkeypatch.setattr(kern, "get_callees", broken) rtrans = ACCRoutineTrans() with pytest.raises(TransformationError) as err: @@ -442,7 +442,7 @@ def test_gpumixin_validate_no_call(): in str(err.value)) # The same error happens for unsupported GPU intrinsics - kschedules = kernel.get_kernel_schedule() + kschedules = kernel.get_callees() call = kschedules[0].walk(Call)[0] call.replace_with( IntrinsicCall.create(IntrinsicCall.Intrinsic.GET_COMMAND)) @@ -468,7 +468,7 @@ def test_kernel_gpu_annotation_trans(rtrans, expected_directive, rtrans.apply(kern) # Check that the directive has been added to the kernel code - kschedules = kern.get_kernel_schedule() + kschedules = kern.get_callees() code = fortran_writer(kschedules[0]) assert expected_directive in code diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 8c6dfe07a7..c351964be8 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -410,7 +410,7 @@ def validate_it_can_run_on_gpu(self, node, options): # or that the frontend failed to convert it into PSyIR) reraise it # as a TransformationError try: - kernel_schedules = node.get_kernel_schedule() + kernel_schedules = node.get_callees() except Exception as error: raise TransformationError( f"Failed to create PSyIR for kernel '{node.name}'. " @@ -548,7 +548,7 @@ def apply(self, node, options=None): node.modified = True # Get the schedule representing the kernel subroutine - routines = node.get_kernel_schedule() + routines = node.get_callees() else: routines = [node] @@ -2244,7 +2244,7 @@ class LFRicKernelConstTrans(Transformation): >>> trans = LFRicKernelConstTrans() >>> for kernel in schedule.coded_kernels(): >>> trans.apply(kernel, number_of_layers=150) - >>> kernel_schedule = kernel.get_kernel_schedule() + >>> kernel_schedule = kernel.get_callees()[0] >>> # Uncomment the following line to see a text view of the >>> # symbol table >>> # print(kernel_schedule.symbol_table.view()) @@ -2415,7 +2415,7 @@ def make_constant(symbol_table, arg_position, value, arg_list_info = KernCallArgList(kernel) arg_list_info.generate() try: - kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_callees() except NotImplementedError as excinfo: raise TransformationError( f"Failed to parse kernel '{kernel.name}'. Error reported was " @@ -2766,7 +2766,7 @@ def apply(self, node, options=None): node.modified = True # Get the schedule(s) representing the kernel subroutine - routines = node.get_kernel_schedule() + routines = node.get_callees() else: routines = [node] @@ -3009,7 +3009,7 @@ def validate(self, node, options=None): # Check that there are no unqualified imports or undeclared symbols try: - kernels = node.get_kernel_schedule() + kernels = node.get_callees() except SymbolError as err: raise TransformationError( f"Kernel '{node.name}' contains undeclared symbol: " @@ -3041,7 +3041,7 @@ def apply(self, node, options=None): ''' self.validate(node, options) - kernels = node.get_kernel_schedule() + kernels = node.get_callees() # validate() has ensured that there is only one kernel routine. kernel = kernels[0] symtab = kernel.symbol_table From 4fc98e5af7af2400c771a4e4e69785d10be35253 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 13 Jun 2025 14:37:02 +0100 Subject: [PATCH 085/100] #2716 fix incorrect renaming --- .../domain/gocean/transformations/gocean_opencl_trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index 9bb2aab857..a0dbf8e100 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -757,7 +757,7 @@ def _insert_kernel_code_in_opencl_file(self, kernel): # Create a copy of the kernel and remove precision symbols since they # are not supported in the OpenCL backend. # validate() has checked that the kernel is not polymorphic. - schedule = kernel.get_kernel_callees()[0] + schedule = kernel.get_callees()[0] kernel_copy = schedule.copy() symtab = kernel_copy.symbol_table From 852ab7269da0dcdfa499b1ae4df2a1cf28fe40d4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 13 Jun 2025 14:50:00 +0100 Subject: [PATCH 086/100] #2716 simplify gpu_offloading script [skip ci] --- examples/lfric/scripts/gpu_offloading.py | 78 +++++++++++------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 96ca43c8b5..50ad17a195 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -45,9 +45,8 @@ import sys from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import LFRicConstants -from psyclone.psyGen import CodedKern from psyclone.psyir.nodes import ( - Call, Directive, IntrinsicCall, Loop, Routine, Schedule) + Call, Directive, IntrinsicCall, Loop, Routine) from psyclone.psyir.transformations import ( ACCKernelsTrans, InlineTrans, Matmul2CodeTrans, OMPTargetTrans, TransformationError) @@ -69,59 +68,53 @@ OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") -def _inline_calls(kern): +def _inline_calls(sched): ''' Recursively inline all calls within the supplied Kernel or Routine. Currently only attempts to replace MATMUL intrinsic calls with inline code. -` - :param kern: the Kernel or Routine to inline any Calls into. + + :param sched: Routine to inline any Calls into. ''' mod_inline_trans = KernelModuleInlineTrans() intrans = InlineTrans() matrans = Matmul2CodeTrans() - if isinstance(kern, CodedKern): - scheds = kern.get_kernel_schedule() - else: - scheds = [kern] - for sched in scheds: - sched: Schedule - for call in sched.walk(Call): - call: Call - # The NVIDIA compiler (as at 25.3) will sometimes fail to compile - # code with calls to MATMUL with a claim that they are not - # available on the device, e.g.: - # Call to NVHPC runtime function not supported - - # pgf90_matmul_real4_i8 - # Therefore, if we are unable to replace a MATMUL by generic code, - # the resulting TransformationError will signal (to the calling - # routine) that we are not to mark this kernel for offload. - if (isinstance(call, IntrinsicCall) and - call.intrinsic == IntrinsicCall.Intrinsic.MATMUL): - matrans.apply(call) - continue - - # For now we only look at MATMUL calls. In future we may want to - # remove this `continue` and attempt to inline more calls. + for call in sched.walk(Call): + call: Call + # The NVIDIA compiler (as at 25.3) will sometimes fail to compile + # code with calls to MATMUL with a claim that they are not + # available on the device, e.g.: + # Call to NVHPC runtime function not supported - + # pgf90_matmul_real4_i8 + # Therefore, if we are unable to replace a MATMUL by generic code, + # the resulting TransformationError will signal (to the calling + # routine) that we are not to mark this kernel for offload. + if (isinstance(call, IntrinsicCall) and + call.intrinsic == IntrinsicCall.Intrinsic.MATMUL): + matrans.apply(call) continue - if any(name in call.routine.name for name in INLINE_EXCLUSIONS): - continue + # For now we only look at MATMUL calls. In future we may want to + # remove this `continue` and attempt to inline more calls. + continue + + if any(name in call.routine.name for name in INLINE_EXCLUSIONS): + continue + try: + for inner_call in call.get_callees(): + _inline_calls(inner_call) + mod_inline_trans.apply(call) try: - for inner_call in call.get_callees(): - _inline_calls(inner_call) - mod_inline_trans.apply(call) - try: - intrans.apply(call) - except TransformationError as err: - print(f"Failed to inline call {call.debug_string()}:\n" - f"{err}") - except (TransformationError, NotImplementedError) as err: - print(f"Failed to module-inline routine {call.routine.name}:\n" + intrans.apply(call) + except TransformationError as err: + print(f"Failed to inline call {call.debug_string()}:\n" f"{err}") + except (TransformationError, NotImplementedError) as err: + print(f"Failed to module-inline routine {call.routine.name}:\n" + f"{err}") def trans(psyir): @@ -168,7 +161,7 @@ def trans(psyir): for subroutine in psyir.walk(Routine): - print("Transforming invoke '{0}' ...".format(subroutine.name)) + print(f"Transforming invoke '{subroutine.name}' ...") # Make setval_* compute redundantly to the level 1 halo if it # is in its own loop @@ -210,7 +203,8 @@ def trans(psyir): print(f"Failed to module-inline kernel " f"'{kern.name}' due to:\n{err.value}") try: - _inline_calls(kern) + for routine in kern.get_callees(): + _inline_calls(routine) gpu_annotation_trans.apply(kern) print(f"Annotated kernel '{kern.name}'") except TransformationError as err: From 15c6d564cd310961fe9a849772d7126e0efb412c Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 19 Jun 2025 19:33:50 +0100 Subject: [PATCH 087/100] #2716 tidy kernel_print and extend example 19 to exercise it --- examples/lfric/eg19/Makefile | 5 ++++- examples/lfric/scripts/kernel_print.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/lfric/eg19/Makefile b/examples/lfric/eg19/Makefile index 88793ed61b..210063cbb8 100644 --- a/examples/lfric/eg19/Makefile +++ b/examples/lfric/eg19/Makefile @@ -59,8 +59,11 @@ $(EXEC): $(LFRIC_LIB) $(OBJ) compile: transform $(EXEC) +# Runs PSyclone to do the code generation. Also demonstrates the use of the +# example 'kernel_print' transformation which prints the Fortran of each +# kernel found. transform: - ${PSYCLONE} -api lfric algorithm.x90 -opsy mixed_precision_psy.f90 -oalg alg.f90 + ${PSYCLONE} -api lfric algorithm.x90 -s ../scripts/kernel_print.py -opsy mixed_precision_psy.f90 -oalg alg.f90 alg.f90 mixed_precision_psy.f90: transform alg.o: mixed_precision_psy.o diff --git a/examples/lfric/scripts/kernel_print.py b/examples/lfric/scripts/kernel_print.py index 66556de1b9..915fe5efbf 100644 --- a/examples/lfric/scripts/kernel_print.py +++ b/examples/lfric/scripts/kernel_print.py @@ -55,8 +55,7 @@ def trans(psyir): # Loop over all of the Kernels Calls for kernel in psyir.coded_kernels(): try: - kernel_schedules = kernel.get_kernel_schedule() - for ksched in kernel_schedules: + for ksched in kernel.get_callees(): if ksched not in already_printed: kern = fortran_writer(ksched) print(kern) From 49b4410e55d17eab95bd4c0e909a0b3a8df4764e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 19 Jun 2025 19:34:16 +0100 Subject: [PATCH 088/100] #2716 tidy and remove attempts to inline from gpu_offloading.py --- examples/lfric/scripts/gpu_offloading.py | 46 +++++++----------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 50ad17a195..1f242c409e 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -46,7 +46,7 @@ from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import LFRicConstants from psyclone.psyir.nodes import ( - Call, Directive, IntrinsicCall, Loop, Routine) + Call, Directive, IntrinsicCall, Loop, Routine, Schedule) from psyclone.psyir.transformations import ( ACCKernelsTrans, InlineTrans, Matmul2CodeTrans, OMPTargetTrans, TransformationError) @@ -68,18 +68,14 @@ OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") -def _inline_calls(sched): +def _replace_matmuls(sched: Schedule): ''' - Recursively inline all calls within the supplied Kernel or Routine. - - Currently only attempts to replace MATMUL intrinsic calls with inline + Attempts to replace all MATMUL intrinsic calls with inline code. - :param sched: Routine to inline any Calls into. + :param sched: schedule to transform. ''' - mod_inline_trans = KernelModuleInlineTrans() - intrans = InlineTrans() matrans = Matmul2CodeTrans() for call in sched.walk(Call): @@ -95,26 +91,6 @@ def _inline_calls(sched): if (isinstance(call, IntrinsicCall) and call.intrinsic == IntrinsicCall.Intrinsic.MATMUL): matrans.apply(call) - continue - - # For now we only look at MATMUL calls. In future we may want to - # remove this `continue` and attempt to inline more calls. - continue - - if any(name in call.routine.name for name in INLINE_EXCLUSIONS): - continue - try: - for inner_call in call.get_callees(): - _inline_calls(inner_call) - mod_inline_trans.apply(call) - try: - intrans.apply(call) - except TransformationError as err: - print(f"Failed to inline call {call.debug_string()}:\n" - f"{err}") - except (TransformationError, NotImplementedError) as err: - print(f"Failed to module-inline routine {call.routine.name}:\n" - f"{err}") def trans(psyir): @@ -127,6 +103,7 @@ def trans(psyir): :type psyir: :py:class:`psyclone.psyir.nodes.FileContainer` ''' + intrans = InlineTrans() rtrans = LFRicRedundantComputationTrans() ctrans = LFRicColourTrans() otrans = LFRicOMPLoopTrans() @@ -189,12 +166,14 @@ def trans(psyir): const.VALID_DISCONTINUOUS_NAMES): ctrans.apply(loop) - # Mark Kernels inside the loops over cells as GPU-enabled - # (alternatively we could inline them) + # Module-inline the Kernels inside the loops over cells and then mark + # them as GPU-enabled. + # (The latter step won't be necessary if/when we fully inline them.) for loop in subroutine.loops(): if loop.iteration_space.endswith("cell_column"): if offload: for kern in loop.kernels(): + # Attempt to module-inline the kernel. try: mod_inline_trans.apply(kern) print(f"Module-inlined kernel '{kern.name}'") @@ -203,8 +182,10 @@ def trans(psyir): print(f"Failed to module-inline kernel " f"'{kern.name}' due to:\n{err.value}") try: + # Ensure any MATMULs within the kernel are replaced. for routine in kern.get_callees(): - _inline_calls(routine) + _replace_matmuls(routine) + # Finally, annotate the kernel routine for GPU. gpu_annotation_trans.apply(kern) print(f"Annotated kernel '{kern.name}'") except TransformationError as err: @@ -214,8 +195,7 @@ def trans(psyir): f"{err.value}") # For annotated or inlined kernels we could attempt to # provide compile-time dimensions for the temporary - # arrays and convert to code unsupported intrinsics. - + # arrays and convert to code any unsupported intrinsics. # Add GPU offloading to loops unless they are over colours or are null. for loop in subroutine.walk(Loop): kernel_names = [k.name.lower() for k in loop.kernels()] From 393b6cd2f543c7bf3737a8e95f1f84d986d9b356 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 20 Jun 2025 16:03:43 +0100 Subject: [PATCH 089/100] #2716 add support for interface symbol renamed on import --- .../kernel_module_inline_trans.py | 44 ++++++++++++-- src/psyclone/psyir/nodes/call.py | 8 ++- .../kernel_module_inline_trans_test.py | 57 +++++++++++++++++++ 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index a528ceb8b6..d8844905ce 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -47,8 +47,8 @@ from psyclone.psyGen import Transformation, CodedKern from psyclone.psyir.transformations import TransformationError from psyclone.psyir.symbols import ( - ContainerSymbol, DataSymbol, DataTypeSymbol, - ImportInterface, RoutineSymbol, Symbol, SymbolError, SymbolTable) + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + GenericInterfaceSymbol, RoutineSymbol, Symbol, SymbolError, SymbolTable) from psyclone.psyir.nodes import ( Call, Container, FileContainer, Routine, ScopingNode, IntrinsicCall) @@ -97,6 +97,9 @@ def validate(self, node, options=None): :raises TransformationError: if there is no explicit import of the called Routine and there is already a Routine of that name in the parent Container. + :raises TransformationError: if the call is to a polymorphic routine + and there's no Container at the call site to which to add the + interface definition. :raises TransformationError: if the kernel cannot be safely inlined. ''' @@ -127,6 +130,20 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error + if len(kernels) > 1: + # We can't 'module' inline a call to an interface if there's no + # ancestor Container in which to put the interface. + cntr = node + while cntr: + cntr = cntr.ancestor(Container) + if cntr and not isinstance(cntr, FileContainer): + break + else: + raise TransformationError( + f"Cannot module-inline the call to '{kname}' since it is " + f"a polymorphic routine (i.e. an interface) and the call-" + f"site is not within a module.") + # Validate the PSyIR of each routine/kernel. for kernel_schedule in kernels: self._validate_schedule(node, kname, kern_or_call, kernel_schedule) @@ -407,10 +424,16 @@ def apply(self, node, options=None): self.validate(node, options) + external_callee_name = None if isinstance(node, CodedKern): caller_name = node.name else: caller_name = node.routine.symbol.name + if (node.routine.symbol.is_import and + node.routine.symbol.interface.orig_name): + external_callee_name = node.routine.symbol.interface.orig_name + if not external_callee_name: + external_callee_name = caller_name # Get the PSyIR of the routine to module inline as well as the name # with which it is being called. @@ -421,8 +444,10 @@ def apply(self, node, options=None): # that if it exists it is only valid when it references the exact same # implementation. codes_to_inline = node.get_callees() - interface_sym = codes_to_inline[0].symbol_table.lookup( - caller_name) if len(codes_to_inline) > 1 else None + interface_sym = None + if len(codes_to_inline) > 1: + interface_sym = codes_to_inline[0].symbol_table.lookup( + external_callee_name) callsite_table = node.scope.symbol_table @@ -558,7 +583,16 @@ def apply(self, node, options=None): self._rm_imported_routine_symbol(shadowed_sym, codes_to_inline[0], caller_cntr_table) - container.symbol_table.add(interface_sym) + if caller_name != interface_sym.name: + # If the interface was originally renamed on import, then we + # must create a new symbol with the local name. + new_sym = GenericInterfaceSymbol( + caller_name, routines=[(RoutineSymbol("dummy"), True)]) + new_sym.copy_properties(interface_sym) + else: + # Otherwise we can use the existing symbol. + new_sym = interface_sym + container.symbol_table.add(new_sym) interface_sym.visibility = Symbol.Visibility.PRIVATE interface_sym.replace_symbols_using(container.symbol_table) diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index f92d5ee45c..010903e74a 100644 --- a/src/psyclone/psyir/nodes/call.py +++ b/src/psyclone/psyir/nodes/call.py @@ -557,6 +557,10 @@ def get_callees(self): can_be_private = False while cursor.is_import: csym = cursor.interface.container_symbol + if cursor.interface.orig_name: + target_name = cursor.interface.orig_name + else: + target_name = cursor.name try: container = csym.find_container_psyir(local_node=self) except SymbolError: @@ -570,7 +574,7 @@ def get_callees(self): f"RoutineSymbol '{rsym.name}' is imported from " f"Container '{csym.name}' but the PSyIR for that " f"container could not be generated.") - imported_sym = container.symbol_table.lookup(cursor.name) + imported_sym = container.symbol_table.lookup(target_name) if imported_sym.visibility != Symbol.Visibility.PUBLIC: # The required Symbol must be shadowed with a PRIVATE # Symbol in this Container. This means that the one we @@ -578,7 +582,7 @@ def get_callees(self): # import. # TODO #924 - Use ModuleManager to search? raise NotImplementedError( - f"RoutineSymbol '{rsym.name}' is imported from " + f"RoutineSymbol '{target_name}' is imported from " f"Container '{csym.name}' but that Container defines " f"a private Symbol of the same name. Searching for the" f" Container that defines a public Routine with that " diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 07be9717db..fb76157a08 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -847,6 +847,63 @@ def test_module_inline_with_interfaces(tmpdir): assert LFRicBuild(tmpdir).code_compiles(psy) +def test_module_inline_with_renamed_import(monkeypatch, + fortran_reader, + fortran_writer): + '''Test module-inlining when the target routine is use-associated to + a different name in the caller scope. + + ''' + # Create the module containing the subroutine definition, write it to + # file and set the search path so that PSyclone can find it. + make_external_module(monkeypatch, fortran_reader, "my_mod", + '''\ + module my_mod + interface my_interface + module procedure :: my_sub, my_other_sub + end interface my_interface + contains + subroutine my_sub(arg) + real*8, dimension(10), intent(inout) :: arg + arg(1:10) = 1.0 + end subroutine my_sub + subroutine my_other_sub(arg) + real*4, dimension(10), intent(inout) :: arg + arg(1:10) = 1.0 + end subroutine my_other_sub + end module my_mod + ''') + intrans = KernelModuleInlineTrans() + code = '''\ + program my_prog + implicit none + use my_mod, only: local_name=>my_interface + real*4, dimension(10) :: var + call local_name(var) + end program my_prog''' + psyir = fortran_reader.psyir_from_source(code) + with pytest.raises(TransformationError) as err: + intrans.validate(psyir.walk(Call)[0]) + assert ("Cannot module-inline the call to 'local_name' since it is a " + "polymorphic routine" in str(err.value)) + code = '''\ + module second_mod + contains + subroutine doit() + implicit none + use my_mod, only: local_name=>my_interface + real*4, dimension(10) :: var + call local_name(var) + end subroutine doit + end module second_mod''' + psyir = fortran_reader.psyir_from_source(code) + intrans.apply(psyir.walk(Call)[0]) + result = fortran_writer(psyir) + assert "interface local_name" in result + assert "call local_name(var)" in result + assert "subroutine my_sub(arg)" in result + + def test_rm_imported_routine_symbol(fortran_reader): ''' Tests for the _rm_imported_routine_symbol() utility method. From 71c1c13444b7fbbacb7cbba74e3ba76724c57b06 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 20 Jun 2025 16:40:17 +0100 Subject: [PATCH 090/100] #2719 rename to get_callees() in examples --- examples/gocean/eg3/ocl_trans.py | 2 +- examples/lfric/eg15/matvec_opt.py | 2 +- examples/xdsl/backend/xdsl.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index 59b496ddd9..c8d0337105 100644 --- a/examples/gocean/eg3/ocl_trans.py +++ b/examples/gocean/eg3/ocl_trans.py @@ -62,7 +62,7 @@ def trans(psyir): move_boundaries_trans.apply(kern) # Change the syntax to remove the return statements introduced by the # previous transformation - kschedules = kern.get_kernel_schedule() + kschedules = kern.get_callees() # NOTE: we assume the kernel is not polymorphic and thus there is # only one schedule associated with it. fold_trans.apply(kschedules[0]) diff --git a/examples/lfric/eg15/matvec_opt.py b/examples/lfric/eg15/matvec_opt.py index 83f2bc23a3..6239779ab3 100644 --- a/examples/lfric/eg15/matvec_opt.py +++ b/examples/lfric/eg15/matvec_opt.py @@ -91,7 +91,7 @@ def trans(psyir): for kernel in psyir.coded_kernels(): if kernel.name.lower() == "matrix_vector_kernel_code": - kernel_schedules = kernel.get_kernel_schedule() + kernel_schedules = kernel.get_callees() # For simplicity, ASSUME that the kernel is not polymorphic and # thus only has one schedule. kernel_schedule = kernel_schedules[0] diff --git a/examples/xdsl/backend/xdsl.py b/examples/xdsl/backend/xdsl.py index 3d601a2625..f03d7715d7 100644 --- a/examples/xdsl/backend/xdsl.py +++ b/examples/xdsl/backend/xdsl.py @@ -439,7 +439,7 @@ def checkIfStringIsType(self, string, typ): def nemokern_node(self, node): exec_statements = [] - schedules = node.get_kernel_schedule() + schedules = node.get_callees() # IGNORE polymorphic routines. schedule = schedules[0] for child in schedule.children: From 84522efe3a812d19f391b8369f8771a2f7877e0c Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 20 Jun 2025 16:41:26 +0100 Subject: [PATCH 091/100] #2716 add support for renaming of non-polymorphic routine --- .../kernel_module_inline_trans.py | 8 ++++ .../kernel_module_inline_trans_test.py | 38 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index d8844905ce..0a84677408 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -595,6 +595,14 @@ def apply(self, node, options=None): container.symbol_table.add(new_sym) interface_sym.visibility = Symbol.Visibility.PRIVATE interface_sym.replace_symbols_using(container.symbol_table) + else: + # No interface but was the original routine symbol renamed + # on import? + if caller_name != external_callee_name: + # It was so we need to rename the inlined routine. + sym = node.scope.symbol_table.lookup(external_callee_name) + table = sym.find_symbol_table(node) + table.rename_symbol(sym, caller_name) # Update the Kernel to point to the updated PSyIR and set # the module-inline flag to avoid generating the kernel imports diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index fb76157a08..2005df2753 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -850,8 +850,42 @@ def test_module_inline_with_interfaces(tmpdir): def test_module_inline_with_renamed_import(monkeypatch, fortran_reader, fortran_writer): - '''Test module-inlining when the target routine is use-associated to - a different name in the caller scope. + '''Test module-inlining when the target routine is + use-associated to a different name in the caller scope. + + ''' + # Create the module containing the subroutine definition, write it to + # file and set the search path so that PSyclone can find it. + make_external_module(monkeypatch, fortran_reader, "my_mod", + '''\ + module my_mod + contains + subroutine my_sub(arg) + real*8, dimension(10), intent(inout) :: arg + arg(1:10) = 1.0 + end subroutine my_sub + end module my_mod + ''') + intrans = KernelModuleInlineTrans() + code = '''\ + program my_prog + implicit none + use my_mod, only: local_name=>my_sub + real*4, dimension(10) :: var + call local_name(var) + end program my_prog''' + psyir = fortran_reader.psyir_from_source(code) + intrans.apply(psyir.walk(Call)[0]) + result = fortran_writer(psyir) + assert "call local_name(var)" in result + assert "subroutine local_name(arg)" in result + + +def test_module_inline_interface_with_renamed_import(monkeypatch, + fortran_reader, + fortran_writer): + '''Test module-inlining when the target routine is an interface that is + use-associated to a different name in the caller scope. ''' # Create the module containing the subroutine definition, write it to From b4a02a31d787143ce1a1d7cf93c4c20a0b67cba3 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 20 Jun 2025 16:53:12 +0100 Subject: [PATCH 092/100] #2716 update Dev Guide with info on get_callees() --- doc/developer_guide/transformations.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/developer_guide/transformations.rst b/doc/developer_guide/transformations.rst index 05fa34481f..37a0c0d844 100644 --- a/doc/developer_guide/transformations.rst +++ b/doc/developer_guide/transformations.rst @@ -51,13 +51,13 @@ Kernel Transformations PSyclone is able to perform kernel transformations by obtaining the PSyIR representation of the kernel with: -.. automethod:: psyclone.psyGen.CodedKern.get_kernel_schedule +.. automethod:: psyclone.psyGen.CodedKern.get_callees :no-index: -The result of `psyclone.psyGen.Kern.get_kernel_schedule` is a -`psyclone.psyir.nodes.KernelSchedule` which is a specialisation of the -`Routine` class with the `is_program` and `return_type` properties set to -`False` and `None`, respectively. +The result of `psyclone.psyGen.Kern.get_callees` is a list of +`psyclone.psyir.nodes.KernelSchedule` objects. `KernelSchedule` is a +specialisation of the `Routine` class with the `is_program` and `return_type` +properties set to False` and `None`, respectively. In addition to modifying the kernel PSyIR with the desired transformations, the `modified` flag of the `CodedKern` node has to be set. This will let From a185081484ddc41b771a6340f239020ece51f056 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 09:36:30 +0100 Subject: [PATCH 093/100] #2716 mv check on whether mod-inlining required to validate() --- .../kernel_module_inline_trans.py | 24 +++++++++++++++---- .../kernel_module_inline_trans_test.py | 9 ++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 0a84677408..39758a8dc1 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -101,6 +101,8 @@ def validate(self, node, options=None): and there's no Container at the call site to which to add the interface definition. :raises TransformationError: if the kernel cannot be safely inlined. + :raises TransformationError: if the target of the supplied call is + already module inlined. ''' if isinstance(node, CodedKern): @@ -130,6 +132,8 @@ def validate(self, node, options=None): f"'{kname}' due to: {error}" ) from error + needs_inline = False + if len(kernels) > 1: # We can't 'module' inline a call to an interface if there's no # ancestor Container in which to put the interface. @@ -143,10 +147,24 @@ def validate(self, node, options=None): f"Cannot module-inline the call to '{kname}' since it is " f"a polymorphic routine (i.e. an interface) and the call-" f"site is not within a module.") + iface_sym = node.scope.symbol_table.lookup(kname, otherwise=None) + if (not iface_sym or (iface_sym.is_import or + iface_sym.is_unresolved)): + needs_inline = True # Validate the PSyIR of each routine/kernel. for kernel_schedule in kernels: self._validate_schedule(node, kname, kern_or_call, kernel_schedule) + rt_sym = node.scope.symbol_table.lookup(kernel_schedule.name, + otherwise=None) + if (not rt_sym or (rt_sym is not kernel_schedule.symbol) or + (rt_sym.is_import or rt_sym.is_unresolved)): + needs_inline = True + + if not needs_inline: + raise TransformationError( + f"The target of '{node.debug_string().strip()}' is already " + f"module inlined.") def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule): ''' @@ -455,7 +473,7 @@ def apply(self, node, options=None): called_sym = callsite_table.lookup(caller_name, otherwise=None) else: for routine in codes_to_inline: - # N.B.in a PSyKAl DSL, we won't have a RoutineSymbol for the + # N.B. in a PSyKAl DSL, we won't have a RoutineSymbol for the # Kernel that is being called, so we look it up instead of # using node.symbol. called_sym = callsite_table.lookup(caller_name, @@ -464,10 +482,6 @@ def apply(self, node, options=None): (called_sym.is_import or called_sym.is_unresolved)): # This routine is not module-inlined. break - else: - # All routines are module-inlined so there's nothing to do. - # TODO #11 - log this. - return # Deal with the RoutineSymbol that is in scope at the call site. sym_in_ctr = None diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 2005df2753..6a3ccf6fb5 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -1165,9 +1165,12 @@ def test_mod_inline_from_wildcard_import(fortran_reader, fortran_writer, # the same RoutineSymbol new_calls = prog_psyir.walk(Call) assert new_calls[0].routine.symbol is new_calls[1].routine.symbol - # Apply the transformation to the second call. This should silently - # pass as there's nothing to do. - intrans.apply(calls[1]) + # Apply the transformation to the second call. This should fail to validate + # as there's nothing to do. + with pytest.raises(TransformationError) as err: + intrans.validate(calls[1]) + assert ("The target of 'call my_sub(b)' is already module inlined." + in str(err.value)) # We can't compile this because of the use statement. From 655e871dedffe9aa19731cbca8db55734034e2a9 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 10:47:41 +0100 Subject: [PATCH 094/100] #2716 rm MATMUL from list of intrinsics on device --- src/psyclone/psyir/nodes/intrinsic_call.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 746af4f7be..593f70b8fc 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -803,11 +803,13 @@ def is_available_on_device(self): # even with the -gpu=uniform_math flag, ideally it should be # configurable if these are allowed or not. # IntrinsicCall.Intrinsic.LOG10, IntrinsicCall.Intrinsic.REAL, - # The one below are not documented on nvidia compiler + # The ones below are not documented on nvidia compiler IntrinsicCall.Intrinsic.PRODUCT, IntrinsicCall.Intrinsic.SIZE, IntrinsicCall.Intrinsic.SUM, IntrinsicCall.Intrinsic.LBOUND, IntrinsicCall.Intrinsic.MAXVAL, IntrinsicCall.Intrinsic.MINVAL, - IntrinsicCall.Intrinsic.MATMUL, + # MATMUL can fail at link time depending on the precision of + # its arguments. + # IntrinsicCall.Intrinsic.MATMUL, IntrinsicCall.Intrinsic.TINY, IntrinsicCall.Intrinsic.HUGE) @classmethod From 1858ea04e10312ee25c075faba860b42d4cf8725 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 10:48:29 +0100 Subject: [PATCH 095/100] #2716 tidying for review --- .../common/transformations/kernel_module_inline_trans.py | 2 +- src/psyclone/domain/lfric/lfric_kern.py | 2 +- .../transformations/kernel_module_inline_trans_test.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 39758a8dc1..e8ecdf42bd 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -419,7 +419,7 @@ def _rename_import(table: SymbolTable, csym, orig_name=name)) def apply(self, node, options=None): - ''' Bring the kernel/subroutine into this Container. + ''' Bring the implementation of this kernel/call into this Container. NOTE: when applying this transformation to a Kernel in a PSyKAl invoke, *all* Kernels of that name in that invoke are marked as inlined. diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 55521e0b79..18552d3c3f 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -781,7 +781,7 @@ def gen_stub(self) -> Container: return stub_module - def get_interface_symbol(self) -> Union[GenericInterfaceSymbol, None]: + def get_interface_symbol(self) -> Optional[GenericInterfaceSymbol]: ''' :returns: the interface symbol for this kernel if it is polymorphic, None otherwise. diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 6a3ccf6fb5..56234f79fe 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -522,11 +522,11 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): artrans.apply(coded_kern, options={"force": True}) output = str(psy.gen).lower() assert "subroutine mixed_code_32" in output - assert "!$acc routine seq" in output + assert output.count("!$acc routine seq") == 2 assert "subroutine mixed_code_64" in output - # Since we don't currently rename module-inlined kernels, module-inlining - # just one instance means that calls to that same Kernel throughout the - # whole module uses the newly-inlined version. + # Since we don't currently rename module-inlined kernels (TODO #2846), + # module-inlining just one instance means that calls to that same Kernel + # throughout the whole module use the newly-inlined version. assert ("""subroutine invoke_1(scalar_r_phys, field_r_phys, \ operator_r_def, f1, f2, m1, a, m2, istp, qr) use function_space_mod, only : basis, diff_basis From f95cb1afc5dd92fc63d708bd657d6d5ffec91493 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 10:59:44 +0100 Subject: [PATCH 096/100] #2716 fix linting --- src/psyclone/domain/lfric/lfric_kern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 18552d3c3f..79fd71f3fc 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -41,7 +41,7 @@ from collections import OrderedDict from dataclasses import dataclass -from typing import List, Optional, Union +from typing import List, Optional from psyclone.configuration import Config from psyclone.core import AccessType, VariablesAccessMap From 8abadc34c4f95e31dae3432881d68c51ba312295 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 11:16:17 +0100 Subject: [PATCH 097/100] #2716 fix linting of examples --- examples/lfric/scripts/gpu_offloading.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/lfric/scripts/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index 1f242c409e..2e693015af 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -48,8 +48,7 @@ from psyclone.psyir.nodes import ( Call, Directive, IntrinsicCall, Loop, Routine, Schedule) from psyclone.psyir.transformations import ( - ACCKernelsTrans, InlineTrans, Matmul2CodeTrans, OMPTargetTrans, - TransformationError) + ACCKernelsTrans, Matmul2CodeTrans, OMPTargetTrans, TransformationError) from psyclone.transformations import ( LFRicColourTrans, LFRicOMPLoopTrans, LFRicRedundantComputationTrans, OMPParallelTrans, @@ -103,7 +102,6 @@ def trans(psyir): :type psyir: :py:class:`psyclone.psyir.nodes.FileContainer` ''' - intrans = InlineTrans() rtrans = LFRicRedundantComputationTrans() ctrans = LFRicColourTrans() otrans = LFRicOMPLoopTrans() @@ -182,7 +180,8 @@ def trans(psyir): print(f"Failed to module-inline kernel " f"'{kern.name}' due to:\n{err.value}") try: - # Ensure any MATMULs within the kernel are replaced. + # Ensure any MATMULs within the kernel are + # replaced. for routine in kern.get_callees(): _replace_matmuls(routine) # Finally, annotate the kernel routine for GPU. @@ -193,9 +192,10 @@ def trans(psyir): print(f"Failed to annotate '{kern.name}' with " f"GPU-enabled directive due to:\n" f"{err.value}") - # For annotated or inlined kernels we could attempt to - # provide compile-time dimensions for the temporary - # arrays and convert to code any unsupported intrinsics. + # For annotated/inlined kernels we could attempt to + # provide compile-time dimensions for temporary arrays + # and convert to code any unsupported intrinsics. + # Add GPU offloading to loops unless they are over colours or are null. for loop in subroutine.walk(Loop): kernel_names = [k.name.lower() for k in loop.kernels()] From 58ae8c9119af46d3d1ce365d248a596ce5aa8dff Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Jun 2025 11:31:06 +0100 Subject: [PATCH 098/100] #2716 fix test for MATMUL available on device --- src/psyclone/tests/psyir/nodes/intrinsic_call_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py index 02931f0695..8e628d13c7 100644 --- a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py +++ b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py @@ -119,7 +119,7 @@ def test_intrinsiccall_is_inquiry(): (IntrinsicCall.Intrinsic.MAX, True), (IntrinsicCall.Intrinsic.MAXVAL, True), (IntrinsicCall.Intrinsic.ALLOCATE, False), - (IntrinsicCall.Intrinsic.MATMUL, True), + (IntrinsicCall.Intrinsic.MATMUL, False), (IntrinsicCall.Intrinsic.ACOS, True), (IntrinsicCall.Intrinsic.AINT, True), (IntrinsicCall.Intrinsic.ANINT, True), From 2ee067b4734a48b8e75cbf07fe4c6e389deeb8a7 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Mon, 30 Jun 2025 10:35:24 +0100 Subject: [PATCH 099/100] Replace get_kernel_schedule --- .../practicals/LFRic/single_node/3_sequential/matvec_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorial/practicals/LFRic/single_node/3_sequential/matvec_opt.py b/tutorial/practicals/LFRic/single_node/3_sequential/matvec_opt.py index 0c1b04514c..b14a57ccf7 100644 --- a/tutorial/practicals/LFRic/single_node/3_sequential/matvec_opt.py +++ b/tutorial/practicals/LFRic/single_node/3_sequential/matvec_opt.py @@ -65,7 +65,7 @@ def trans(psyir): for kernel in psyir.coded_kernels(): if kernel.name.lower() == "scaled_matrix_vector_code": kernel.modified = True - kernel_schedule = kernel.get_kernel_schedule() + kernel_schedule = kernel.get_callees() # Replace matmul with inline code for icall in kernel_schedule.walk(IntrinsicCall): if icall.intrinsic == IntrinsicCall.Intrinsic.MATMUL: From 00c36fdd9200ca586ec720176cb9e0c53fce8bc8 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Mon, 30 Jun 2025 13:41:30 +0100 Subject: [PATCH 100/100] #2732 Update changelog --- changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog b/changelog index 51b54d46d1..9d6602cfaf 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,6 @@ + 43) PR #2732 for #2716. Add support for module-inlining polymorphic kernels + and rename get_kernel_schedule to get_callees (to match the Call method). + 42) PR #3034 for #2837. Updates the supported versions of Python used in the test suite to 3.10 and 3.13.