# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2021-2025, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
# A. B. G. Chalk, STFC Daresbury Lab
# I. Kavcic, Met Office
# C.M. Maynard, Met Office / University of Reading
# J. Henrichs, Bureau of Meteorology
# J. Remy, Université Grenoble Alpes, Inria
# M. Naylor, University of Cambridge, UK
# -----------------------------------------------------------------------------
''' This module contains the implementation of the various OpenMP Directive
nodes.'''
import abc
import itertools
import sympy
import logging
from typing import List
from psyclone.configuration import Config
from psyclone.core import AccessType
from psyclone.errors import (GenerationError,
UnresolvedDependencyError)
from psyclone.psyir.nodes.array_mixin import ArrayMixin
from psyclone.psyir.nodes.array_reference import ArrayReference
from psyclone.psyir.nodes.assignment import Assignment
from psyclone.psyir.nodes.atomic_mixin import (
AtomicDirectiveMixin,
AtomicDirectiveType,
)
from psyclone.psyir.nodes.call import Call
from psyclone.psyir.nodes.data_sharing_attribute_mixin import (
DataSharingAttributeMixin,
)
from psyclone.psyir.nodes.directive import StandaloneDirective, \
RegionDirective
from psyclone.psyir.nodes.intrinsic_call import IntrinsicCall
from psyclone.psyir.nodes.literal import Literal
from psyclone.psyir.nodes.loop import Loop
from psyclone.psyir.nodes.node import Node
from psyclone.psyir.nodes.operation import BinaryOperation
from psyclone.psyir.nodes.omp_clauses import OMPGrainsizeClause, \
OMPNowaitClause, OMPNogroupClause, OMPNumTasksClause, OMPPrivateClause, \
OMPDefaultClause, OMPReductionClause, OMPScheduleClause, \
OMPFirstprivateClause, OMPDependClause
from psyclone.psyir.nodes.ranges import Range
from psyclone.psyir.nodes.reference import Reference
from psyclone.psyir.nodes.routine import Routine
from psyclone.psyir.nodes.schedule import Schedule
from psyclone.psyir.nodes.structure_reference import StructureReference
from psyclone.psyir.symbols import (
INTEGER_TYPE, DataSymbol, ImportInterface, ContainerSymbol,
RoutineSymbol)
# OMP_OPERATOR_MAPPING is used to determine the operator to use in the
# reduction clause of an OpenMP directive.
OMP_OPERATOR_MAPPING = {AccessType.SUM: "+"}
[docs]
class OMPDirective(metaclass=abc.ABCMeta):
'''
Base mixin class for all OpenMP-related directives.
This class is useful to provide a unique common ancestor to all the
OpenMP directives, for instance when traversing the tree with
`node.walk(OMPDirective)`
Note that classes inheriting from it must place the OMPDirective in
front of the other Directive node sub-class, so that the Python
MRO gives preference to this class's attributes.
'''
_PREFIX = "OMP"
[docs]
class OMPRegionDirective(OMPDirective, RegionDirective, metaclass=abc.ABCMeta):
'''
Base class for all OpenMP region-related directives.
'''
def _get_reductions_list(self, reduction_type):
'''
Returns the names of all scalars within this region that require a
reduction of type 'reduction_type'. Returned names will be unique.
TODO #514 - this only works for the PSyKAl APIs currently. It needs
extending/replacing with the use of the PSyIR Dependence Analysis.
:param reduction_type: the reduction type (e.g. AccessType.SUM) to
search for.
:type reduction_type: :py:class:`psyclone.core.access_type.AccessType`
:returns: names of scalar arguments with reduction access.
:rtype: list[str]
'''
result = []
# TODO #514: not yet working with generic PSyIR, so skip for now
if Config.get().api not in ('gocean', 'lfric'):
return result
const = Config.get().api_conf().get_constants()
for call in self.kernels():
for arg in call.arguments.args:
if call.reprod_reduction:
# In this case we do the reduction serially instead of
# using an OpenMP clause
continue
if arg.argument_type in const.VALID_SCALAR_NAMES:
if arg.descriptor.access == reduction_type:
if arg.name not in result:
result.append(arg.name)
return result
[docs]
class OMPStandaloneDirective(OMPDirective, StandaloneDirective,
metaclass=abc.ABCMeta):
''' Base class for all OpenMP-related standalone directives. '''
[docs]
class OMPDeclareTargetDirective(OMPStandaloneDirective):
'''
Class representing an OpenMP Declare Target directive in the PSyIR.
'''
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp routine". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
return "omp declare target"
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this directive is not the first statement
in a routine.
'''
if self.parent and (not isinstance(self.parent, Routine) or
self.parent.children[0] is not self):
raise GenerationError(
f"A OMPDeclareTargetDirective must be the first child (index "
f"0) of a Routine but found one as child {self.position} of a "
f"{type(self.parent).__name__}.")
super().validate_global_constraints()
[docs]
class OMPTaskwaitDirective(OMPStandaloneDirective):
'''
Class representing an OpenMP TASKWAIT directive in the PSyIR.
'''
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp taskwait". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
'''
return "omp taskwait"
[docs]
class OMPBarrierDirective(OMPStandaloneDirective):
'''
Class representing an OpenMP BARRIER directive in the PSyIR.
'''
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this OMPBarrier is not enclosed
within some OpenMP parallel region.
'''
# It is only at the point of code generation that we can check for
# correctness (given that we don't mandate the order that a user
# can apply transformations to the code). As a Parallel Child
# directive, we must have an OMPParallelDirective as an ancestor
# somewhere back up the tree.
if not self.ancestor(OMPParallelDirective,
excluding=OMPParallelDoDirective):
raise GenerationError(
"OMPBarrierDirective must be inside an OMP parallel region "
"but could not find an ancestor OMPParallelDirective node")
super().validate_global_constraints()
[docs]
def begin_string(self) -> str:
'''Returns the beginning statement of this directive, i.e.
"omp barrier". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
'''
return "omp barrier"
[docs]
class OMPSerialDirective(OMPRegionDirective, metaclass=abc.ABCMeta):
'''
Abstract class representing OpenMP serial regions, e.g.
OpenMP SINGLE or OpenMP Master.
'''
def _valid_dependence_literals(self, lit1, lit2):
'''
Compares two Nodes to check whether they are a valid dependency
pair. For two Nodes where at least one is a Literal, a valid
dependency is any pair of Literals.
:param lit1: the first node to compare.
:type lit1: :py:class:`psyclone.psyir.nodes.Node`
:param lit2: the second node to compare.
:type lit2: :py:class:`psyclone.psyir.nodes.Node`
:returns: whether or not these two nodes can be used as a valid
dependency pair in OpenMP.
:rtype: bool
'''
# Check both are Literals
# If a Literal index into an array that is a dependency
# has calculated dependency to a
# non-Literal array index, this will return False, as this is not
# currently supported in PSyclone.
# If literals are not the same its fine, since a(1) is not a
# dependency to a(2), so as long as both are Literals this is ok.
return isinstance(lit1, Literal) and isinstance(lit2, Literal)
def _valid_dependence_ranges(self, arraymixin1, arraymixin2, index):
'''
Compares two ArrayMixin Nodes to check whether they are a valid
dependency pair on the provided index. For two Nodes where at least
one has a Range at this index, they must both have Ranges, and both be
full ranges, i.e. ":".
:param arraymixin1: the first node to validate.
:type arraymixin1: :py:class:`psyclone.psyir.nodes.ArrayMixin`
:param arraymixin2: the second node to validate.
:type arraymixin2: :py:class:`psyclone.psyir.nodes.ArrayMixin`
:returns: whether or not these two nodes can be used as a valid
dependency pair in OpenMP, based upon the provided index.
:rtype: bool
'''
# We know both inputs are always ArrayMixin as this is a private
# function.
# Check both are Ranges
if (not isinstance(arraymixin1.indices[index], Range) or not
isinstance(arraymixin2.indices[index], Range)):
# Range index to a dependency has calculated dependency to a
# non-Range index, which is not currently supported in PSyclone
return False
# To be valid, both ranges need to be full ranges.
# If we have a range index between dependencies which does not cover
# the full array range, it is not currently supported in
# PSyclone (due to OpenMP limitations), so False will be returned.
return (arraymixin1.is_full_range(index) and
arraymixin2.is_full_range(index))
def _compute_accesses_get_start_stop_step(self, preceding_nodes, task,
symbol):
'''
Computes the start, stop and step values used in the _compute_accesses
function by searching through the preceding nodes for the last access
to the symbol.
:param preceding_nodes: a list of nodes that precede the task in the
tree.
:type preceding_nodes: List[:py:class:`psyclone.psyir.nodes.Node`]
:param task: the OMPTaskDirective node being used in
_compute_accesses.
:type task: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:param symbol: the symbol used by the ref in _compute_accesses.
:type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
:returns: a tuple containing the start, stop and step nodes (or None
if there is no value).
:rtype: Tuple[:py:class`psyclone.psyir.nodes.Node, None]
'''
start = None
stop = None
step = None
for node in preceding_nodes:
# Only Assignment, Loop or Call nodes can modify the symbol in our
# Reference
if not isinstance(node, (Assignment, Loop, Call)):
continue
# At the moment we allow all IntrinsicCall nodes through, and
# assume that all IntrinsicCall nodes we find don't modify
# symbols, but only read from them.
if isinstance(node, Call) and not isinstance(node, IntrinsicCall):
# Currently opting to fail on any non-intrinsic Call.
# Potentially it might be possible to check if the Symbol is
# written to and only if so then raise an error
raise UnresolvedDependencyError(
"Found a Call in preceding_nodes, which "
"is not yet supported.")
if isinstance(node, Assignment) and node.lhs.symbol == symbol:
start = node.rhs.copy()
break
if isinstance(node, Loop) and node.variable == symbol:
# If the loop is not an ancestor of the task then
# we don't currently support it.
ancestor_loop = task.ancestor(Loop, limit=self)
is_ancestor = False
while ancestor_loop is not None:
if ancestor_loop == node:
is_ancestor = True
break
ancestor_loop = ancestor_loop.ancestor(Loop, limit=self)
if not is_ancestor:
raise UnresolvedDependencyError(
f"Found a dependency index that "
f"was updated as a Loop variable "
f"that is not an ancestor Loop of "
f"the task. The variable is "
f"'{node.variable.name}'.")
# It has to be an ancestor loop, so we want to find the start,
# stop and step Nodes
start, stop, step = node.start_expr, node.stop_expr, \
node.step_expr
break
return (start, stop, step)
def _compute_accesses(self, ref, preceding_nodes, task):
'''
Computes the set of accesses for a Reference or BinaryOperation
Node, based upon the preceding_nodes and containing task.
The returned result is either a set of Literals, or Literals
and BinaryOperations.
If ref is a BinaryOperation, it needs to be of the following formats:
1. Reference ADD/SUB Literal
2. Literal ADD Reference
3. Binop(Literal MUL Literal) ADD Reference
4. Reference ADD/SUB Binop(Literal MUL Literal)
:param ref: the Reference or BinaryOperation node to compute
accesses for.
:type ref: Union[:py:class:`psyclone.psyir.nodes.Reference,
:py:class:`psyclone.psyir.nodes.BinaryOperation]
:param preceding_nodes: a list of nodes that precede the task in the
tree.
:type preceding_nodes: List[:py:class:`psyclone.psyir.nodes.Node`]
:param task: the OMPTaskDirective node containing ref as a child.
:type task: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:raises UnresolvedDependencyError: If the ref contains an unsupported
BinaryOperation structure, such as
a non-ADD/SUB/MUL operator. Check
error message for more details.
:raises UnresolvedDependencyError: If preceding_nodes contains a Call
node.
:raises UnresolvedDependencyError: If ref is a BinaryOperation and
neither child of ref is a Literal
or BinaryOperation.
:raises UnresolvedDependencyError: If there is a dependency between
ref (a BinaryOperation) and a
previously set constant.
:raises UnresolvedDependencyError: If there is a dependency between
ref and a Loop variable that is
not an ancestor of task.
:raises UnresolvedDependencyError: If preceding_nodes contains a
dependent loop with a non-Literal
step.
:returns: a list of the dependency values for the input ref, or a
dict of the start, stop and step values.
:rtype: List[Union[:py:class:`psyclone.psyir.nodes.Literal`,
:py:class:`psyclone.psyir.nodes.BinaryOperation`]] or
Dict[str: py:class:`psyclone.psyir.nodes.Node`]
'''
if isinstance(ref, Reference):
symbol = ref.symbol
else:
# Get the symbol out of the Binop, and store some other
# important information. We store the step value of the
# ancestor loop (which will be the value of the Literal, or
# one of the Literals if an operand is a BinaryOperation).
# In the case that one of the operands is a BinaryOperation,
# we also store a "num_entries" value, which is based upon the
# multiplier of the step value. This is how we can handle
# cases such as array(i+57) if the step value is 32, the
# dependencies stored would be array(i+32) and array(i+64).
if isinstance(ref.children[0], Literal):
if ref.operator == BinaryOperation.Operator.ADD:
# Have Literal + Reference. Store the symbol of the
# Reference and the integer value of the Literal.
symbol = ref.children[1].symbol
binop_val = int(ref.children[0].value)
num_entries = 2
else:
raise UnresolvedDependencyError(
f"Found a dependency index that is "
f"a BinaryOperation where the "
f"format is Literal OP Reference "
f"with a non-ADD operand "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
elif isinstance(ref.children[1], Literal):
# Have Reference OP Literal. Store the symbol of the
# Reference, and the integer value of the Literal. If the
# operator is negative, then we store the value negated.
if ref.operator in [BinaryOperation.Operator.ADD,
BinaryOperation.Operator.SUB]:
symbol = ref.children[0].symbol
binop_val = int(ref.children[1].value)
num_entries = 2
if ref.operator == BinaryOperation.Operator.SUB:
binop_val = -binop_val
else:
raise UnresolvedDependencyError(
f"Found a dependency index that is "
f"a BinaryOperation where the "
f"Operator is neither ADD not SUB "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
elif isinstance(ref.children[0], BinaryOperation):
if ref.operator == BinaryOperation.Operator.ADD:
# Have Binop ADD Reference. Store the symbol of the
# Reference, and store the binop. The binop is of
# structure Literal MUL Literal, where the second
# Literal is to the step of a parent loop.
symbol = ref.children[1].symbol
binop = ref.children[0]
if binop.operator != BinaryOperation.Operator.MUL:
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"BinaryOperation with a child "
f"BinaryOperation with a non-MUL operator "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
# These binary operations are format of Literal MUL Literal
# where step_val is the 2nd literal and the multiplier
# is the first literal
if (not (isinstance(binop.children[0], Literal) and
isinstance(binop.children[1], Literal))):
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"BinaryOperation with a child "
f"BinaryOperation with a non-Literal child "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
# We store the step of the parent loop in binop_val, and
# use the other operand to compute how many entries we
# need to compute to validate this dependency list.
binop_val = int(binop.children[1].value)
num_entries = int(binop.children[0].value)+1
else:
raise UnresolvedDependencyError(
f"Found a dependency index that is "
f"a BinaryOperation where the "
f"format is BinaryOperator OP "
f"Reference with a non-ADD operand "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
elif isinstance(ref.children[1], BinaryOperation):
# Have Reference ADD/SUB Binop. Store the symbol of the
# Reference, and store the binop. The binop is of
# structure Literal MUL Literal, where the second
# Literal is to the step of a parent loop.
if ref.operator in [BinaryOperation.Operator.ADD,
BinaryOperation.Operator.SUB]:
symbol = ref.children[0].symbol
binop = ref.children[1]
if binop.operator != BinaryOperation.Operator.MUL:
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"BinaryOperation with a child "
f"BinaryOperation with a non-MUL operator "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
# These binary operations are format of Literal MUL Literal
# where step_val is the 2nd literal.
if (not (isinstance(binop.children[0], Literal) and
isinstance(binop.children[1], Literal))):
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"BinaryOperation with an operand "
f"BinaryOperation with a non-Literal operand "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
# We store the step of the parent loop in binop_val, and
# use the other operand to compute how many entries we
# need to compute to validate this dependency list.
binop_val = int(binop.children[1].value)
num_entries = int(binop.children[0].value)+1
if ref.operator == BinaryOperation.Operator.SUB:
# If the operator is SUB then we use 1 less
# entry in the list, as Fortran arrays start
# from 1.
binop_val = -binop_val
num_entries = num_entries-1
else:
raise UnresolvedDependencyError(
f"Found a dependency index that is "
f"a BinaryOperation where the "
f"format is Reference OP "
f"BinaryOperation with a non-ADD, "
f"non-SUB operand "
f"which is not supported. "
f"The operation found was "
f"'{ref.debug_string()}'.")
else:
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"BinaryOperation where neither child "
f"is a Literal or BinaryOperation. "
f"PSyclone can't validate "
f"this dependency. "
f"The operation found was "
f"'{ref.debug_string()}'.")
start, stop, step = self._compute_accesses_get_start_stop_step(
preceding_nodes, task, symbol)
if isinstance(ref, BinaryOperation):
output_list = []
if step is None:
# Found no ancestor loop, PSyclone cannot handle
# this case, as BinaryOperations created by OMPTaskDirective
# in dependencies will always be based on ancestor loops.
raise UnresolvedDependencyError(
f"Found a dependency between a "
f"BinaryOperation and a previously "
f"set constant value. "
f"PSyclone cannot yet handle this "
f"interaction. The error occurs from "
f"'{ref.debug_string()}'.")
# If the step isn't a Literal value, then we can't compute what
# the address accesses at compile time, so we can't validate the
# dependency.
if not isinstance(step, Literal):
raise UnresolvedDependencyError(
f"Found a dependency index that is a "
f"Loop variable with a non-Literal step "
f"which we can't resolve in PSyclone. "
f"Containing node is '{ref.debug_string()}'.")
# If the start and stop are both Literals, we can compute a set
# of accesses this BinaryOperation is related to precisely.
if (isinstance(start, Literal) and isinstance(stop, Literal)):
# Fill the output list with all values from start to stop
# incremented by step
startval = int(start.value)
stopval = int(stop.value)
stepval = int(step.value)
# We loop from startval to stopval + 1 as PSyIR loops will
# include stopval, wheras Python loops do not.
for i in range(startval, stopval + 1, stepval):
new_x = i + binop_val
output_list.append(Literal(f"{new_x}", INTEGER_TYPE))
return output_list
# If they are not all literals, we have a special case. In this
# case we return a dict containing start, stop and step and this
# is compared directly to the start, stop and step of a
# corresponding access.
output_list = {}
output_list["start"] = BinaryOperation.create(
BinaryOperation.Operator.ADD,
start.copy(),
Literal(f"{binop_val}", INTEGER_TYPE)
)
output_list["stop"] = stop.copy()
output_list["step"] = step.copy()
return output_list
if step is None:
# Result for an assignment.
output_list = [start]
return output_list
output_list = []
# If step is not a Literal then we probably can't resolve this
if not isinstance(step, Literal):
raise UnresolvedDependencyError(
"Found a dependency index that is a "
"Loop variable with a non-Literal step "
"which we can't resolve in PSyclone.")
# Special case when all are Literals
if (isinstance(start, Literal) and isinstance(stop, Literal)):
# Fill the output list with all values from start to stop
# incremented by step
startval = int(start.value)
stopval = int(stop.value)
stepval = int(step.value)
# We loop from startval to stopval + 1 as PSyIR loops will include
# stopval, wheras Python loops do not.
for i in range(startval, stopval + 1, stepval):
output_list.append(Literal(f"{i}", INTEGER_TYPE))
return output_list
# the sequence only. In this case, we have a non-parent loop reference
# which is also firstprivate (as shared indices are forbidden in
# OMPTaskDirective already), so is essentially a constant. In this
# case therefore we will have an unknown start and stop value, so we
# verify this dependency differently. To ensure this special case is
# understood as a special case, we return a dict with the 3 members.
output_list = {}
output_list["start"] = start.copy()
output_list["stop"] = stop.copy()
output_list["step"] = step.copy()
return output_list
def _check_valid_overlap(self, sympy_ref1s, sympy_ref2s):
'''
Takes two lists of SymPy expressions, and checks that any overlaps
between the expressions is valid for OpenMP depend clauses.
:param sympy_ref1s: the list of SymPy expressions corresponding to
the first dependency clause.
:type sympy_ref1s: List[:py:class:`sympy.core.basic.Basic`]
:param sympy_ref2s: the list of SymPy expressions corresponding to
the second dependency clause.
:type sympy_ref2s: List[:py:class:`sympy.core.basic.Basic`]
:returns: whether this is a valid overlap according to the OpenMP
standard.
:rtype: bool
'''
# r1_min will contain the minimum computed value for (ref + value)
# from the list. r1_max will contain the maximum computed value.
# Loop through the values in sympy_ref1s, and compute the maximum
# and minumum values in that list. These correspond to the maximum and
# minimum values used for accessing the array relative to the
# symbol used as a base access.
values = [int(member) for member in sympy_ref1s]
r1_min = min(values)
r1_max = max(values)
# Loop over the elements in sympy_ref2s and check that the dependency
# is valid in OpenMP.
for member in sympy_ref2s:
# If the value is between min and max of r1 then we check that
# the value is in the values list
val = int(member)
if r1_min <= val <= r1_max:
if val not in values:
# Found incompatible dependency between two
# array accesses, ref1 is in range r1_min
# to r1_max, but doesn't contain val.
# This can happen if we have two loops with
# different start values or steps.
return False
return True
def _valid_dependence_ref_binop(self, ref1, ref2, task1, task2):
'''
Compares two Reference/BinaryOperation Nodes to check they are a set
of dependencies that are valid according to OpenMP. Both these nodes
are array indices on the same array symbol, so for OpenMP to correctly
compute this dependency, we must guarantee at compile time that we
know the addresses/array sections covered by this index are identical.
:param ref1: the first Node to compare.
:type ref1: Union[:py:class:`psyclone.psyir.nodes.Reference`,
:py:class:`psyclone.psyir.nodes.BinaryOperation`]
:param ref2: the second Node to compare.
:type ref2: Union[:py:class:`psyclone.psyir.nodes.Reference`,
:py:class:`psyclone.psyir.nodes.BinaryOperation`]
:param task1: the task containing ref1 as a child.
:type task1: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:param task2: the task containing ref2 as a child.
:type task2: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:raises GenerationError: If ref1 and ref2 are dependencies on the
same array, and one does not contain a
Reference but the other does.
:raises GenerationError: If ref1 and ref2 are dependencies on the
same array but are References to different
variables.
:raises GenerationError: If ref1 and ref2 are dependencies on the
same array, but the computed index values
are not dependent according to OpenMP.
:returns: whether or not these two nodes can be used as a valid
dependency on the same array in OpenMP.
:rtype: bool
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.backend.sympy_writer import SymPyWriter
# In this case we have two Reference/BinaryOperation as indices.
# We need to attempt to find their value set and check the value
# set matches.
# Find all the nodes before these tasks
preceding_t1 = task1.preceding(reverse=True)
preceding_t2 = task2.preceding(reverse=True)
# Get access list for each ref
try:
ref1_accesses = self._compute_accesses(ref1, preceding_t1, task1)
ref2_accesses = self._compute_accesses(ref2, preceding_t2, task2)
except UnresolvedDependencyError:
# If we get a UnresolvedDependencyError from compute_accesses, then
# we found an access that isn't able to be handled by PSyclone, so
# dependencies based on it need to be handled by a taskwait
return False
# Create our sympy_writer
sympy_writer = SymPyWriter()
# If either of the returned accesses are a dict, this is a special
# case where both must be a dict and have the same start, stop and
# step.
if isinstance(ref1_accesses, dict) or isinstance(ref2_accesses, dict):
# If they aren't both dicts then we need to return False as
# the special case isn't handled correctly.
if type(ref1_accesses) is not type(ref2_accesses):
return False
# If they're both dicts then we need the step to be equal for
# this dependency to be satisfiable.
if ref1_accesses["step"] != ref2_accesses["step"]:
return False
# Now we know the step is equal, we need the start values to be
# start1 = start2 + x * step, where x is an integer value.
# We use SymPy to solve this equation and perform this check.
sympy_start1 = sympy_writer(ref1_accesses["start"])
sympy_start2 = sympy_writer(ref2_accesses["start"])
sympy_step = sympy_writer(ref2_accesses["step"])
b_sym = sympy.Symbol('b')
result = sympy.solvers.solve(sympy_start1 - sympy_start2 +
b_sym * sympy_step, b_sym)
if not isinstance(result[0], sympy.core.numbers.Integer):
return False
# If we know the start and step are aligned, all possible
# dependencies are aligned so we don't need to check the stop
# value.
return True
# If we have a list, then we have a set of Literal values.
# We use the SymPyWriter to convert these objects to expressions
# we can use to obtain integer values for these Literals
sympy_ref1s = sympy_writer(ref1_accesses)
sympy_ref2s = sympy_writer(ref2_accesses)
return self._check_valid_overlap(sympy_ref1s, sympy_ref2s)
def _check_dependency_pairing_valid(self, node1, node2, task1, task2):
'''
Given a pair of nodes which are children of a OMPDependClause, this
function checks whether the described dependence is correctly
described by the OpenMP standard.
If the dependence is not going to be handled safely, this function
returns False, else it returns true.
:param node1: the first input node to check.
:type node1: :py:class:`psyclone.psyir.nodes.Reference`
:param node2: the second input node to check.
:type node2: :py:class:`psyclone.psyir.nodes.Reference`
:param task1: the OMPTaskDirective node containing node1 as a
dependency
:type task1: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:param task2: the OMPTaskDirective node containing node2 as a
dependency
:type task2: :py:class:`psyclone.psyir.nodes.OMPTaskDirective`
:returns: whether the dependence is going to be handled safely
according to the OpenMP standard.
:rtype: bool
'''
# Checking the symbol is the same works. If the symbol is not the same
# then there's no dependence, so its valid.
if node1.symbol != node2.symbol:
return True
# The typing check handles any edge case where we have node1 and node2
# pointing to the same symbol, but one is a specialised reference type
# and the other is a base Reference type - this is unlikely to happen
# but we check just in case. In this case we have to assume there is
# an unhandled dependency
if type(node1) is not type(node2):
return False
# For structure reference we need to check they access
# the same member. If they don't, no dependence so valid.
if isinstance(node1, StructureReference):
# If either is a StructureReference here they must both be,
# as they access the same symbol.
# We can't just do == on the Member child, as that
# will recurse and check the array indices for any
# ArrayMixin children
# Check the signature of both StructureReference
# to see if they are accessing the same data
ref0_sig = node1.get_signature_and_indices()[0]
ref1_sig = node2.get_signature_and_indices()[0]
if ref0_sig != ref1_sig:
return True
# If we have (exactly) Reference objects we filter out
# non-matching ones with the symbol check, and matching ones
# are always valid since they are simple accesses.
# pylint: disable=unidiomatic-typecheck
if type(node1) is Reference:
return True
# All remaining objects are some sort of Array access
array1 = None
array2 = None
# PSyclone will not handle dependencies on multiple array indexes
# at the moment, so we return False.
if len(node1.walk(ArrayMixin)) > 1 or len(node2.walk(ArrayMixin)) > 1:
return False
if isinstance(node1, ArrayReference):
array1 = node1
array2 = node2
else:
array1 = node1.walk(ArrayMixin)[0]
array2 = node2.walk(ArrayMixin)[0]
for i, index in enumerate(array1.indices):
if (isinstance(index, Literal) or
isinstance(array2.indices[i], Literal)):
valid = self._valid_dependence_literals(
index, array2.indices[i])
elif (isinstance(index, Range) or
isinstance(array2.indices[i], Range)):
valid = self._valid_dependence_ranges(
array1, array2, i)
else:
# The only remaining option is that the indices are
# References or BinaryOperations
valid = self._valid_dependence_ref_binop(
index, array2.indices[i], task1, task2)
# If this was not valid then return False, else keep checking
# other indices
if not valid:
return False
return valid
def _validate_task_dependencies(self):
'''
Validates all task dependencies in this OMPSerialDirective region are
valid within the restraints of OpenMP & PSyclone. This is done through
a variety of helper functions, and checks each pair of tasks' inout,
outin and outout combinations.
Any task dependencies that are detected and will not be handled by
OpenMP's depend clause will be handled through the addition of
OMPTaskwaitDirective nodes.
:raises NotImplementedError: If this region contains both an
OMPTaskDirective and an
OMPTaskloopDirective.
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.omp_task_directive import OMPTaskDirective
tasks = self.walk(OMPTaskDirective)
# For now we disallow Tasks and Taskloop directives in the same Serial
# Region
if len(tasks) > 0 and any(self.walk(OMPTaskloopDirective)):
raise NotImplementedError("OMPTaskDirectives and "
"OMPTaskloopDirectives are not "
"currently supported inside the same "
"parent serial region.")
pairs = itertools.combinations(tasks, 2)
# List of tuples of dependent nodes that aren't handled by OpenMP
unhandled_dependent_nodes = []
# Lowest and highest position nodes contain the abs_position of each
# tuple inside unhandled_dependent_nodes, used for sorting the arrays
# and checking if the unhandled dependency has a taskwait inbetween.
lowest_position_nodes = []
highest_position_nodes = []
for pair in pairs:
task1 = pair[0]
task2 = pair[1]
# Find all References in each tasks' depend clauses
# Should we cache these instead?
task1_in = [x for x in task1.input_depend_clause.children
if isinstance(x, Reference)]
task1_out = [x for x in task1.output_depend_clause.children
if isinstance(x, Reference)]
task2_in = [x for x in task2.input_depend_clause.children
if isinstance(x, Reference)]
task2_out = [x for x in task2.output_depend_clause.children
if isinstance(x, Reference)]
inout = list(itertools.product(task1_in, task2_out))
outin = list(itertools.product(task1_out, task2_in))
outout = list(itertools.product(task1_out, task2_out))
# Loop through each potential dependency pair and check they
# will be handled correctly.
# Need to predefine satisfiable in case all lists are empty.
satisfiable = True
for mem in inout + outin + outout:
satisfiable = \
self._check_dependency_pairing_valid(mem[0], mem[1],
task1, task2)
# As soon as any is not satisfiable, then we don't need to
# continue checking.
if not satisfiable:
break
# If we have an unsatisfiable dependency between two tasks, then we
# need to have a taskwait between them always. We need to loop up
# to find these tasks' parents which are closest to the Schedule
# which contains both tasks, and use them as the nodes which are
# dependent.
if not satisfiable:
# Find the lowest schedule containing both nodes.
schedule1 = task1.ancestor(Schedule, shared_with=task2)
# Find the closest ancestor to the common schedule.
task1_proxy = task1
while task1_proxy.parent is not schedule1:
task1_proxy = task1_proxy.parent
task2_proxy = task2
while task2_proxy.parent is not schedule1:
task2_proxy = task2_proxy.parent
# Now we have the closest nodes to the closest common ancestor
# schedule, so add them to the unhandled_dependent_nodes list.
if task1_proxy is not task2_proxy:
# If they end up with the same proxy, they have the same
# ancestor tree but are in different schedules. This means
# that they are in something like an if/else block with
# one node in an if block and the other in the else block.
# These dependencies we can ignore as they are not ever
# both executed
unhandled_dependent_nodes.append(
(task1_proxy, task2_proxy))
lowest_position_nodes.append(min(task1_proxy.abs_position,
task2_proxy.abs_position))
highest_position_nodes.append(
max(task1_proxy.abs_position,
task2_proxy.abs_position))
# If we have no invalid dependencies we can return early
if len(unhandled_dependent_nodes) == 0:
return
# Need to sort lists by highest_position_nodes value, and then
# by lowest value if tied.
# Based upon
# https://stackoverflow.com/questions/9764298/how-to-sort-two-
# lists-which-reference-each-other-in-the-exact-same-way
# sorted_highest_positions and sorted_lowest_positions contain
# the abs_positions for the corresponding Nodes in the tuple at
# the same index in sorted_dependency_pairs. The
# sorted_dependency_pairs list contains each pair of unhandled
# dependency nodes that were previously computed, but sorted
# according to abs_position in the tree.
sorted_highest_positions, sorted_lowest_positions, \
sorted_dependency_pairs = (list(t) for t in
zip(*sorted(zip(
highest_position_nodes,
lowest_position_nodes,
unhandled_dependent_nodes)
)))
# The location of any node where need to place an OMPTaskwaitDirective
# to ensure code correctness. The size of this list should be
# minimised during construction as we will not add another
# OMPTaskwaitDirective when a dependency will be handled already by
# an existing OMPTaskwaitDirective or one that will be created during
# this process.
taskwait_location_nodes = []
# Stores the abs_position for each of the OMPTaskwaitDirective nodes
# that does or will exist.
taskwait_location_abs_pos = []
for taskwait in self.walk(OMPTaskwaitDirective):
taskwait_location_nodes.append(taskwait)
taskwait_location_abs_pos.append(taskwait.abs_position)
# Add the first node to have a taskwait placed in front of it into the
# list, unless one of the existing OMPTaskwaitDirective nodes already
# satisfies the dependency.
lo_abs_pos = sorted_lowest_positions[0]
hi_abs_pos = sorted_highest_positions[0]
for ind, taskwait_loc in enumerate(taskwait_location_nodes):
if (taskwait_location_abs_pos[ind] <= hi_abs_pos and
taskwait_location_abs_pos[ind] >= lo_abs_pos):
# We potentially already satisfy this initial dependency
if (sorted_dependency_pairs[0][1].ancestor(Schedule) is
taskwait_loc.ancestor(Schedule)):
break
else:
taskwait_location_nodes.append(sorted_dependency_pairs[0][1])
taskwait_location_abs_pos.append(sorted_highest_positions[0])
for index, pairs in enumerate(sorted_dependency_pairs[1:]):
# Add 1 to index here because we're looking from [1:]
lo_abs_pos = sorted_lowest_positions[index+1]
hi_abs_pos = sorted_highest_positions[index+1]
for ind, taskwait_loc in enumerate(taskwait_location_nodes):
if (taskwait_location_abs_pos[ind] <= hi_abs_pos and
taskwait_location_abs_pos[ind] >= lo_abs_pos):
# We have a taskwait meant to be placed here that is
# potentially already satisfied. To check we need to
# ensure that the ancestor schedules of the nodes
# are identical
if (pairs[0].ancestor(Schedule) is
taskwait_loc.ancestor(Schedule)):
break
else:
# If we didn't find a taskwait we plan to add that satisfies
# this dependency, add it to the list
taskwait_location_nodes.append(pairs[1])
taskwait_location_abs_pos.append(hi_abs_pos)
# Now loop through the list in reverse and add taskwaits unless the
# node is already a taskwait
taskwait_location_nodes.reverse()
for taskwait_loc in taskwait_location_nodes:
if isinstance(taskwait_loc, OMPTaskwaitDirective):
continue
node_parent = taskwait_loc.parent
loc = taskwait_loc.position
node_parent.addchild(OMPTaskwaitDirective(), loc)
[docs]
def lower_to_language_level(self):
'''
Checks that any task dependencies inside this node are valid.
'''
# Perform parent ops
super().lower_to_language_level()
# Validate any task dependencies in this OMPSerialRegion.
self._validate_task_dependencies()
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this OMPSerial is not enclosed
within some OpenMP parallel region.
:raises GenerationError: if this OMPSerial is enclosed within
any OMPSerialDirective subclass region.
'''
# It is only at the point of code generation that we can check for
# correctness (given that we don't mandate the order that a user
# can apply transformations to the code). As a Parallel Child
# directive, we must have an OMPParallelDirective as an ancestor
# somewhere back up the tree.
# Also check the single region is not enclosed within another OpenMP
# single region.
# It could in principle be allowed for that parent to be a ParallelDo
# directive, however I can't think of a use case that would be done
# best in a parallel code by that pattern
if not self.ancestor(OMPParallelDirective,
excluding=OMPParallelDoDirective):
raise GenerationError(
f"{self._text_name} must be inside an OMP parallel region but "
f"could not find an ancestor OMPParallelDirective node")
if self.ancestor(OMPSerialDirective):
raise GenerationError(
f"{self._text_name} must not be inside another OpenMP "
f"serial region")
super().validate_global_constraints()
[docs]
class OMPSingleDirective(OMPSerialDirective):
'''
Class representing an OpenMP SINGLE directive in the PSyIR.
:param nowait: argument describing whether this single should have
a nowait clause applied. Default value is False.
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
'''
_children_valid_format = "Schedule, [OMPNowaitClause]"
# Textual description of the node
_text_name = "OMPSingleDirective"
def __init__(self, nowait: bool = False, **kwargs):
self._nowait = nowait
# Call the init method of the base class once we've stored
# the nowait requirement
super().__init__(**kwargs)
if self._nowait:
self.children.append(OMPNowaitClause())
@staticmethod
def _validate_child(position, child):
'''
Decides whether a given child and position are valid for this node.
The rules are:
1. Child 0 must always be a Schedule.
2. Child 1 can only be a OMPNowaitClause.
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0:
return isinstance(child, Schedule)
if position == 1:
return isinstance(child, OMPNowaitClause)
return False
@property
def nowait(self):
'''
:returns: whether the nowait clause is specified for this directive.
:rtype: bool
'''
return self._nowait
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp single". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
return "omp single"
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end single". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end single"
[docs]
class OMPMasterDirective(OMPSerialDirective):
'''
Class representing an OpenMP MASTER directive in the PSyclone AST.
'''
# Textual description of the node
_text_name = "OMPMasterDirective"
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp master". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
return "omp master"
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end master". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end master"
[docs]
class OMPParallelDirective(OMPRegionDirective, DataSharingAttributeMixin):
''' Class representing an OpenMP Parallel directive.
'''
_children_valid_format = ("Schedule, OMPDefaultClause, OMPPrivateClause, "
"OMPFirstprivate, [OMPReductionClause]*")
[docs]
@classmethod
def create(cls, children=None):
'''
Create an OMPParallelDirective.
:param children: The child nodes of the new directive.
:type children: List of :py:class:`psyclone.psyir.nodes.Node`
:returns: A new OMPParallelDirective.
:rtype: :py:class:`psyclone.psyir.nodes.OMPParallelDirective`
'''
instance = cls(children=children)
# An OMPParallelDirective must have 4 children.
# Child 0 is a Schedule, created in the constructor.
# The create function adds the other three mandatory children:
# OMPDefaultClause, OMPPrivateClause and OMPFirstprivateClause
instance.addchild(OMPDefaultClause(clause_type=OMPDefaultClause.
DefaultClauseTypes.SHARED))
instance.addchild(OMPPrivateClause())
instance.addchild(OMPFirstprivateClause())
return instance
@staticmethod
def _validate_child(position, child):
'''
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0 and isinstance(child, Schedule):
return True
if position == 1 and isinstance(child, OMPDefaultClause):
return True
if position == 2 and isinstance(child, OMPPrivateClause):
return True
if position == 3 and isinstance(child, OMPFirstprivateClause):
return True
if position >= 4 and isinstance(child, OMPReductionClause):
return True
return False
@property
def default_clause(self):
'''
:returns: The OMPDefaultClause associated with this Directive.
:rtype: :py:class:`psyclone.psyir.nodes.OMPDefaultClause`
'''
return self.children[1]
@property
def private_clause(self):
'''
:returns: The current OMPPrivateClause associated with this Directive.
:rtype: :py:class:`psyclone.psyir.nodes.OMPPrivateClause`
'''
return self.children[2]
[docs]
def lower_to_language_level(self):
'''
In-place construction of clauses as PSyIR constructs.
At the higher level these clauses rely on dynamic variable dependence
logic to decide what is private and what is shared, so we use this
lowering step to find out which References are private, and place them
explicitly in the lower-level tree to be processed by the backend
visitor.
:returns: the lowered version of this node.
:rtype: :py:class:`psyclone.psyir.node.Node`
:raises GenerationError: if the OpenMP directive needs some
synchronisation mechanism to create valid
code. These are not implemented yet.
'''
# first check whether we have more than one reduction with the same
# name in this Schedule. If so, raise an error as this is not
# supported for a parallel region.
names = []
reduction_kernels = self.reductions()
for call in reduction_kernels:
name = call.reduction_arg.name
if name in names:
raise GenerationError(
f"Reduction variables can only be used once in an invoke. "
f"'{name}' is used multiple times, please use a different "
f"reduction variable")
names.append(name)
if reduction_kernels:
first_type = type(self.dir_body[0])
for child in self.dir_body.children:
if first_type != type(child):
raise GenerationError(
"Cannot correctly generate code for an OpenMP parallel"
" region with reductions and containing children of "
"different types.")
# pylint: disable=import-outside-toplevel
from psyclone.psyGen import zero_reduction_variables
zero_reduction_variables(reduction_kernels)
# Reproducible reduction will be done serially by accumulating the
# partial results in an array indexed by the thread index
reprod_red_call_list = self.reductions(reprod=True)
if reprod_red_call_list:
# Use a private thread index variable
omp_lib = self.scope.symbol_table.find_or_create(
"omp_lib", symbol_type=ContainerSymbol)
omp_get_thread_num = self.scope.symbol_table.find_or_create(
"omp_get_thread_num", symbol_type=RoutineSymbol,
interface=ImportInterface(omp_lib))
thread_idx = self.scope.symbol_table.find_or_create_tag(
"omp_thread_index", root_name="th_idx",
symbol_type=DataSymbol, datatype=INTEGER_TYPE)
assignment = Assignment.create(
lhs=Reference(thread_idx),
rhs=BinaryOperation.create(
BinaryOperation.Operator.ADD,
Call.create(omp_get_thread_num),
Literal("1", INTEGER_TYPE))
)
self.dir_body.addchild(assignment, 0)
# Now finish the reproducible reductions
if reprod_red_call_list:
for call in reversed(reprod_red_call_list):
call.reduction_sum_loop()
# Lower the first two children
for child in self.children[:2]:
child.lower_to_language_level()
# Create data sharing clauses (order alphabetically to make generation
# reproducible)
private, fprivate, need_sync = self.infer_sharing_attributes()
if reprod_red_call_list:
private.add(thread_idx)
private_clause = OMPPrivateClause.create(
sorted(private, key=lambda x: x.name))
fprivate_clause = OMPFirstprivateClause.create(
sorted(fprivate, key=lambda x: x.name))
# Check all of the need_sync nodes are synchronized in children.
# unless it has reduction_kernels which are handled separately
sync_clauses = self.walk(OMPDependClause)
if not reduction_kernels and need_sync:
for sym in need_sync:
for clause in sync_clauses:
# Needs to be an out depend clause to synchronize
if clause.operator == "in":
continue
# Check if the symbol is in this depend clause.
if sym.name in [child.symbol.name for child in
clause.children]:
break
else:
logger = logging.getLogger(__name__)
logger.warning(
"Lowering '%s' detected a possible race condition for "
"symbol '%s'. Make sure this is a false WaW dependency"
" or the code includes the necessary "
"synchronisations.", type(self).__name__, sym.name)
self.children[2].replace_with(private_clause)
self.children[3].replace_with(fprivate_clause)
return self
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp parallel". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
result = "omp parallel"
# TODO #514: not yet working with NEMO, so commented out for now
# if not self._reprod:
# result += self._reduction_string()
return result
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end parallel". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end parallel"
[docs]
def infer_sharing_attributes(self):
'''
The PSyIR does not specify if each symbol inside an OpenMP region is
private, firstprivate, shared or shared but needs synchronisation,
the attributes are inferred looking at the usage of each symbol inside
the parallel region.
This method analyses the directive body and automatically classifies
each symbol using the following rules:
- All arrays are shared unless listed in the explicitly private list.
- Scalars that are accessed only once are shared.
- Scalars that are read-only or written outside a loop are shared.
- Scalars written in multiple iterations of a loop are private, unless:
* there is a write-after-read dependency in a loop iteration,
in this case they are shared but need synchronisation;
* they are read before in the same parallel region (but not inside
the same loop iteration), in this case they are firstprivate.
* they are only conditionally written in some iterations;
in this case they are firstprivate.
This method returns the sets of private, firstprivate, and shared but
needing synchronisation symbols, all symbols not in these sets are
assumed shared. How to synchronise the symbols in the third set is
up to the caller of this method.
:returns: three set of symbols that classify each of the symbols in
the directive body as PRIVATE, FIRSTPRIVATE or SHARED NEEDING
SYNCHRONISATION.
:rtype: Tuple[Set(:py:class:`psyclone.psyir.symbols.Symbol`),
Set(:py:class:`psyclone.psyir.symbols.Symbol`),
Set(:py:class:`psyclone.psyir.symbols.Symbol`)]
:raises GenerationError: if the DefaultClauseType associated with
this OMPParallelDirective is not shared.
'''
if (self.default_clause.clause_type !=
OMPDefaultClause.DefaultClauseTypes.SHARED):
raise GenerationError("OMPParallelClause cannot correctly generate"
" the private clause when its default "
"data sharing attribute in its default "
"clause is not 'shared'.")
return super().infer_sharing_attributes()
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this OMPDoDirective is not enclosed
within some OpenMP parallel region.
'''
if self.ancestor(OMPParallelDirective) is not None:
raise GenerationError("Cannot nest OpenMP parallel regions.")
self._encloses_omp_directive()
def _encloses_omp_directive(self):
''' Check that this Parallel region contains other OpenMP
directives. While it doesn't have to (in order to be valid
OpenMP), it is likely that an absence of directives
is an error on the part of the user. '''
# We need to recurse down through all our children and check
# whether any of them are an OMPRegionDirective.
node_list = self.walk(OMPRegionDirective)
if not node_list:
# TODO raise a warning here so that the user can decide
# whether or not this is OK.
pass
# raise GenerationError("OpenMP parallel region does not enclose "
# "any OpenMP directives. This is probably "
# "not what you want.")
[docs]
class OMPTaskloopDirective(OMPRegionDirective):
'''
Class representing an OpenMP TASKLOOP directive in the PSyIR.
:param grainsize: The grainsize value used to specify the grainsize
clause on this OpenMP directive. If this is None
the grainsize clause is not applied. Default
value is None.
:type grainsize: int or None.
:param num_tasks: The num_tasks value used to specify the num_tasks
clause on this OpenMP directive. If this is None
the num_tasks clause is not applied. Default value
is None.
:type num_tasks: int or None.
:param nogroup: Whether the nogroup clause should be used for this node.
Default value is False
:type nogroup: bool
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
:raises GenerationError: if this OMPTaskloopDirective has both
a grainsize and num_tasks value
specified.
'''
# This specification respects the mutual exclusion of OMPGransizeClause
# and OMPNumTasksClause, but adds an additional ordering requirement.
# Other specifications to soften the ordering requirement are possible,
# but need additional checks in the global constraints instead.
_children_valid_format = ("Schedule, [OMPGrainsizeClause | "
"OMPNumTasksClause], [OMPNogroupClause]")
def __init__(self, grainsize=None, num_tasks=None, nogroup=False,
**kwargs):
# These remain primarily for the gen_code interface
self._grainsize = grainsize
self._num_tasks = num_tasks
self._nogroup = nogroup
if self._grainsize is not None and self._num_tasks is not None:
raise GenerationError(
"OMPTaskloopDirective must not have both grainsize and "
"numtasks clauses specified.")
super().__init__(**kwargs)
if self._grainsize is not None:
child = [Literal(f"{grainsize}", INTEGER_TYPE)]
self._children.append(OMPGrainsizeClause(children=child))
if self._num_tasks is not None:
child = [Literal(f"{num_tasks}", INTEGER_TYPE)]
self._children.append(OMPNumTasksClause(children=child))
if self._nogroup:
self._children.append(OMPNogroupClause())
@staticmethod
def _validate_child(position, child):
'''
Decides whether a given child and position are valid for this node.
The rules are:
1. Child 0 must always be a Schedule.
2. Child 1 may be either a OMPGrainsizeClause or OMPNumTasksClause,
or if neither of those clauses are present, it may be a
OMPNogroupClause.
3. Child 2 must always be a OMPNogroupClause, and can only exist if
child 1 is a OMPGrainsizeClause or OMPNumTasksClause.
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0:
return isinstance(child, Schedule)
if position == 1:
return isinstance(child, (OMPGrainsizeClause, OMPNumTasksClause,
OMPNogroupClause))
if position == 2:
return isinstance(child, OMPNogroupClause)
return False
@property
def nogroup(self):
'''
:returns: the nogroup clause status of this node.
:rtype: bool
'''
return self._nogroup
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this OMPTaskloopDirective is not
enclosed within an OpenMP serial region.
:raises GenerationError: if this OMPTaskloopDirective has two
Nogroup clauses as children.
'''
# It is only at the point of code generation that we can check for
# correctness (given that we don't mandate the order that a user
# can apply transformations to the code). A taskloop directive, we must
# have an OMPSerialDirective as an ancestor back up the tree.
if not self.ancestor(OMPSerialDirective):
raise GenerationError(
"OMPTaskloopDirective must be inside an OMP Serial region "
"but could not find an ancestor node")
# Check children are well formed.
# _validate_child will ensure position 0 and 1 are valid.
if len(self._children) == 3 and isinstance(self._children[1],
OMPNogroupClause):
raise GenerationError(
"OMPTaskloopDirective has two Nogroup clauses as children "
"which is not allowed.")
super().validate_global_constraints()
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp taskloop ...". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the beginning statement for this directive.
:rtype: str
'''
return "omp taskloop"
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end taskloop". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end taskloop"
[docs]
class OMPDoDirective(OMPRegionDirective, DataSharingAttributeMixin):
'''
Class representing an OpenMP DO directive in the PSyIR.
:param Optional[str] omp_schedule: the OpenMP schedule to use. Defaults
to "none" which means it is
implementation dependent.
:param Optional[int] collapse: optional number of nested loops to
collapse into a single iteration space to
parallelise. Defaults to None.
:param Optional[bool] reprod: whether or not to generate code for
run-reproducible OpenMP reductions (if not
specified the value is provided by the
PSyclone Config file).
:param nowait: whether or not to add a nowait clause onto this directive.
Default is False.
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
'''
_directive_string = "do"
_children_valid_format = ("Schedule, [OMPReductionClause]*")
def __init__(self, omp_schedule: str = "none",
collapse: int = None, reprod: bool = None,
nowait: bool = False,
**kwargs):
super().__init__(**kwargs)
if reprod is None:
self._reprod = Config.get().reproducible_reductions
else:
self._reprod = reprod
self._omp_schedule = omp_schedule
self._collapse = None
self.collapse = collapse # Use setter with error checking
# TODO #514 - reductions are only implemented in LFRic, for now we
# store the needed clause when lowering, but this needs a better
# solution
self._lowered_reduction_string = ""
self.nowait = nowait
@staticmethod
def _validate_child(position, child):
'''
Decides whether a given child and position are valid for this node.
Currently, the children are a Schedule followed by zero or more
OMPReductionClause.
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0:
return isinstance(child, Schedule)
elif position >= 1:
return isinstance(child, OMPReductionClause)
return False
def __eq__(self, other):
'''
Checks whether two nodes are equal. Two OMPDoDirective nodes are equal
if they have the same schedule, the same reproducible reduction option
(and the inherited equality is True), and they have the same reduction
clauses.
:param object other: the object to check equality to.
:returns: whether other is equal to self.
:rtype: bool
'''
is_eq = super().__eq__(other)
is_eq = is_eq and self.omp_schedule == other.omp_schedule
is_eq = is_eq and self.reprod == other.reprod
is_eq = is_eq and self.collapse == other.collapse
return is_eq
@property
def nowait(self) -> bool:
'''
:returns: whether this directive has a nowait clause.
'''
return self._nowait
@nowait.setter
def nowait(self, value: bool):
'''
Sets whether this directive should have a nowait clause attached.
:param value: whether this directive should have a nowait clause
attached.
:raises TypeError: if value is not a bool.
'''
if not isinstance(value, bool):
raise TypeError(
f"The {type(self).__name__} nowait clause must be a bool, "
f"but value '{value}' has been given."
)
self._nowait = value
@property
def collapse(self):
'''
:returns: the value of the collapse clause.
:rtype: int or NoneType
'''
return self._collapse
@collapse.setter
def collapse(self, value):
'''
:param value: optional number of nested loop to collapse into a
single iteration space to parallelise. Defaults to None.
:type value: int or NoneType.
:raises TypeError: if the collapse value given is not an integer
or NoneType.
:raises ValueError: if the collapse integer given is not positive.
'''
if value is not None and not isinstance(value, int):
raise TypeError(
f"The {type(self).__name__} collapse clause must be a positive"
f" integer or None, but value '{value}' has been given.")
if value is not None and value <= 0:
raise ValueError(
f"The {type(self).__name__} collapse clause must be a positive"
f" integer or None, but value '{value}' has been given.")
self._collapse = value
[docs]
def node_str(self, colour=True):
'''
Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
parts = []
if self.omp_schedule != "none":
parts.append(f"omp_schedule={self.omp_schedule}")
if self.reductions():
parts.append(f"reprod={self._reprod}")
if self._collapse and self._collapse > 1:
parts.append(f"collapse={self._collapse}")
return f"{self.coloured_name(colour)}[{','.join(parts)}]"
def _reduction_string(self):
'''
:returns: the OMP reduction information.
:rtype: str
'''
for reduction_type in AccessType.get_valid_reduction_modes():
reductions = self._get_reductions_list(reduction_type)
parts = []
for reduction in reductions:
parts.append(f"reduction("
f"{OMP_OPERATOR_MAPPING[reduction_type]}:"
f"{reduction})")
return ", ".join(parts)
@property
def omp_schedule(self):
'''
:returns: the omp_schedule for this object.
:rtype: str
'''
return self._omp_schedule
@omp_schedule.setter
def omp_schedule(self, value):
'''
:param str value: the omp_schedule for this object.
:raises TypeError: if the provided omp_schedule is not a valid
schedule string.
'''
if not isinstance(value, str):
raise TypeError(
f"{type(self).__name__} omp_schedule should be a str "
f"but found '{type(value).__name__}'.")
if (value.split(',')[0].lower() not in
OMPScheduleClause.VALID_OMP_SCHEDULES):
raise TypeError(
f"{type(self).__name__} omp_schedule should be one of "
f"{OMPScheduleClause.VALID_OMP_SCHEDULES} but found "
f"'{value}'.")
self._omp_schedule = value
@property
def reprod(self):
'''
:returns: whether reprod has been set for this object or not.
'''
return self._reprod
@reprod.setter
def reprod(self, value):
'''
:param bool value: enable or disable reproducible loop parallelism.
'''
self._reprod = value
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
:raises GenerationError: if this OMPDoDirective is not enclosed
within some OpenMP parallel region.
'''
# It is only at the point of code generation that we can check for
# correctness (given that we don't mandate the order that a user
# can apply transformations to the code). As a loop
# directive, we must have an OMPParallelDirective as an ancestor
# somewhere back up the tree.
if not self.ancestor(OMPParallelDirective,
excluding=OMPParallelDoDirective):
raise GenerationError(
"OMPDoDirective must be inside an OMP parallel region but "
"could not find an ancestor OMPParallelDirective node")
self._validate_single_loop()
self._validate_collapse_value()
super().validate_global_constraints()
def _validate_collapse_value(self):
'''
Checks that if there is a collapse clause, there must be as many
immediately nested loops as the collapse value.
:raises GenerationError: if this OMPLoopDirective has a collapse
clause but it doesn't have the expected
number of nested Loops.
'''
if self._collapse:
cursor = self.dir_body
for depth in range(self._collapse):
if (len(cursor.children) != 1 or
not isinstance(cursor.children[0], Loop)):
raise GenerationError(
f"{type(self).__name__} must have as many immediately "
f"nested loops as the collapse clause specifies but "
f"'{self}' has a collapse={self._collapse} and the "
f"nested body at depth {depth} cannot be "
f"collapsed.")
cursor = cursor.children[0].loop_body
def _validate_single_loop(self):
'''
Checks that this directive is only applied to a single Loop node.
:raises GenerationError: if this directive has more than one child.
:raises GenerationError: if the child of this directive is not a Loop.
'''
if len(self.dir_body.children) != 1:
raise GenerationError(
f"An {type(self).__name__} can only be applied to a single "
f"loop but this Node has {len(self.dir_body.children)} "
f"children: {self.dir_body.children}")
if not isinstance(self.dir_body[0], Loop):
raise GenerationError(
f"An {type(self).__name__} can only be applied to a loop but "
f"this Node has a child of type "
f"'{type(self.dir_body[0]).__name__}'")
[docs]
def lower_to_language_level(self):
'''
In-place construction of clauses as PSyIR constructs.
The clauses here may need to be updated if code has changed, or be
added if not yet present.
:returns: the lowered version of this node.
:rtype: :py:class:`psyclone.psyir.node.Node`
'''
self._lowered_reduction_string = self._reduction_string()
return super().lower_to_language_level()
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp do ...". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the beginning statement for this directive.
:rtype: str
'''
string = f"omp {self._directive_string}"
if self.omp_schedule != "none":
string += f" schedule({self.omp_schedule})"
if self._collapse:
string += f" collapse({self._collapse})"
if self._lowered_reduction_string:
string += f" {self._lowered_reduction_string}"
return string
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end do". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
string = f"omp end {self._directive_string}"
if self.nowait:
string += " nowait"
return string
[docs]
class OMPParallelDoDirective(OMPParallelDirective, OMPDoDirective):
''' Class for the !$OMP PARALLEL DO directive. This inherits from
both OMPParallelDirective (because it creates a new OpenMP
thread-parallel region) and OMPDoDirective (because it
causes a loop to be parallelised).
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
'''
_children_valid_format = ("Schedule, OMPDefaultClause, OMPPrivateClause, "
"OMPFirstprivateClause, OMPScheduleClause, "
"[OMPReductionClause]*")
_directive_string = "parallel do"
def __init__(self, **kwargs):
OMPDoDirective.__init__(self, **kwargs)
self.addchild(OMPDefaultClause(
clause_type=OMPDefaultClause.DefaultClauseTypes.SHARED))
self.addchild(OMPPrivateClause())
self.addchild(OMPFirstprivateClause())
self.addchild(OMPScheduleClause())
[docs]
@classmethod
def create(cls, children=None, **kwargs):
'''
Create an OMPParallelDoDirective.
:param children: The child nodes of the new directive.
:type children: List of :py:class:`psyclone.psyir.nodes.Node`
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
:returns: A new OMPParallelDoDirective.
:rtype: :py:class:`psyclone.psyir.nodes.OMPParallelDoDirective`
'''
return cls(children=children, **kwargs)
@staticmethod
def _validate_child(position, child):
'''
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0 and isinstance(child, Schedule):
return True
if position == 1 and isinstance(child, OMPDefaultClause):
return True
if position == 2 and isinstance(child, OMPPrivateClause):
return True
if position == 3 and isinstance(child, OMPFirstprivateClause):
return True
if position == 4 and isinstance(child, OMPScheduleClause):
return True
if position >= 5 and isinstance(child, OMPReductionClause):
return True
return False
[docs]
def lower_to_language_level(self):
'''
In-place construction of clauses as PSyIR constructs.
The clauses here may need to be updated if code has changed, or be
added if not yet present.
:returns: the lowered version of this node.
:rtype: :py:class:`psyclone.psyir.node.Node`
'''
# Calling the super() explicitly to avoid confusion
# with the multiple-inheritance
self._lowered_reduction_string = self._reduction_string()
OMPParallelDirective.lower_to_language_level(self)
self.children[4].replace_with(OMPScheduleClause(self._omp_schedule))
return self
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp parallel do ...". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the beginning statement for this directive.
:rtype: str
'''
string = f"omp {self._directive_string}"
if self._collapse:
string += f" collapse({self._collapse})"
if self._lowered_reduction_string:
string += f" {self._lowered_reduction_string}"
return string
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end parallel do". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return f"omp end {self._directive_string}"
[docs]
def validate_global_constraints(self):
'''
Perform validation checks that can only be done at code-generation
time.
'''
OMPParallelDirective.validate_global_constraints(self)
self._validate_single_loop()
self._validate_collapse_value()
[docs]
class OMPTeamsDistributeParallelDoDirective(OMPParallelDoDirective):
''' Class representing the OMP teams distribute parallel do directive. '''
_directive_string = "teams distribute parallel do"
[docs]
class OMPTeamsLoopDirective(OMPParallelDoDirective):
''' Class representing the OMP teams loop directive.
'''
_directive_string = "teams loop"
[docs]
class OMPTargetDirective(OMPRegionDirective, DataSharingAttributeMixin):
''' Class for the !$OMP TARGET directive that offloads the code contained
in its region into an accelerator device.
:param nowait: whether or not to add a nowait clause onto this directive.
Default is False.
'''
def __init__(self, nowait: bool = False,
**kwargs):
super().__init__(**kwargs)
self.nowait = nowait
@property
def nowait(self) -> bool:
'''
:returns: whether this directive has a nowait clause.
'''
return self._nowait
@nowait.setter
def nowait(self, value: bool):
'''
Sets whether this directive should have a nowait clause attached.
:param value: whether this directive should have a nowait clause
attached.
:raises TypeError: if value is not a bool.
'''
if not isinstance(value, bool):
raise TypeError(
f"The {type(self).__name__} nowait clause must be a bool, "
f"but value '{value}' has been given."
)
self._nowait = value
[docs]
def begin_string(self):
'''Returns the beginning statement of this directive, i.e.
"omp target". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
string = "omp target"
if self.nowait:
string += " nowait"
return string
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end target". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end target"
[docs]
class OMPLoopDirective(OMPRegionDirective):
''' Class for the !$OMP LOOP directive that specifies that the iterations
of the associated loops may execute concurrently.
:param Optional[int] collapse: optional number of nested loops to
collapse into a single iteration space to
parallelise. Defaults to None.
:param nowait: whether or not to add a nowait clause onto this directive.
Default is False.
:param kwargs: additional keyword arguments provided to the PSyIR node.
:type kwargs: unwrapped dict.
'''
_children_valid_format = ("Schedule, [OMPReductionClause]*")
def __init__(self, collapse=None, nowait: bool = False, **kwargs):
super().__init__(**kwargs)
self._collapse = None
self.collapse = collapse # Use setter with error checking
self.nowait = nowait
@staticmethod
def _validate_child(position, child):
'''
Decides whether a given child and position are valid for this node.
Currently, the children are zero or more OMPReductionClause.
:param int position: the position to be validated.
:param child: a child to be validated.
:type child: :py:class:`psyclone.psyir.nodes.Node`
:return: whether the given child and position are valid for this node.
:rtype: bool
'''
if position == 0:
return isinstance(child, Schedule)
elif position >= 1:
return isinstance(child, OMPReductionClause)
return False
def __eq__(self, other):
'''
Checks whether two nodes are equal. Two OMPLoopDirective nodes are
equal if they have the same collapse status and the inherited
equality is true, and they have the same reduction clauses.
:param object other: the object to check equality to.
:returns: whether other is equal to self.
:rtype: bool
'''
is_eq = super().__eq__(other)
is_eq = is_eq and self.collapse == other.collapse
return is_eq
@property
def nowait(self) -> bool:
'''
:returns: whether this directive has a nowait clause.
'''
return self._nowait
@nowait.setter
def nowait(self, value: bool):
'''
Sets whether this directive should have a nowait clause attached.
:param value: whether this directive should have a nowait clause
attached.
:raises TypeError: if value is not a bool.
'''
if not isinstance(value, bool):
raise TypeError(
f"The {type(self).__name__} nowait clause must be a bool, "
f"but value '{value}' has been given."
)
self._nowait = value
@property
def collapse(self):
'''
:returns: the value of the collapse clause.
:rtype: int or NoneType
'''
return self._collapse
@collapse.setter
def collapse(self, value):
'''
:param value: optional number of nested loop to collapse into a
single iteration space to parallelise. Defaults to None.
:type value: int or NoneType.
:raises TypeError: if the collapse value given is not an integer
or NoneType.
:raises ValueError: if the collapse integer given is not positive.
'''
if value is not None and not isinstance(value, int):
raise TypeError(
f"The OMPLoopDirective collapse clause must be a positive "
f"integer or None, but value '{value}' has been given.")
if value is not None and value <= 0:
raise ValueError(
f"The OMPLoopDirective collapse clause must be a positive "
f"integer or None, but value '{value}' has been given.")
self._collapse = value
[docs]
def node_str(self, colour=True):
''' Returns the name of this node with (optional) control codes
to generate coloured output in a terminal that supports it.
:param bool colour: whether or not to include colour control codes.
:returns: description of this node, possibly coloured.
:rtype: str
'''
text = self.coloured_name(colour)
if self._collapse:
text += f"[collapse={self._collapse}]"
else:
text += "[]"
return text
[docs]
def begin_string(self):
''' Returns the beginning statement of this directive, i.e. "omp loop".
The visitor is responsible for adding the correct directive beginning
(e.g. "!$").
:returns: the opening statement of this directive.
:rtype: str
'''
string = "omp loop"
if self._collapse:
string += f" collapse({self._collapse})"
if self.nowait:
string += " nowait"
return string
[docs]
def end_string(self):
'''Returns the end (or closing) statement of this directive, i.e.
"omp end loop". The visitor is responsible for adding the
correct directive beginning (e.g. "!$").
:returns: the end statement for this directive.
:rtype: str
'''
return "omp end loop"
[docs]
def validate_global_constraints(self):
''' Perform validation of those global constraints that can only be
done at code-generation time.
:raises GenerationError: if this OMPLoopDirective has more than one
child in its associated schedule.
:raises GenerationError: if the schedule associated with this
OMPLoopDirective does not contain a Loop.
:raises GenerationError: this directive must be inside a omp target
or parallel region.
:raises GenerationError: if this OMPLoopDirective has a collapse
clause but it doesn't have the expected
number of nested Loops.
'''
if len(self.dir_body.children) != 1:
raise GenerationError(
f"OMPLoopDirective must have exactly one child in its "
f"associated schedule but found {self.dir_body.children}.")
if not isinstance(self.dir_body.children[0], Loop):
raise GenerationError(
f"OMPLoopDirective must have a Loop as child of its associated"
f" schedule but found '{self.dir_body.children[0]}'.")
if not self.ancestor((OMPTargetDirective, OMPParallelDirective)):
# Also omp teams or omp threads regions but these are not supported
# in the PSyIR
raise GenerationError(
f"OMPLoopDirective must be inside a OMPTargetDirective or a "
f"OMPParallelDirective, but '{self}' is not.")
# If there is a collapse clause, there must be as many immediately
# nested loops as the collapse value
if self._collapse:
cursor = self.dir_body.children[0]
for depth in range(self._collapse):
if not isinstance(cursor, Loop):
raise GenerationError(
f"OMPLoopDirective must have as many immediately "
f"nested loops as the collapse clause specifies but "
f"'{self}' has a collapse={self._collapse} and the "
f"nested statement at depth {depth} is a "
f"{type(cursor).__name__} rather than a Loop.")
cursor = cursor.loop_body.children[0]
super().validate_global_constraints()
[docs]
class OMPAtomicDirective(OMPRegionDirective, AtomicDirectiveMixin):
'''
OpenMP directive to represent that the memory accesses in the associated
assignment must be performed atomically.
Note that the standard supports blocks with 2 assignments but this is
currently unsupported in the PSyIR.
:param ast: the entry in the fparser2 parse tree representing the code
contained within this directive or None.
:type ast: Optional[:py:class:`fparser.two.Fortran2003.Base`]
:param children: the nodes that will be children of this
Directive node or None.
:param parent: PSyIR node that is the parent of this Directive or None.
:param directive_type: the directive type of the atomic operation.
:type directive_type: :py:class:`psyclone.psyir.nodes.OMPAtomicDirective.\
AtomicDirectiveType`
'''
def __init__(self, ast=None, children: List[Node] = None,
parent: Node = None,
directive_type: AtomicDirectiveType = None):
# The default atomic directive in OpenMP is UPDATE
if not directive_type:
directive_type = AtomicDirectiveType.UPDATE
if not isinstance(directive_type,
AtomicDirectiveType):
raise TypeError(
f"OMPAtomicDirective expects an AtomicDirectiveType as the "
f"directive_type but found {directive_type}."
)
super().__init__(ast, children, parent)
# TODO #2398 - We can probably work out a directive_type for
# a lot of single statement OMPAtomicDirectives.
self._directive_type = directive_type
[docs]
def begin_string(self):
'''
:returns: the opening string statement of this directive.
:rtype: str
'''
directive_type_to_str = {
AtomicDirectiveType.UPDATE: "update",
AtomicDirectiveType.READ: "read",
AtomicDirectiveType.WRITE: "write",
AtomicDirectiveType.CAPTURE: "capture",
}
return f"omp atomic {directive_type_to_str[self._directive_type]}"
[docs]
def end_string(self):
'''
:returns: the ending string statement of this directive.
:rtype: str
'''
return "omp end atomic"
[docs]
def validate_global_constraints(self):
''' Perform validation of those global constraints that can only be
done at code-generation time.
:raises GenerationError: if the OMPAtomicDirective associated
statement does not conform to a valid OpenMP
atomic operation.
'''
if not self.children or len(self.dir_body.children) != 1:
raise GenerationError(
f"Atomic directives must always have one and only one"
f" associated statement, but found: '{self.debug_string()}'")
stmt = self.dir_body[0]
if not self.is_valid_atomic_statement(stmt):
raise GenerationError(
f"Statement '{self.children[0].debug_string()}' is not a "
f"valid OpenMP Atomic statement.")
[docs]
class OMPSimdDirective(OMPRegionDirective):
'''
OpenMP directive to inform that the associated loop can be vectorised.
'''
[docs]
def begin_string(self):
'''
:returns: the opening string statement of this directive.
:rtype: str
'''
return "omp simd"
[docs]
def end_string(self):
'''
:returns: the ending string statement of this directive.
:rtype: str
'''
return "omp end simd"
[docs]
def validate_global_constraints(self):
''' Perform validation of those global constraints that can only be
done at code-generation time.
:raises GenerationError: if the OMPSimdDirective does not contain
precisely one loop.
'''
if (not self.children or len(self.dir_body.children) != 1 or
not isinstance(self.dir_body[0], Loop)):
raise GenerationError(
f"The OMP SIMD directives must always have one and only one"
f" associated loop, but found: '{self.debug_string()}'")
# For automatic API documentation generation
__all__ = ["OMPRegionDirective", "OMPParallelDirective", "OMPSingleDirective",
"OMPMasterDirective", "OMPDoDirective", "OMPParallelDoDirective",
"OMPSerialDirective", "OMPTaskloopDirective", "OMPTargetDirective",
"OMPTaskwaitDirective", "OMPDirective", "OMPStandaloneDirective",
"OMPLoopDirective", "OMPDeclareTargetDirective",
"OMPAtomicDirective", "OMPSimdDirective", "OMPBarrierDirective"]