Reference Guide  2.5.0
psy_data_trans.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2019-2024, Science and Technology Facilities Council.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice,
14 # this list of conditions and the following disclaimer in the documentation
15 # and/or other materials provided with the distribution.
16 #
17 # * Neither the name of the copyright holder nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 # POSSIBILITY OF SUCH DAMAGE.
33 # -----------------------------------------------------------------------------
34 # Author: J. Henrichs, Bureau of Meteorology
35 # Modified: A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
36 
37 '''Contains the PSyData transformation.
38 '''
39 
40 from psyclone.configuration import Config
41 from psyclone.errors import InternalError
42 from psyclone.psyGen import InvokeSchedule, Kern
43 from psyclone.psyir.nodes import PSyDataNode, Schedule, Return, \
44  OMPDoDirective, ACCDirective, ACCLoopDirective, Routine
45 from psyclone.psyir.transformations.region_trans import RegionTrans
47  import TransformationError
48 
49 
51  ''' Create a PSyData region around a list of statements. For
52  example:
53 
54  >>> from psyclone.parse.algorithm import parse
55  >>> from psyclone.parse.utils import ParseError
56  >>> from psyclone.psyGen import PSyFactory
57  >>> api = "gocean1.0"
58  >>> ast, invoke_info = parse(SOURCE_FILE, api=api)
59  >>> psy = PSyFactory(api).create(invoke_info)
60  >>>
61  >>> from psyclone.psyir.transformations import PSyDataTrans
62  >>> data_trans = PSyDataTrans()
63  >>>
64  >>> schedule = psy.invokes.get('invoke_0').schedule
65  >>> # Uncomment the following line to see a text view of the schedule
66  >>> # print(schedule.view())
67  >>>
68  >>> # Enclose all children within a single PSyData region
69  >>> data_trans.apply(schedule.children)
70  >>> # Uncomment the following line to see a text view of the schedule
71  >>> # print(schedule.view())
72  >>> # Or to use custom region name:
73  >>> data_trans.apply(schedule.children,
74  ... {"region_name": ("module","region")})
75 
76  :param node_class: The Node class of which an instance will be inserted \
77  into the tree (defaults to PSyDataNode).
78  :type node_class: :py:class:`psyclone.psyir.nodes.ExtractNode`
79 
80  '''
81  # Unlike other transformations we can be fairly relaxed about the nodes
82  # that a region can contain as we don't have to understand them.
83  excluded_node_types = (Return,)
84 
85  # This dictionary keeps track of region+module names that are already
86  # used. For each key (which is module_name+"|"+region_name) it contains
87  # how many regions with that name have been created. This number will
88  # then be added as an index to create unique region identifiers.
89  _used_kernel_names = {}
90 
91  def __init__(self, node_class=PSyDataNode):
92  super().__init__()
93  self._node_class_node_class = node_class
94 
95  # ------------------------------------------------------------------------
96  def __str__(self):
97  return (f"Create a sub-tree of the PSyIR that has a node of type "
98  f"{self._node_class.__name__} at its root.")
99 
100  # ------------------------------------------------------------------------
101  @property
102  def name(self):
103  '''This function returns the name of the transformation.
104  It uses the Python 2/3 compatible way of returning the
105  class name as a string, which means that the same function can
106  be used for all derived classes.
107 
108  :returns: the name of this transformation as a string.
109  :rtype: str
110  '''
111 
112  return self.__class__.__name__
113 
114  # -------------------------------------------------------------------------
116  '''Returns a new dictionary with additional options, specific to the
117  transformation, that will be added to the user option. Any values
118  specified by the user will take precedence.
119 
120  :returns: a dictionary with additional options.
121  :rtype: Dict[str, Any]
122  '''
123 
124  return {}
125 
126  # -------------------------------------------------------------------------
127  def merge_in_default_options(self, options):
128  '''This function returns a new dictionary which contains the default
129  options for this transformation plus al user-specified options.
130  Any user-specified option will take precedence over the default
131  values.
132 
133  :param options: a dictionary with options for transformations.
134  :type options: Dict[str, Any]
135  :returns: a new dictionary which merges the default options with \
136  the user-specified options.
137  :rtype: Dict[str:Any]
138 
139  '''
140  new_options = self.get_default_optionsget_default_options()
141  if options:
142  # Update will overwrite any existing setting with the ones
143  # specified by the user:
144  new_options.update(options)
145  return new_options
146 
147  # ------------------------------------------------------------------------
148  def get_unique_region_name(self, nodes, options):
149  '''This function returns the region and module name. If they are
150  specified in the user options, these names will just be returned (it
151  is then up to the user to guarantee uniqueness). Otherwise a name
152  based on the module and invoke will be created using indices to
153  make sure the name is unique.
154 
155  :param nodes: a list of nodes.
156  :type nodes: list of :py:obj:`psyclone.psyir.nodes.Node`
157  :param options: a dictionary with options for transformations.
158  :type options: Dict[str, Any]
159  :param (str,str) options["region_name"]: an optional name to \
160  use for this PSyData area, provided as a 2-tuple containing a \
161  location name followed by a local name. The pair of strings \
162  should uniquely identify a region unless aggregate information \
163  is required (and is supported by the runtime library).
164 
165  '''
166  # We don't use a static method here since it might be useful to
167  # overwrite this functions in derived classes
168  name = options.get("region_name", None)
169  if name:
170  # pylint: disable=too-many-boolean-expressions
171  if not isinstance(name, tuple) or not len(name) == 2 or \
172  not name[0] or not isinstance(name[0], str) or \
173  not name[1] or not isinstance(name[1], str):
174  raise InternalError(
175  "Error in PSyDataTrans. The name must be a "
176  "tuple containing two non-empty strings.")
177  # pylint: enable=too-many-boolean-expressions
178  # Valid PSyData names have been provided by the user.
179  return name
180 
181  invoke = nodes[0].ancestor(InvokeSchedule).invoke
182  module_name = invoke.invokes.psy.name
183 
184  # Use the invoke name as a starting point.
185  region_name = invoke.name
186  kerns = []
187  for node in nodes:
188  kerns.extend(node.walk(Kern))
189 
190  if len(kerns) == 1:
191  # This PSyData region only has one kernel within it,
192  # so append the kernel name.
193  region_name += f":{kerns[0].name}"
194 
195  # Add a region index to ensure uniqueness when there are
196  # multiple regions in an invoke.
197  key = module_name + "|" + region_name
198  idx = PSyDataTrans._used_kernel_names.get(key, 0)
199  PSyDataTrans._used_kernel_names[key] = idx + 1
200  region_name += f":r{idx}"
201  return (module_name, region_name)
202 
203  # ------------------------------------------------------------------------
204  def validate(self, nodes, options=None):
205  '''
206  Checks that the supplied list of nodes is valid, that the location
207  for this node is valid (not between a loop-directive and its loop),
208  that there aren't any name clashes with symbols that must be
209  imported from the appropriate PSyData library and finally, calls the
210  validate method of the base class.
211 
212  :param nodes: a node or list of nodes to be instrumented with \
213  PSyData API calls.
214  :type nodes: (list of) :py:class:`psyclone.psyir.nodes.Loop`
215 
216  :param options: a dictionary with options for transformations.
217  :type options: Optional[Dict[str, Any]]
218  :param str options["prefix"]: a prefix to use for the PSyData module \
219  name (``PREFIX_psy_data_mod``) and the PSyDataType \
220  (``PREFIX_PSYDATATYPE``) - a "_" will be added automatically. \
221  It defaults to "".
222  :param (str,str) options["region_name"]: an optional name to \
223  use for this PSyData area, provided as a 2-tuple containing a \
224  location name followed by a local name. The pair of strings \
225  should uniquely identify a region unless aggregate information \
226  is required (and is supported by the runtime library).
227 
228  :raises TransformationError: if the supplied list of nodes is empty.
229  :raises TransformationError: if the PSyData node is inserted \
230  between an OpenMP/ACC directive and the loop(s) to which it \
231  applies.
232  :raises TransformationError: if the 'prefix' or 'region_name' options \
233  are not valid.
234  :raises TransformationError: if there will be a name clash between \
235  any existing symbols and those that must be imported from the \
236  appropriate PSyData library.
237 
238  '''
239  # pylint: disable=too-many-branches
240  node_list = self.get_node_listget_node_list(nodes)
241 
242  if not node_list:
243  raise TransformationError("Cannot apply transformation to an "
244  "empty list of nodes.")
245 
246  node_parent = node_list[0].parent
247  if isinstance(node_parent, Schedule) and \
248  isinstance(node_parent.parent, (OMPDoDirective, ACCLoopDirective)):
249  raise TransformationError("A PSyData node cannot be inserted "
250  "between an OpenMP/ACC directive and "
251  "the loop(s) to which it applies!")
252 
253  if node_list[0].ancestor(ACCDirective):
254  raise TransformationError("A PSyData node cannot be inserted "
255  "inside an OpenACC region.")
256 
257  my_options = self.merge_in_default_optionsmerge_in_default_options(options)
258  if "region_name" in my_options:
259  name = my_options["region_name"]
260  # pylint: disable=too-many-boolean-expressions
261  if not isinstance(name, tuple) or not len(name) == 2 or \
262  not name[0] or not isinstance(name[0], str) or \
263  not name[1] or not isinstance(name[1], str):
264  raise TransformationError(
265  f"Error in {self.name}. User-supplied region name "
266  f"must be a tuple containing two non-empty strings.")
267  # pylint: enable=too-many-boolean-expressions
268  prefix = my_options.get("prefix", None)
269  if "prefix" in my_options:
270  prefix = my_options.get("prefix", None)
271  if prefix not in Config.get().valid_psy_data_prefixes:
272  raise TransformationError(
273  f"Error in 'prefix' parameter: found '{prefix}', while"
274  f" one of {Config.get().valid_psy_data_prefixes} was "
275  f"expected as defined in {Config.get().filename}")
276 
277  # We have to create an instance of the node that will be inserted in
278  # order to find out what module name it will use.
279  pdata_node = self._node_class_node_class(options=my_options)
280  table = node_list[0].scope.symbol_table
281  for name in ([sym.name for sym in pdata_node.imported_symbols] +
282  [pdata_node.fortran_module]):
283  try:
284  _ = table.lookup_with_tag(name)
285  except KeyError as err:
286  # The tag doesn't exist which means that we haven't already
287  # added this symbol as part of a PSyData transformation. Check
288  # for any clashes with existing symbols.
289  try:
290  _ = table.lookup(name)
291  raise TransformationError(
292  f"Cannot add PSyData calls because there is already a "
293  f"symbol named '{name}' which clashes with one of "
294  f"those used by the PSyclone PSyData API. ") from err
295  except KeyError:
296  pass
297 
298  super().validate(node_list, my_options)
299 
300  # ------------------------------------------------------------------------
301  def apply(self, nodes, options=None):
302  # pylint: disable=arguments-renamed
303  '''Apply this transformation to a subset of the nodes within a
304  schedule - i.e. enclose the specified Nodes in the
305  schedule within a single PSyData region.
306 
307  :param nodes: can be a single node or a list of nodes.
308  :type nodes: :py:obj:`psyclone.psyir.nodes.Node` or list of \
309  :py:obj:`psyclone.psyir.nodes.Node`
310  :param options: a dictionary with options for transformations.
311  :type options: Optional[Dict[str, Any]]
312  :param str options["prefix"]: a prefix to use for the PSyData module \
313  name (``PREFIX_psy_data_mod``) and the PSyDataType \
314  (``PREFIX_PSYDATATYPE``) - a "_" will be added automatically. \
315  It defaults to "".
316  :param (str,str) options["region_name"]: an optional name to \
317  use for this PSyData area, provided as a 2-tuple containing a \
318  location name followed by a local name. The pair of strings \
319  should uniquely identify a region unless aggregate information \
320  is required (and is supported by the runtime library).
321 
322  '''
323  node_list = self.get_node_listget_node_list(nodes)
324 
325  # Add any transformation-specific settings that are required:
326  my_options = self.merge_in_default_optionsmerge_in_default_options(options)
327  # Perform validation checks
328  self.validatevalidatevalidatevalidate(node_list, my_options)
329 
330  # Get useful references
331  parent = node_list[0].parent
332  position = node_list[0].position
333 
334  # We always use the Routine symbol table
335  table = node_list[0].ancestor(Routine).symbol_table
336 
337  # Create an instance of the required class that implements
338  # the code extraction using the PSyData API, e.g. a
339  # ExtractNode. We pass the user-specified options to the
340  # create() method. An example use case for this is the
341  # 'create_driver' flag, where the calling program can control if
342  # a stand-alone driver program should be created or not (when
343  # performing kernel extraction).
344  for node in node_list:
345  node.detach()
346 
347  psy_data_node = self._node_class_node_class.create(
348  node_list, symbol_table=table, options=my_options)
349  parent.addchild(psy_data_node, position)
350 
351 
352 # =============================================================================
353 # For AutoAPI documentation generation
354 __all__ = ['PSyDataTrans']
def validate(self, node, options=None)
Definition: psyGen.py:2799