39 ''' This module implements the PSyclone LFRic API by specialising the PSyLoop
40 base class from psyGen.py.
53 ArrayReference, ACCRegionDirective,
54 OMPRegionDirective, Routine)
60 The LFRic-specific PSyLoop class. This passes the LFRic-specific
61 loop information to the base class so it creates the one
62 we require. Creates LFRic-specific loop bounds when the code is
65 :param str loop_type: the type (iteration space) of this loop.
66 :param kwargs: additional keyword arguments provided to the PSyIR node.
67 :type kwargs: unwrapped dict.
69 :raises InternalError: if an unrecognised loop_type is specified.
73 def __init__(self, loop_type="", **kwargs):
75 super().__init__(valid_loop_types=const.VALID_LOOP_TYPES, **kwargs)
84 tag =
"colours_loop_idx"
85 suggested_name =
"colour"
88 suggested_name =
"cell"
94 suggested_name =
"cell"
97 f
"Unsupported loop type '{self.loop_type}' found when "
98 f
"creating loop variable. Supported values are 'colours', "
99 f
"'colour', 'dof' or '' (for cell-columns).")
101 self.
variablevariable = self.scope.symbol_table.find_or_create_tag(
102 tag, root_name=suggested_name, symbol_type=DataSymbol,
103 datatype=
LFRicTypes(
"LFRicIntegerScalarDataType")())
106 self.addchild(Literal(
"NOT_INITIALISED", INTEGER_TYPE,
108 self.addchild(Literal(
"NOT_INITIALISED", INTEGER_TYPE,
110 self.addchild(Literal(
"1", INTEGER_TYPE, parent=self))
111 self.addchild(Schedule(parent=self))
120 '''In-place replacement of DSL or high-level concepts into generic
121 PSyIR constructs. This function replaces a LFRicLoop with a PSyLoop
122 and inserts the loop boundaries into the new PSyLoop, or removes
123 the loop node in case of a domain kernel. Once TODO #1731 is done
124 (which should fix the loop boundaries, which atm rely on index of
125 the loop in the schedule, i.e. can change when transformations are
126 applied), this function can likely be removed.
128 :returns: the lowered version of this node.
129 :rtype: :py:class:`psyclone.psyir.node.Node`
135 for child
in self.loop_body.children:
136 child.validate_global_constraints()
142 step = self.step_expr.copy()
145 for child
in self.loop_body.children:
146 child.lower_to_language_level()
149 loop = Loop.create(self._variable, start, stop, step, [])
150 loop.loop_body._symbol_table = \
151 self.loop_body.symbol_table.shallow_copy()
152 loop.children[3] = self.loop_body.copy()
153 self.replace_with(loop)
158 for child
in self.loop_body.children:
159 child.lower_to_language_level()
162 if len(self.loop_body.children) > 1:
163 raise NotImplementedError(
164 f
"Lowering LFRic domain loops that produce more than one "
165 f
"children is not yet supported, but found:\n "
167 lowered_node = self.loop_body[0].detach()
168 self.replace_with(lowered_node)
173 ''' Creates a text summary of this loop node. We override this
174 method from the Loop class because, in Dynamo0.3, the function
175 space is now an object and we need to call orig_name on it. We
176 also include the upper loop bound as this can now be modified.
178 :param bool colour: whether or not to include control codes for colour.
180 :returns: text summary of this node, optionally with control codes \
181 for colour highlighting.
186 return f
"{self.coloured_name(colour)}[type='null']"
189 upper_bound = (f
"{self._upper_bound_name}"
190 f
"({self._upper_bound_halo_depth})")
193 return (f
"{self.coloured_name(colour)}[type='{self._loop_type}', "
194 f
"field_space='{self._field_space.orig_name}', "
195 f
"it_space='{self.iteration_space}', "
196 f
"upper_bound='{upper_bound}']")
200 Load the state of this Loop using the supplied Kernel
201 object. This method is provided so that we can individually
202 construct Loop objects for a given kernel call.
204 :param kern: Kernel object to use to populate state of Loop
205 :type kern: :py:class:`psyclone.domain.lfric.LFRicKern`
207 :raises GenerationError: if the field updated by the kernel has an \
208 unexpected function space or if the kernel's 'operates-on' is \
209 not consistent with the loop type.
214 self.
_field_field_field = kern.arguments.iteration_space_arg()
220 f
"A LFRicLoop of type 'null' can only contain a kernel that "
221 f
"operates on the 'domain' but kernel '{kern.name}' operates "
222 f
"on '{kern.iterates_over}'.")
228 if isinstance(kern, LFRicBuiltIn):
231 if Config.get().api_conf(
"dynamo0.3").compute_annexed_dofs \
232 and Config.get().distributed_memory \
233 and not kern.is_reduction:
238 if Config.get().distributed_memory:
244 const.VALID_DISCONTINUOUS_NAMES):
249 const.CONTINUOUS_FUNCTION_SPACES):
258 if not kern.all_updates_are_writes:
263 const.VALID_ANY_SPACE_NAMES):
268 if not kern.all_updates_are_writes:
274 f
"Unexpected function space found. Expecting one of "
275 f
"{const.VALID_FUNCTION_SPACES} but found "
276 f
"'{self.field_space.orig_name}'")
281 ''' Set the lower bounds of this loop '''
283 if name
not in const.VALID_LOOP_BOUNDS_NAMES:
285 "The specified lower bound loop name is invalid")
286 if name
in [
"inner"] + const.HALO_ACCESS_LOOP_BOUNDS
and index < 1:
288 "The specified index '{index}' for this lower loop bound is "
294 '''Set the upper bound of this loop
296 :param name: A loop upper bound name. This should be a supported name.
298 :param index: An optional argument indicating the depth of halo
303 if name
not in const.VALID_LOOP_BOUNDS_NAMES:
305 f
"The specified upper loop bound name is invalid. Expected "
306 f
"one of {const.VALID_LOOP_BOUNDS_NAMES} but found '{name}'")
313 if name
in ([
"inner"] + const.HALO_ACCESS_LOOP_BOUNDS)
and \
317 f
"The specified index '{index}' for this upper loop bound "
324 ''' Returns the name of the upper loop bound '''
329 '''Returns the index of the upper loop bound. This is None if the upper
330 bound name is not in HALO_ACCESS_LOOP_BOUNDS.
332 :returns: the depth of the halo for a loops upper bound. If it \
333 is None then a depth has not been provided. The depth value is \
334 only valid when the upper-bound name is associated with a halo \
341 def _lower_bound_fortran(self):
343 Create the associated Fortran code for the type of lower bound.
345 TODO: Issue #440. lower_bound_fortran should generate PSyIR.
347 :returns: the Fortran code for the lower bound.
350 :raises GenerationError: if self._lower_bound_name is not "start" \
352 :raises GenerationError: if self._lower_bound_name is unrecognised.
355 if not Config.get().distributed_memory
and \
358 f
"The lower bound must be 'start' if we are sequential but "
359 f
"found '{self._upper_bound_name}'")
368 prev_space_name =
"inner"
369 prev_space_index_str =
"1"
372 prev_space_name =
"ncells"
373 prev_space_index_str =
""
380 f
"Unsupported lower bound name '{self._lower_bound_name}' "
384 mesh_obj_name = self.ancestor(InvokeSchedule).symbol_table.\
385 find_or_create_tag(
"mesh").name
386 return mesh_obj_name +
"%get_last_" + prev_space_name +
"_cell(" \
387 + prev_space_index_str +
")+1"
390 def _mesh_name(self):
392 :returns: the name of the mesh variable from which to get the bounds \
412 return self.ancestor(InvokeSchedule).symbol_table.\
413 lookup_with_tag(tag_name).name
415 def _upper_bound_fortran(self):
416 ''' Create the Fortran code that gives the appropriate upper bound
417 value for this type of loop.
419 TODO: Issue #440. upper_bound_fortran should generate PSyIR.
421 :returns: Fortran code for the upper bound of this loop.
434 kernels = self.walk(LFRicKern)
437 "Failed to find a kernel within a loop over colours.")
441 if not kern.ncolours_var:
443 f
"All kernels within a loop over colours must have "
444 f
"been coloured but kernel '{kern.name}' has not")
445 return kernels[0].ncolours_var
451 root_name =
"last_edge_cell_all_colours"
455 InvokeSchedule).symbol_table.find_or_create_tag(root_name)
456 return f
"{sym.name}(colour)"
461 sym_tab = self.ancestor(InvokeSchedule).symbol_table
468 depth = sym_tab.find_or_create_tag(
469 f
"max_halo_depth_{self._mesh_name}").name
470 root_name =
"last_halo_cell_all_colours"
473 sym = sym_tab.find_or_create_tag(root_name)
474 return f
"{sym.name}(colour, {depth})"
476 if Config.get().distributed_memory:
478 result = (f
"{self.field.proxy_name_indexed}%"
479 f
"{self.field.ref_name()}%get_last_dof_owned()")
482 f
"{self.field.proxy_name_indexed}%"
483 f
"{self.field.ref_name()}%get_last_dof_annexed()")
488 if Config.get().distributed_memory:
489 result = f
"{self._mesh_name}%get_last_edge_cell()"
491 result = (f
"{self.field.proxy_name_indexed}%"
492 f
"{self.field.ref_name()}%get_ncell()")
495 if Config.get().distributed_memory:
496 return f
"{self._mesh_name}%get_last_halo_cell({halo_index})"
498 "'cell_halo' is not a valid loop upper bound for "
499 "sequential/shared-memory code")
501 if Config.get().distributed_memory:
502 return (f
"{self.field.proxy_name_indexed}%"
503 f
"{self.field.ref_name()}%get_last_dof_halo("
506 "'dof_halo' is not a valid loop upper bound for "
507 "sequential/shared-memory code")
509 if Config.get().distributed_memory:
510 return f
"{self._mesh_name}%get_last_inner_cell({halo_index})"
512 "'inner' is not a valid loop upper bound for "
513 "sequential/shared-memory code")
515 f
"Unsupported upper bound name '{self._upper_bound_name}' found "
516 f
"in lfricloop.upper_bound_fortran()")
518 def _halo_read_access(self, arg):
520 Determines whether the supplied argument has (or might have) its
521 halo data read within this loop. Returns True if it does, or if
522 it might and False if it definitely does not.
524 :param arg: an argument contained within this loop.
525 :type arg: :py:class:`psyclone.dynamo0p3.DynArgument`
527 :returns: True if the argument reads, or might read from the \
528 halo and False otherwise.
531 :raises GenerationError: if an unsupported upper loop bound name is \
532 provided for kernels with stencil access.
533 :raises InternalError: if an unsupported field access is found.
534 :raises InternalError: if an unsupported argument type is found.
538 if arg.is_scalar
or arg.is_operator:
543 if arg.access
in [AccessType.WRITE]:
546 if arg.access
in AccessType.all_read_accesses():
548 if arg.descriptor.stencil:
551 f
"Loop bounds other than 'cell_halo' and 'ncells' "
552 f
"are currently unsupported for kernels with "
553 f
"stencil accesses. Found "
554 f
"'{self._upper_bound_name}'.")
566 if (
not arg.discontinuous
and
574 if not arg.discontinuous
and \
579 return not Config.get().api_conf(
"dynamo0.3").\
584 f
"Unexpected field access type '{arg.access}' found for arg "
587 f
"Expecting arg '{arg.name}' to be an operator, scalar or field, "
588 f
"but found '{arg.argument_type}'.")
590 def _add_field_component_halo_exchange(self, halo_field, idx=None):
591 '''An internal helper method to add the halo exchange call immediately
592 before this loop using the halo_field argument for the
593 associated field information and the optional idx argument if
594 the field is a vector field.
596 In certain situations the halo exchange will not be
597 required. This is dealt with by adding the halo exchange,
598 asking it if it is required and then removing it if it is
599 not. This may seem strange but the logic for determining
600 whether a halo exchange is required is within the halo
601 exchange class so it is simplest to do it this way
603 :param halo_field: the argument requiring a halo exchange
604 :type halo_field: :py:class:`psyclone.dynamo0p3.DynArgument`
605 :param index: optional argument providing the vector index if
606 there is one and None if not. Defaults to None.
607 :type index: int or None
609 :raises InternalError: if there are two forward write \
610 dependencies and they are both associated with halo \
617 exchange = LFRicHaloExchange(halo_field,
620 self.parent.children.insert(self.position,
630 required, _ = exchange.required(ignore_hex_dep=
True)
638 results = exchange.field.forward_write_dependencies()
640 first_dep_call = results[0].call
641 if isinstance(first_dep_call, HaloExchange):
645 next_results = results[0].forward_write_dependencies()
646 if next_results
and any(tmp
for tmp
in next_results
647 if isinstance(tmp.call,
650 f
"When replacing a halo exchange with another one "
651 f
"for field {exchange.field.name}, a subsequent "
652 f
"dependent halo exchange was found. This should "
654 first_dep_call.detach()
656 def _add_halo_exchange(self, halo_field):
657 '''Internal helper method to add (a) halo exchange call(s) immediately
658 before this loop using the halo_field argument for the
659 associated field information. If the field is a vector then
660 add the appropriate number of halo exchange calls.
662 :param halo_field: the argument requiring a halo exchange
663 :type halo_field: :py:class:`psyclone.dynamo0p3.DynArgument`
666 if halo_field.vector_size > 1:
670 for idx
in range(1, halo_field.vector_size+1):
676 '''add and/or remove halo exchanges due to changes in the loops
691 for call
in self.kernels():
692 for arg
in call.arguments.args:
693 if arg.access
in AccessType.all_write_accesses():
694 dep_arg_list = arg.forward_read_dependencies()
695 for dep_arg
in dep_arg_list:
696 if isinstance(dep_arg.call, LFRicHaloExchange):
699 halo_exchange = dep_arg.call
700 required, _ = halo_exchange.required()
702 halo_exchange.detach()
705 '''Add halo exchanges before this loop as required by fields within
706 this loop. To keep the logic simple we assume that any field
707 that accesses the halo will require a halo exchange and then
708 remove the halo exchange if this is not the case (when
709 previous writers perform sufficient redundant computation). It
710 is implemented this way as the halo exchange class determines
711 whether it is required or not so a halo exchange needs to
712 exist in order to find out. The appropriate logic is coded in
713 the _add_halo_exchange helper method. In some cases a new halo
714 exchange will replace an existing one. In this situation that
715 routine also removes the old one.
722 prev_arg_list = halo_field.backward_write_dependencies()
723 if not prev_arg_list:
730 if len(prev_arg_list) > 1:
733 if halo_field.vector_size <= 1:
735 f
"Error in create_halo_exchanges. Expecting field "
736 f
"'{halo_field.name}' to be a vector as it has "
737 f
"multiple previous dependencies")
738 if len(prev_arg_list) != halo_field.vector_size:
740 f
"Error in create_halo_exchanges. Expecting a "
741 f
"dependence for each vector index for field "
742 f
"'{halo_field.name}' but the number of "
743 f
"dependencies is '{halo_field.vector_size}' and "
744 f
"the vector size is '{len(prev_arg_list)}'.")
745 for arg
in prev_arg_list:
749 if not isinstance(arg.call, LFRicHaloExchange):
751 "Error in create_halo_exchanges. Expecting "
752 "all dependent nodes to be halo exchanges")
753 prev_node = prev_arg_list[0].call
757 if not isinstance(prev_node, LFRicHaloExchange):
767 :returns: the PSyIR for the lower bound of this loop.
768 :rtype: :py:class:`psyclone.psyir.Node`
771 inv_sched = self.ancestor(Routine)
772 sym_table = inv_sched.symbol_table
773 loops = inv_sched.loops()
775 for index, loop
in enumerate(loops):
779 root_name = f
"loop{posn}_start"
780 lbound = sym_table.find_or_create_integer_symbol(root_name,
782 self.children[0] = Reference(lbound)
783 return self.children[0]
788 :returns: the PSyIR for the upper bound of this loop.
789 :rtype: :py:class:`psyclone.psyir.Node`
792 inv_sched = self.ancestor(Routine)
793 sym_table = inv_sched.symbol_table
798 parent_loop = self.ancestor(Loop)
799 colour_var = parent_loop.variable
814 if self.kernels()[0].is_intergrid:
815 root_name += f
"_{self._field_name}"
816 depth_sym = sym_table.lookup_with_tag(
817 f
"max_halo_depth_{root_name}")
818 halo_depth = Reference(depth_sym)
820 return ArrayReference.create(asym, [Reference(colour_var),
822 return ArrayReference.create(asym, [Reference(colour_var)])
826 loops = inv_sched.loops()
828 for index, loop
in enumerate(loops):
832 root_name = f
"loop{posn}_stop"
833 ubound = sym_table.find_or_create_integer_symbol(root_name,
835 self.children[1] = Reference(ubound)
836 return self.children[1]
839 ''' Call the base class to generate the code and then add any
840 required halo exchanges.
842 :param parent: an f2pygen object that will be the parent of \
843 f2pygen objects created in this method.
844 :type parent: :py:class:`psyclone.f2pygen.BaseGen`
853 "OpenMP parallel region.")
861 if not (Config.get().distributed_memory
and
870 if self.ancestor((ACCRegionDirective, OMPRegionDirective)):
880 prev_node_name =
"loop"
882 prev_node_name =
"kernel"
883 parent.add(
CommentGen(parent, f
" Set halos dirty/clean for fields "
884 f
"modified in the above {prev_node_name}"))
893 Generates the necessary code to mark halo regions for all modified
894 fields as clean or dirty following execution of this loop.
896 :param parent: the node in the f2pygen AST to which to add content.
897 :type parent: :py:class:`psyclone.f2pygen.BaseGen`
903 sym_table = self.ancestor(InvokeSchedule).symbol_table
913 hwa = HaloWriteAccess(field, sym_table)
914 if not hwa.max_depth
or hwa.dirty_outer:
916 if field.vector_size > 1:
919 for index
in range(1, field.vector_size+1):
920 parent.add(
CallGen(parent, name=field.proxy_name +
921 f
"({index})%set_dirty()"))
923 parent.add(
CallGen(parent, name=field.proxy_name +
927 if hwa.literal_depth:
929 halo_depth = hwa.literal_depth
933 if field.vector_size > 1:
936 for index
in range(1, field.vector_size+1):
938 parent, name=f
"{field.proxy_name}({index})%"
939 f
"set_clean({halo_depth})"))
942 parent, name=f
"{field.proxy_name}%set_clean("
947 halo_depth = sym_table.lookup_with_tag(
948 "max_halo_depth_mesh").name
954 if field.vector_size > 1:
957 for index
in range(1, field.vector_size+1):
959 name=f
"{field.proxy_name}({index})%"
960 f
"set_clean({halo_depth})")
963 call =
CallGen(parent, name=f
"{field.proxy_name}%"
964 f
"set_clean({halo_depth})")
968 test_all_variables=False,
969 signatures_to_ignore=None,
972 This function is an LFRic-specific override of the default method
973 in the Loop class. It allows domain-specific rules to be applied when
974 determining whether or not loop iterations are independent.
976 :param bool test_all_variables: if True, it will test if all variable
977 accesses are independent, otherwise it will stop after the first
978 variable access is found that isn't.
979 :param signatures_to_ignore: list of signatures for which to skip
981 :type signatures_to_ignore: Optional[
982 List[:py:class:`psyclone.core.Signature`]]
983 :param dep_tools: an optional instance of DependencyTools so that the
984 caller can access any diagnostic messages detailing why the loop
985 iterations are not independent.
986 :type dep_tools: Optional[
987 :py:class:`psyclone.psyir.tools.DependencyTools`]
989 :returns: True if the loop iterations are independent, False otherwise.
996 dtools = DependencyTools()
1008 stat = dtools.can_loop_be_parallelised(
1009 self, test_all_variables=test_all_variables,
1010 signatures_to_ignore=signatures_to_ignore)
1013 except (InternalError, KeyError):
1034 dtools._add_message(
1035 f
"Builtin '{self.kernel.name}' performs a reduction",
1036 DTCode.WARN_SCALAR_REDUCTION)
1048 dtools._add_message(
1049 f
"Kernel '{self.kernel.name}' performs an INC update",
1050 DTCode.ERROR_WRITE_WRITE_RACE)
1054 raise InternalError(f
"independent_iterations: loop of type "
1055 f
"'{self.loop_type}' is not supported.")
1061 __all__ = [
'LFRicLoop']
def loop_type(self, value)
def unique_modified_args(self, arg_type)
def field_space(self, my_field_space)
def unique_fields_with_halo_reads(self)
def gen_mark_halos_clean_dirty(self, parent)
def gen_mark_halos_clean_dirty(self, parent)
def lower_to_language_level(self)
def _add_halo_exchange(self, halo_field)
def set_upper_bound(self, name, index=None)
def _add_field_component_halo_exchange(self, halo_field, idx=None)
def node_str(self, colour=True)
def gen_code(self, parent)
def upper_bound_halo_depth(self)
def update_halo_exchanges(self)
def upper_bound_name(self)
def create_halo_exchanges(self)
def set_lower_bound(self, name, index=None)
def independent_iterations(self, test_all_variables=False, signatures_to_ignore=None, dep_tools=None)