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. 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 diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index 04302ef594..c8d0337105 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): @@ -62,7 +62,10 @@ def trans(psyir): 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_callees() + # 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 6a22ff6194..6239779ab3 100644 --- a/examples/lfric/eg15/matvec_opt.py +++ b/examples/lfric/eg15/matvec_opt.py @@ -91,7 +91,10 @@ def trans(psyir): for kernel in psyir.coded_kernels(): if kernel.name.lower() == "matrix_vector_kernel_code": - kernel_schedule = 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] # Replace matmul with inline code for icall in kernel_schedule.walk(IntrinsicCall): if icall.intrinsic is IntrinsicCall.Intrinsic.MATMUL: 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/gpu_offloading.py b/examples/lfric/scripts/gpu_offloading.py index ce2ffec2a3..2e693015af 100644 --- a/examples/lfric/scripts/gpu_offloading.py +++ b/examples/lfric/scripts/gpu_offloading.py @@ -45,9 +45,10 @@ 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.psyir.nodes import ( + Call, Directive, IntrinsicCall, Loop, Routine, Schedule) from psyclone.psyir.transformations import ( - ACCKernelsTrans, TransformationError, OMPTargetTrans) + ACCKernelsTrans, Matmul2CodeTrans, OMPTargetTrans, TransformationError) from psyclone.transformations import ( LFRicColourTrans, LFRicOMPLoopTrans, LFRicRedundantComputationTrans, OMPParallelTrans, @@ -59,9 +60,38 @@ INVOKE_EXCLUSIONS = [ ] +# We won't attempt to inline calls to routines with names that contain +# these strings (because they're not computationally important). +INLINE_EXCLUSIONS = ["abort", "logging"] + OFFLOAD_DIRECTIVES = os.getenv('LFRIC_OFFLOAD_DIRECTIVES', "none") +def _replace_matmuls(sched: Schedule): + ''' + Attempts to replace all MATMUL intrinsic calls with inline + code. + + :param sched: schedule to transform. + + ''' + matrans = Matmul2CodeTrans() + + 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) + + def trans(psyir): '''Applies PSyclone colouring and GPU offloading transformations. Any kernels that cannot be offloaded to GPU are parallelised using OpenMP @@ -106,7 +136,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 @@ -122,7 +152,8 @@ def trans(psyir): 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 @@ -133,19 +164,27 @@ 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}'") except TransformationError as err: - print(f"Failed to module-inline '{kern.name}' due " - f"to:\n{err.value}") + failed_inline.add(kern.name.lower()) + 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(): + _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: @@ -153,9 +192,9 @@ 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 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): diff --git a/examples/lfric/scripts/kernel_print.py b/examples/lfric/scripts/kernel_print.py index 497efd33d4..915fe5efbf 100644 --- a/examples/lfric/scripts/kernel_print.py +++ b/examples/lfric/scripts/kernel_print.py @@ -55,11 +55,11 @@ def trans(psyir): # Loop over all of the Kernels Calls for kernel in psyir.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) + for ksched in kernel.get_callees(): + 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 " + print(f"Code of '{kernel.name}' " f"cannot be printed because:\n{err}") diff --git a/examples/xdsl/backend/xdsl.py b/examples/xdsl/backend/xdsl.py index 1eaabf150b..f03d7715d7 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_callees() + # IGNORE polymorphic routines. + schedule = schedules[0] for child in schedule.children: exec_statements.append(self._visit(child)) return exec_statements 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 a2e5f60bc1..e8ecdf42bd 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,13 +97,12 @@ 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 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. + :raises TransformationError: if the target of the supplied call is + already module inlined. ''' if isinstance(node, CodedKern): @@ -124,18 +123,68 @@ 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: - _, kernel_schedule = ( - 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} " 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. + 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.") + 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): + ''' + 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 add new imports # from this module at the call site, and we don't do this yet). @@ -161,7 +210,7 @@ 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) @@ -177,12 +226,13 @@ def validate(self, node, options=None): # 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 # 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 @@ -267,55 +317,13 @@ def _prepare_code_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 its a container (from + # 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) return copied_routines - @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`. - - :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, :py:class:`psyclone.psyir.nodes.Call`) - - :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. - 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. - 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() - # 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") - return (caller_name, routines[0]) - @staticmethod def _rm_imported_routine_symbol(symbol: Symbol, schedule: Routine, @@ -362,6 +370,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 @@ -370,6 +380,10 @@ def _rm_imported_routine_symbol(symbol: Symbol, # 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.lower()) @@ -405,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. @@ -428,6 +442,17 @@ 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. # Note that we use the resolved callee subroutine name and not the @@ -436,28 +461,31 @@ 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 = ( - KernelModuleInlineTrans._get_psyir_to_inline(node)) - callee_name = code_to_inline.name - codes_to_inline = [code_to_inline] + codes_to_inline = node.get_callees() + 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 - 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 using - # node.symbol. + if interface_sym: 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. - break else: - # All routines are module-inlined so there's nothing to do. - # TODO #11 - log this. - return + 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 + # using node.symbol. + 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. + break # 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) @@ -467,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 @@ -478,33 +506,24 @@ 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 updated_routines = self._prepare_code_to_inline(codes_to_inline) - # Update the Kernel to point to the updated PSyIR. - if isinstance(node, CodedKern): - # TODO #924 - this will need updating to support kernels with - # multiple schedules (mixed precision). - # pylint: disable=protected-access - node._kern_schedule = updated_routines[0] - if callee_name != caller_name: - node.name = callee_name - # The Container into which we will inline the Routine(s). container = node.ancestor(Container) @@ -561,14 +580,60 @@ 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 - # Set the module-inline flag to avoid generating the kernel imports + 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) + if shadowed_sym: + self._rm_imported_routine_symbol(shadowed_sym, + codes_to_inline[0], + caller_cntr_table) + 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) + 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 # 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): + 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._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 12c826075f..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 @@ -179,63 +179,64 @@ def apply(self, node, options=None): inner_loop.iteration_space = "go_all_pts" outer_loop.iteration_space = "go_all_pts" - # Update Kernel - kschedule = 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, - BinaryOperation.create( + # Update Kernel implementation(s). + for kschedule in node.get_callees(): + + 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 ee76e2f142..a0dbf8e100 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -113,27 +113,26 @@ 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 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( @@ -147,17 +146,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,19 +194,21 @@ def validate(self, node, options=None): # type information). for kern in node.kernels(): KernelModuleInlineTrans().validate(kern) - ksched = 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 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_callees(): + + 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(): @@ -753,7 +756,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() + # validate() has checked that the kernel is not polymorphic. + schedule = kernel.get_callees()[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 18e9724e2c..79fd71f3fc 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -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,72 +781,82 @@ def gen_stub(self) -> Container: return stub_module - 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. + def get_interface_symbol(self) -> Optional[GenericInterfaceSymbol]: + ''' + :returns: the interface symbol for this kernel if it is polymorphic, + None otherwise. + ''' + kscheds = self.get_callees() + if len(kscheds) == 1: + return None + cntr = kscheds[0].ancestor(Container) + return cntr.symbol_table.lookup(self.name) + + 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 + 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: Schedule representing the kernel code. - :rtype: :py:class:`psyclone.psyGen.KernelSchedule` + :returns: the Schedule(s) representing the kernel implementation. - :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_schedule: - return self._kern_schedule - - # Get the PSyIR Kernel Schedule(s) - routines = Fparser2Reader().get_routine_schedules(self.name, self.ast) - 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. - 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._kern_schedule = ksched - - return self._kern_schedule + ''' + if self._schedules: + return self._schedules + + # 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: + 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 + else: + raise InternalError( + f"Failed to find any routines for Kernel '{self.name}'. " + f"Source of Kernel is:\n{self.ast}") + + new_schedules = [] + for routine 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( + 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._schedules = new_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 d6e79ddfe3..b16308804c 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1075,16 +1075,21 @@ 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 list comprising just one Schedule. + :returns: a schedule representing the GOcean kernel code. - :rtype: :py:class:`psyclone.gocean1p0.GOKernelSchedule` + :rtype: list[: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 + if self._schedules: + return self._schedules # Construct the PSyIR of the Fortran parse tree. astp = Fparser2Reader() @@ -1105,9 +1110,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._schedules = [routine] - return self._kern_schedule + return self._schedules class GOKernelArguments(Arguments): diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 3b806a5074..1682e7142a 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1333,31 +1333,40 @@ 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 schedule for the kernel - # Whether or not this kernel has been transformed + self._fp2_ast = None #: The fparser2 AST for the kernel + #: PSyIR schedule(s) for the kernel + self._schedules = None + #: Whether or not this kernel has been transformed self._modified = False - # Whether or not to in-line this kernel into the module containing - # the PSy layer + #: 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 - def get_kernel_schedule(self): + def get_interface_symbol(self) -> None: ''' - 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. + By default, a Kern is not polymorphic and therefore has no interface + symbol. - :returns: Schedule representing the kernel code. - :rtype: :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) - # TODO: Validate kernel with metadata (issue #288). - return self._kern_schedule + return None + + def get_callees(self): + ''' + Returns the PSyIR Schedule(s) representing the kernel code. The + Schedules are just generated on first invocation, this allows us to + retain transformations that may subsequently be applied to the + Schedule(s). + + :returns: Schedule(s) representing the kernel code. + :rtype: list[:py:class:`psyclone.psyir.nodes.KernelSchedule`] + + :raises NotImplementedError: must be overridden in sub-class. + + ''' + raise NotImplementedError( + f"get_callees() must be overridden in class {self.__class__}") @property def opencl_options(self): @@ -1500,7 +1509,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: @@ -1656,7 +1665,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_callees() + new_kern_code = fortran_writer(schedules[0].root) fll = FortLineLength() new_kern_code = fll.process(new_kern_code) @@ -1694,28 +1704,36 @@ 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 - kern_schedule = self.get_kernel_schedule() - container = kern_schedule.ancestor(Container) + # We need to get the kernel schedule before modifying self.name. + kern_schedules = self.get_callees() + 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 = self.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. + 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") + 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/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py index 8859342e0d..79169d4f79 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, Tuple from psyclone.configuration import Config from psyclone.core import AccessType, VariablesAccessMap -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 @@ -48,6 +49,7 @@ from psyclone.psyir.nodes.reference import Reference from psyclone.psyir.nodes.routine import Routine from psyclone.psyir.symbols import ( + GenericInterfaceSymbol, DefaultModuleInterface, RoutineSymbol, Symbol, @@ -55,8 +57,6 @@ UnsupportedFortranType, DataSymbol, ) -from typing import List, Tuple -from psyclone.errors import PSycloneError class CallMatchingArgumentsNotFound(PSycloneError): @@ -479,25 +479,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 @@ -508,15 +489,22 @@ def _location_txt(node): cursor = table.node have_codeblock = False while cursor: + # We want to look in both Containers and FileContainers. if isinstance(cursor, Container): - psyir = cursor.find_routine_psyir(rsym.name, - allow_private=True) - if psyir: + 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(name, + allow_private=True) + if psyir: + routines.append(psyir) + if routines: rsym.interface = DefaultModuleInterface() - return [psyir] - if not have_codeblock: - have_codeblock = any(isinstance(child, CodeBlock) for - child in cursor.children) + 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)] @@ -526,16 +514,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 @@ -563,12 +552,17 @@ 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. 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: @@ -577,7 +571,12 @@ 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}") - imported_sym = container.symbol_table.lookup(cursor.name) + 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(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 @@ -585,7 +584,7 @@ def _location_txt(node): # 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 " @@ -598,33 +597,40 @@ 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. cursor = container while cursor and isinstance(cursor, Container): routines = [] - for name in cursor.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 = cursor.find_routine_psyir( name, allow_private=can_be_private) if psyir: routines.append(psyir) - if routines: + if all_names and len(routines) == len(all_names): + # We've resolved everything. 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( diff --git a/src/psyclone/psyir/nodes/container.py b/src/psyclone/psyir/nodes/container.py index c130dcdafa..4dd15a0ae1 100644 --- a/src/psyclone/psyir/nodes/container.py +++ b/src/psyclone/psyir/nodes/container.py @@ -233,9 +233,9 @@ 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, + scope_limit=self) + if not rsym: return [] if isinstance(rsym, GenericInterfaceSymbol): return [rt.symbol.name.lower() for rt in rsym.routines] diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index bbfc39d7d8..252dc45ae8 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -976,6 +976,9 @@ def is_inquiry(self): IntrinsicCall.Intrinsic.MAXVAL, IntrinsicCall.Intrinsic.MINVAL, IntrinsicCall.Intrinsic.TINY, IntrinsicCall.Intrinsic.HUGE ) +# MATMUL can fail at link time depending on the precision of +# its arguments. +# IntrinsicCall.Intrinsic.MATMUL, # All nvfortran intrinsics available on GPUs NVFORTRAN_ALL = NVFORTRAN_UNIFORM + ( diff --git a/src/psyclone/psyir/nodes/routine.py b/src/psyclone/psyir/nodes/routine.py index 236c151f87..b78a26fd63 100644 --- a/src/psyclone/psyir/nodes/routine.py +++ b/src/psyclone/psyir/nodes/routine.py @@ -358,7 +358,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) # As we now have the RoutineSymbol back in a Container, we @@ -413,13 +414,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): diff --git a/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py b/src/psyclone/psyir/transformations/intrinsics/matmul2code_trans.py index 1f3343b558..329f291564 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( diff --git a/src/psyclone/psyir/transformations/omp_task_trans.py b/src/psyclone/psyir/transformations/omp_task_trans.py index 45f898f824..d24dfd4fa8 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_callees() + 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,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_callees() + for sched in schedules: + cond_trans.apply(sched) kern.lower_to_language_level() calls = node.walk(Call) 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 f0a8cbe68d..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 @@ -47,10 +47,11 @@ 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 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, @@ -102,7 +103,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_callees() + kern_schedule = kern_schedules[0] mymod = kern_schedule.symbol_table.new_symbol( "mymod", symbol_type=ContainerSymbol) @@ -117,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. @@ -132,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 " @@ -160,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_callees() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild(block) with pytest.raises(TransformationError) as err: inline_trans.validate(kernels[0]) @@ -175,9 +179,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_callees() + 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") @@ -190,8 +196,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_callees() + ksched = kschedules[0] + ksched.pop_all_children() + ksched.addchild( IntrinsicCall.create(IntrinsicCall.Intrinsic.DATE_AND_TIME, [])) inline_trans.validate(kernels[0]) @@ -254,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_schedule", routine) + monkeypatch.setattr(kern_call, "_schedules", [routine]) # and try to apply the transformation inline_trans = KernelModuleInlineTrans() @@ -277,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_schedule", routine) + monkeypatch.setattr(kern_call, "_schedules", [routine]) # and try to apply the transformation with pytest.raises(TransformationError) as err: @@ -299,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_schedule", routine) + monkeypatch.setattr(kern_call, "_schedules", [routine]) container = kern_call.ancestor(Container) assert "compute_cv_code" not in container.symbol_table @@ -403,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_schedule", 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 @@ -471,18 +479,16 @@ 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 - # 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 - - # 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": @@ -497,6 +503,40 @@ 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 output.count("!$acc routine seq") == 2 + assert "subroutine mixed_code_64" in output + # 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 + 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) + + def test_module_inline_apply_with_sub_use(tmpdir): ''' Test that we can module inline a kernel subroutine which contains a use statement''' @@ -564,8 +604,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -586,8 +626,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -610,8 +650,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -634,8 +674,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -655,8 +695,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine,) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -675,8 +715,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -692,8 +732,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -712,8 +752,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -730,8 +770,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_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 @@ -746,8 +786,8 @@ def test_module_inline_apply_bring_in_non_local_symbols( end module my_mod ''') routine = psyir.walk(Routine)[0] - (new_routine, ) = inline_trans._prepare_code_to_inline([routine]) - result = fortran_writer(new_routine) + new_routines = inline_trans._prepare_code_to_inline([routine]) + result = fortran_writer(new_routines[0]) assert "use external_mod, only : a" in result @@ -785,39 +825,117 @@ 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) - 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 + 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]) + 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 # And it is valid code assert LFRicBuild(tmpdir).code_compiles(psy) -def test_get_psyir_to_inline(monkeypatch): +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 that _get_psyir_to_inline() raises the expected error if more than - one potential routine implementation is found. + # 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. ''' - 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]) + # 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: - 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)) + 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): @@ -862,11 +980,12 @@ def test_rm_imported_routine_symbol(fortran_reader): 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): @@ -882,9 +1001,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 ''' @@ -893,13 +1014,16 @@ def test_psyir_mod_inline(fortran_reader, fortran_writer, tmpdir, 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, 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 @@ -918,12 +1042,29 @@ 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 - # Check that repeating the transformation does nothing. - intrans.apply(calls[0]) - output2 = fortran_writer(psyir) - assert output2 == output - # We can't test the compilation of this code because of the 'use my_mod.' + assert "use my_mod, only : my_interface, my_other_sub\n" in output + + # 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'. + 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 + # 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) @pytest.mark.usefixtures("clear_module_manager_instance") @@ -1024,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. diff --git a/src/psyclone/tests/domain/gocean/kernel/gokern_test.py b/src/psyclone/tests/domain/gocean/kernel/gokern_test.py index 75dd0ef46d..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"), @@ -99,15 +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_schedule is None - sched = kern.get_kernel_schedule() + assert kern._schedules is None + 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. - sched2 = kern.get_kernel_schedule() - assert sched2 is sched + 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. - kern._kern_schedule = None + kern._schedules = None # Remove the subroutine that implements the kernel from the Fortran # parse tree. subs = walk(kern.ast, Fortran2003.Subroutine_Subprogram) @@ -116,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 166c2c2efe..c9a6bb70ee 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -34,8 +34,7 @@ # 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 @@ -158,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_callees()[0].symbol_table.lookup("rdt") assert ksymbol.is_argument - assert kernel.get_kernel_schedule().symbol_table.argument_list[-1] == \ + assert kernel.get_callees()[0].symbol_table.argument_list[-1] == \ ksymbol - assert len(kernel.get_kernel_schedule().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()) + 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 @@ -211,7 +210,8 @@ def create_data_symbol(arg): trans.apply(kernel) fwriter = FortranWriter() - kernel_code = fwriter(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, " "magic)" in kernel_code) @@ -289,7 +289,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_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 eb1491dec4..e2e8f5a31c 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -1494,14 +1494,15 @@ 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_callees() + code = fortran_writer(schedules[0]) assert "!$acc routine seq\n" in code # If the kernel schedule is not accessible, the transformation fails 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 " @@ -1517,16 +1518,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_callees() + 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 311425467d..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 @@ -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) from psyclone.psyir.symbols import ArgumentInterface from psyclone.gocean1p0 import GOLoop from psyclone.psyir.transformations import TransformationError -from psyclone.psyir.backend.fortran import FortranWriter API = "gocean" @@ -59,11 +59,11 @@ 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.''' 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)) @@ -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_callees() + ksched = routines[0] + ksched.symbol_table.new_symbol("ystart") # Apply the transformation trans = GOMoveIterationBoundariesInsideKernelTrans() @@ -118,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() + kschedule = kernel.get_callees()[0] # - It has the boundary conditions mask assert isinstance(kschedule.children[0], IfBlock) @@ -151,7 +153,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. @@ -212,5 +215,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 diff --git a/src/psyclone/tests/domain/lfric/lfric_kern_test.py b/src/psyclone/tests/domain/lfric/lfric_kern_test.py index bd2762a1fa..5c87defff6 100644 --- a/src/psyclone/tests/domain/lfric/lfric_kern_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_kern_test.py @@ -48,14 +48,17 @@ 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.nodes import Reference, KernelSchedule -from psyclone.psyir.symbols import ArgumentInterface, DataSymbol, REAL_TYPE, \ - INTEGER_TYPE, ArrayType +from psyclone.psyir.frontend.fparser2 import Fparser2Reader +from psyclone.psyir.nodes import Container, KernelSchedule, Reference, Routine +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 @@ -136,8 +139,8 @@ def test_kern_getter_errors(): in str(err.value)) -def test_get_kernel_schedule(): - '''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. @@ -149,17 +152,70 @@ def test_get_kernel_schedule(): # matrix vector kernel kernel = schedule[2].loop_body[0] - assert kernel._kern_schedule is None + assert kernel._schedules is None + + 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_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. + 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")) + with pytest.raises(InternalError) as err: + kernel.get_callees() + assert ("Failed to find any routines for Kernel 'matrix_vector_code'" + in str(err.value)) - kernel_schedule = kernel.get_kernel_schedule() - assert isinstance(kernel_schedule, KernelSchedule) - assert kernel._kern_schedule is kernel_schedule - kernel_schedule_2 = kernel.get_kernel_schedule() - assert kernel_schedule is kernel_schedule_2 +def test_get_callees_same_container(monkeypatch): + ''' + Check that get_callees() 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_callees() to search. + monkeypatch.setattr(kern, "_schedules", None) + 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_callees_mixed_precision(): + ''' + Test that get_callees() and get_interface_symbol() work for a + mixed-precision kernel. -def test_get_kernel_schedule_mixed_precision(): + ''' + _, 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_callees()) == 2 + isym = kern.get_interface_symbol() + assert isinstance(isym, GenericInterfaceSymbol) + assert isym.name == "mixed_code" + + +@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_callees_mixed_precision_match(): ''' Test that we can get the correct schedule for a mixed-precision kernel. @@ -178,12 +234,16 @@ def test_get_kernel_schedule_mixed_precision(): # 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}" -def test_get_kernel_sched_mixed_precision_no_match(monkeypatch): +@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_callees_mixed_precision_no_match(monkeypatch): ''' Test that we get the expected error if there's no matching implementation for a mixed-precision kernel. @@ -202,36 +262,12 @@ 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)) -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 @@ -246,7 +282,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_callees() + sched = schedules[0] kernel.validate_kernel_code_args(sched.symbol_table) # Force LFRicKern to think that this kernel is an 'apply' kernel and @@ -254,7 +291,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/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index c7206e4ea8..ed7ccf6cd0 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() + 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() + 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 79ce3489a9..461b65d070 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -591,16 +591,17 @@ 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_schedule = kern.get_kernel_schedule() - assert isinstance(kern_schedule, KernelSchedule) + kern_schedules = kern.get_callees() + assert len(kern_schedules) == 1 + assert isinstance(kern_schedules[0], KernelSchedule) def test_codedkern_node_str(): @@ -834,6 +835,25 @@ def test_kern_children_validation(): "is a LeafNode and doesn't accept children.") in str(excinfo.value) +def test_codedkern_get_callees(monkeypatch): + ''' + Check that CodedKern.get_callees() raises a NotImplementedError + (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) + metadata = LFRicKernMetadata(ast) + kern = LFRicKern() + kern.load_meta(metadata) + monkeypatch.setattr(kern, "__class__", CodedKern) + with pytest.raises(NotImplementedError) as err: + kern.get_callees() + assert ("get_callees() must be overridden in class " + in str(err.value)) + assert kern.get_interface_symbol() is None + + 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 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 1c8d0fdc20..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,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_callees() + kernel_schedule = kernel_schedules[0] references = kernel_schedule.walk(Reference) # Symbol in KernelSchedule SymbolTable diff --git a/src/psyclone/tests/psyir/nodes/call_test.py b/src/psyclone/tests/psyir/nodes/call_test.py index c40fdfcd81..7e7c1942ed 100644 --- a/src/psyclone/tests/psyir/nodes/call_test.py +++ b/src/psyclone/tests/psyir/nodes/call_test.py @@ -42,13 +42,13 @@ from psyclone.core import Signature from psyclone.errors import GenerationError 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) + ArrayType, INTEGER_TYPE, ContainerSymbol, DataSymbol, NoType, + RoutineSymbol, REAL_TYPE, SymbolError, UnresolvedInterface) class SpecialCall(Call): @@ -675,6 +675,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 @@ -1405,7 +1444,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. @@ -1423,6 +1462,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): @@ -1464,47 +1510,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 @@ -1556,13 +1561,19 @@ 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] 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 ("Failed to find a Routine named 'bottom' in code:\n'" + "subroutine top()" in str(err.value)) def test_call_get_callees_import_local_container(fortran_reader): 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 diff --git a/src/psyclone/tests/psyir/nodes/routine_test.py b/src/psyclone/tests/psyir/nodes/routine_test.py index 6255a4db93..b6b9aaefe7 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_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] - ksched = kcall0.get_kernel_schedule() - ctable = ksched.ancestor(Container).symbol_table + 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. 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") diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py index c04076d2ad..d3a2b2fccb 100644 --- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py @@ -1526,6 +1526,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) diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 7b087a0906..50cf0101f2 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -267,6 +267,48 @@ 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): + ''' + 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() + psy, invoke = get_invoke("26.8_mixed_precision_args.f90", + api="lfric", idx=0) + sched = invoke.schedule + 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 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 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(op_ncell_3d,ndf_w0,ndf_w0), intent(in) :: op + + !$acc routine seq''' in contents) + 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) + kernels = sched.coded_kernels() + rtrans.apply(kernels[1], options={"force": True}) + assert LFRicBuild(kernel_outputdir).code_compiles(psy) + + # The following tests test the MarkRoutineForGPUMixin validation, for this # it uses the ACCRoutineTrans as instance of this Mixin. @@ -283,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 @@ -317,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: @@ -424,7 +442,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_callees() + call = kschedules[0].walk(Call)[0] call.replace_with( IntrinsicCall.create(IntrinsicCall.Intrinsic.GET_COMMAND)) with pytest.raises(TransformationError) as err: @@ -449,7 +468,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_callees() + code = fortran_writer(kschedules[0]) assert expected_directive in code diff --git a/src/psyclone/tests/test_files/lfric/3.5_multi_polymorphic_kernels_multi_invokes.f90 b/src/psyclone/tests/test_files/lfric/3.5_multi_polymorphic_kernels_multi_invokes.f90 new file mode 100644 index 0000000000..ac2e35832c --- /dev/null +++ b/src/psyclone/tests/test_files/lfric/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 diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 8ea4d49d97..7397c20a21 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -62,7 +62,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, Return, @@ -385,8 +385,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). @@ -415,101 +413,89 @@ 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_callees() except Exception as error: raise TransformationError( f"Failed to create PSyIR for kernel '{node.name}'. " f"Cannot transform such a kernel.") from error - if not node.ancestor(Container, shared_with=kernel_schedule): - # The KernelSchedule to be transformed has not been inlined - # into the Container of the call-site. Therefore we can - # 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. - if len(kernel_schedule.root.walk(Routine)) > 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. - 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. - vam = kernel_schedule.reference_accesses() - ktable = kernel_schedule.symbol_table - for sig in vam.all_signatures: - name = sig.var_name - first = vam[sig].all_accesses[0].node - if isinstance(first, (Symbol, DataType)): - 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: + vam = sched.reference_accesses() + ktable = sched.symbol_table + for sig in vam.all_signatures: + name = sig.var_name + first = vam[sig].all_accesses[0].node + if isinstance(first, (Symbol, DataType)): 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 = kernel_schedule.walk(Call) - for call in calls: - if not call.is_available_on_device(device_string): - if isinstance(call, IntrinsicCall): - if device_string: - device_str = (f"on the '{device_string}' accelerator " - f"device") - else: - device_str = "on the default accelerator device" + 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}' calls intrinsic " - f"'{call.intrinsic.name}' which is not available " - f"{device_str}. Use the 'device_string' option to " - f"specify a different 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).") + 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"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.") + + for call in sched.walk(Call): + if not call.is_available_on_device(device_string): + if isinstance(call, IntrinsicCall): + if device_string: + device_str = (f"on the '{device_string}' " + f"accelerator device") + else: + device_str = "on the default accelerator device" + raise TransformationError( + f"{k_or_r} '{node.name}' calls intrinsic " + f"'{call.intrinsic.name}' which is not available " + f"{device_str}. Use the 'device_string' option to " + f"specify a different 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): @@ -577,15 +563,14 @@ def apply(self, node, options=None): node.modified = True # Get the schedule representing the kernel subroutine - routine = node.get_kernel_schedule() + routines = node.get_callees() else: - routine = node + routines = [node] - for child in routine.children: - if isinstance(child, OMPDeclareTargetDirective): - return # The routine is already marked with OMPDeclareTarget - - routine.children.insert(0, OMPDeclareTargetDirective()) + for routine in routines: + 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. @@ -2288,7 +2273,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()) @@ -2459,16 +2444,17 @@ 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_callees() 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) + 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 @@ -2810,19 +2796,20 @@ def apply(self, node, options=None): # Flag that the kernel has been modified node.modified = True - # Get the schedule representing the kernel subroutine - routine = node.get_kernel_schedule() + # Get the schedule(s) representing the kernel subroutine + routines = node.get_callees() 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): ''' @@ -3033,10 +3020,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): @@ -3053,20 +3042,23 @@ 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_callees() except SymbolError as err: raise TransformationError( 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): ''' @@ -3082,7 +3074,9 @@ def apply(self, node, options=None): ''' self.validate(node, options) - kernel = 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 invoke_symtab = node.ancestor(InvokeSchedule).symbol_table count_imported_vars_removed = 0 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: