From 64d05069c1e79d676a4de6519dfc31634b5ae68a Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Wed, 22 Nov 2023 21:11:23 +0100 Subject: [PATCH 01/20] More support for math intrinsics in DaCe --- dace/frontend/fortran/ast_transforms.py | 12 +- dace/frontend/fortran/fortran_parser.py | 3 +- dace/frontend/fortran/intrinsics.py | 78 ++++++++--- tests/fortran/intrinsic_math.py | 164 ++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 tests/fortran/intrinsic_math.py diff --git a/dace/frontend/fortran/ast_transforms.py b/dace/frontend/fortran/ast_transforms.py index 0c96560fba..c77d17beb4 100644 --- a/dace/frontend/fortran/ast_transforms.py +++ b/dace/frontend/fortran/ast_transforms.py @@ -184,7 +184,7 @@ def __init__(self, funcs=None): from dace.frontend.fortran.intrinsics import FortranIntrinsics self.excepted_funcs = [ - "malloc", "exp", "pow", "sqrt", "cbrt", "max", "abs", "min", "__dace_sign", "tanh", + "malloc", "pow", "cbrt", "__dace_sign", "tanh", "atan2", "__dace_epsilon", *FortranIntrinsics.function_names() ] @@ -220,7 +220,7 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): from dace.frontend.fortran.intrinsics import FortranIntrinsics if not stop and node.name.name not in [ - "malloc", "exp", "pow", "sqrt", "cbrt", "max", "min", "abs", "tanh", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions() + "malloc", "pow", "cbrt", "atan2", "tanh", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions() ]: self.nodes.append(node) return self.generic_visit(node) @@ -241,7 +241,7 @@ def __init__(self, count=0): def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): from dace.frontend.fortran.intrinsics import FortranIntrinsics - if node.name.name in ["malloc", "exp", "pow", "sqrt", "cbrt", "max", "min", "abs", "tanh", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: + if node.name.name in ["malloc", "min", "max", "pow", "cbrt", "tanh", "atan2", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: return self.generic_visit(node) if hasattr(node, "subroutine"): if node.subroutine is True: @@ -368,7 +368,8 @@ def __init__(self): self.nodes: List[ast_internal_classes.Array_Subscript_Node] = [] def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): - if node.name.name in ["sqrt", "exp", "pow", "max", "min", "abs", "tanh"]: + from dace.frontend.fortran.intrinsics import FortranIntrinsics + if node.name.name in ["pow", "atan2", "tanh", *FortranIntrinsics.retained_function_names()]: return self.generic_visit(node) else: return @@ -401,7 +402,8 @@ def __init__(self, ast: ast_internal_classes.FNode, normalize_offsets: bool = Fa self.scope_vars.visit(ast) def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): - if node.name.name in ["sqrt", "exp", "pow", "max", "min", "abs", "tanh"]: + from dace.frontend.fortran.intrinsics import FortranIntrinsics + if node.name.name in ["pow", "atan2", "tanh", *FortranIntrinsics.retained_function_names()]: return self.generic_visit(node) else: return node diff --git a/dace/frontend/fortran/fortran_parser.py b/dace/frontend/fortran/fortran_parser.py index 52344c141f..a5b1f63cb1 100644 --- a/dace/frontend/fortran/fortran_parser.py +++ b/dace/frontend/fortran/fortran_parser.py @@ -818,7 +818,8 @@ def binop2sdfg(self, node: ast_internal_classes.BinOp_Node, sdfg: SDFG, cfg: Con calls.visit(node) if len(calls.nodes) == 1: augmented_call = calls.nodes[0] - if augmented_call.name.name not in ["sqrt", "exp", "pow", "max", "min", "abs", "tanh", "__dace_epsilon"]: + from dace.frontend.fortran.intrinsics import FortranIntrinsics + if augmented_call.name.name not in ["pow", "atan2", "tanh", "__dace_epsilon", *FortranIntrinsics.retained_function_names()]: augmented_call.args.append(node.lval) augmented_call.hasret = True self.call2sdfg(augmented_call, sdfg, cfg) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index c2e5afe79b..fc54b2b16a 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -939,6 +939,47 @@ def _generate_loop_body(self, node: ast_internal_classes.FNode) -> ast_internal_ line_number=node.line_number ) +class MathFunctions(IntrinsicTransformation): + + INTRINSIC_TO_DACE = { + "MIN": "min", + "MAX": "max", + "SQRT": "sqrt", + "ABS": "abs", + "EXP": "exp", + "COSH": "cosh", + "TANH": "tanh", + "ATAN2": "atan2", + } + + FUNC_TYPE = { + "min": "DOUBLE", + "max": "DOUBLE", + "sqrt": "DOUBLE", + "abs": "DOUBLE", + "exp": "DOUBLE", + "tanh": "DOUBLE", + "cosh": "DOUBLE", + "atan2": "DOUBLE", + } + + @staticmethod + def replacable(func_name: str) -> bool: + return func_name in MathFunctions.INTRINSIC_TO_DACE + + @staticmethod + def replacable_type(func_name: str) -> bool: + return func_name in MathFunctions.FUNC_TYPE + + @staticmethod + def replace(func_name: str) -> ast_internal_classes.FNode: + return ast_internal_classes.Name_Node(name=MathFunctions.INTRINSIC_TO_DACE[func_name]) + + @staticmethod + def replace_function_reference(name: ast_internal_classes.Name_Node, args: ast_internal_classes.Arg_List_Node, line): + call_type = MathFunctions.FUNC_TYPE[name.name] + return ast_internal_classes.Call_Expr_Node(name=name, type=call_type, args=args.args, line_number=line) + class FortranIntrinsics: IMPLEMENTATIONS_AST = { @@ -971,11 +1012,18 @@ def transformations(self) -> Set[Type[NodeTransformer]]: @staticmethod def function_names() -> List[str]: - return list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()) + return [*list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()), *list(MathFunctions.INTRINSIC_TO_DACE.values())] + + @staticmethod + def retained_function_names() -> List[str]: + return list(MathFunctions.INTRINSIC_TO_DACE.values()) @staticmethod def call_extraction_exemptions() -> List[str]: - return [func.Transformation.func_name() for func in FortranIntrinsics.EXEMPTED_FROM_CALL_EXTRACTION] + return [ + *[func.Transformation.func_name() for func in FortranIntrinsics.EXEMPTED_FROM_CALL_EXTRACTION], + *list(MathFunctions.INTRINSIC_TO_DACE.values()) + ] def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Node: @@ -983,39 +1031,25 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod replacements = { "INT": "__dace_int", "DBLE": "__dace_dble", - "SQRT": "sqrt", - "COSH": "cosh", - "ABS": "abs", - "MIN": "min", - "MAX": "max", - "EXP": "exp", "EPSILON": "__dace_epsilon", - "TANH": "tanh", "SIGN": "__dace_sign", - "EXP": "exp" } if func_name in replacements: return ast_internal_classes.Name_Node(name=replacements[func_name]) - else: + elif MathFunctions.replacable(func_name): + return MathFunctions.replace(func_name) - if self.IMPLEMENTATIONS_AST[func_name].has_transformation(): - self._transformations_to_run.add(self.IMPLEMENTATIONS_AST[func_name].Transformation) + if self.IMPLEMENTATIONS_AST[func_name].has_transformation(): + self._transformations_to_run.add(self.IMPLEMENTATIONS_AST[func_name].Transformation) - return ast_internal_classes.Name_Node(name=self.IMPLEMENTATIONS_AST[func_name].replaced_name(func_name)) + return ast_internal_classes.Name_Node(name=self.IMPLEMENTATIONS_AST[func_name].replaced_name(func_name)) def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: ast_internal_classes.Arg_List_Node, line): func_types = { "__dace_int": "INT", "__dace_dble": "DOUBLE", - "sqrt": "DOUBLE", - "cosh": "DOUBLE", - "abs": "DOUBLE", - "min": "DOUBLE", - "max": "DOUBLE", - "exp": "DOUBLE", "__dace_epsilon": "DOUBLE", - "tanh": "DOUBLE", "__dace_sign": "DOUBLE", } if name.name in func_types: @@ -1024,6 +1058,8 @@ def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: return ast_internal_classes.Call_Expr_Node(name=name, type=call_type, args=args.args, line_number=line) elif name.name in self.DIRECT_REPLACEMENTS: return self.DIRECT_REPLACEMENTS[name.name].replace(name, args, line) + elif MathFunctions.replacable_type(name.name): + return MathFunctions.replace_function_reference(name, args, line) else: # We will do the actual type replacement later # To that end, we need to know the input types - but these we do not know at the moment. diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py new file mode 100644 index 0000000000..e4ddaf5cdc --- /dev/null +++ b/tests/fortran/intrinsic_math.py @@ -0,0 +1,164 @@ +# Copyright 2019-2023 ETH Zurich and the DaCe authors. All rights reserved. + +import numpy as np +import pytest + +from dace.frontend.fortran import fortran_parser + +def test_fortran_frontend_min_max(): + test_string = """ + PROGRAM intrinsic_math_test_min_max + implicit none + double precision, dimension(2) :: arg1 + double precision, dimension(2) :: arg2 + double precision, dimension(2) :: res1 + double precision, dimension(2) :: res2 + CALL intrinsic_math_test_function(arg1, arg2, res1, res2) + end + + SUBROUTINE intrinsic_math_test_function(arg1, arg2, res1, res2) + double precision, dimension(2) :: arg1 + double precision, dimension(2) :: arg2 + double precision, dimension(2) :: res1 + double precision, dimension(2) :: res2 + + res1(1) = MIN(arg1(1), arg2(1)) + res1(2) = MIN(arg1(2), arg2(2)) + + res2(1) = MAX(arg1(1), arg2(1)) + res2(2) = MAX(arg1(2), arg2(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_min_max", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + arg1 = np.full([size], 42, order="F", dtype=np.float64) + arg2 = np.full([size], 42, order="F", dtype=np.float64) + + arg1[0] = 20 + arg1[1] = 25 + arg2[0] = 30 + arg2[1] = 18 + + res1 = np.full([2], 42, order="F", dtype=np.float64) + res2 = np.full([2], 42, order="F", dtype=np.float64) + sdfg(arg1=arg1, arg2=arg2, res1=res1, res2=res2) + + assert res1[0] == 20 + assert res1[1] == 18 + assert res2[0] == 30 + assert res2[1] == 25 + + +def test_fortran_frontend_sqrt(): + test_string = """ + PROGRAM intrinsic_math_test_sqrt + implicit none + double precision, dimension(2) :: d + double precision, dimension(2) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(2) :: d + double precision, dimension(2) :: res + + res(1) = SQRT(d(1)) + res(2) = SQRT(d(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_sqrt", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 2 + d[1] = 5 + res = np.full([2], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + py_res = np.sqrt(d) + + for f_res, p_res in zip(res, py_res): + assert abs(f_res - p_res) < 10**-9 + +def test_fortran_frontend_abs(): + test_string = """ + PROGRAM intrinsic_math_test_abs + implicit none + double precision, dimension(2) :: d + double precision, dimension(2) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(2) :: d + double precision, dimension(2) :: res + + res(1) = ABS(d(1)) + res(2) = ABS(d(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_abs", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = -30 + d[1] = 40 + res = np.full([2], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + + assert res[0] == 30 + assert res[1] == 40 + +def test_fortran_frontend_exp(): + test_string = """ + PROGRAM intrinsic_math_test_exp + implicit none + double precision, dimension(2) :: d + double precision, dimension(2) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(2) :: d + double precision, dimension(2) :: res + + res(1) = EXP(d(1)) + res(2) = EXP(d(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 2 + d[1] = 4.5 + res = np.full([2], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + py_res = np.exp(d) + + for f_res, p_res in zip(res, py_res): + assert abs(f_res - p_res) < 10**-9 + + +if __name__ == "__main__": + + test_fortran_frontend_min_max() + test_fortran_frontend_sqrt() + test_fortran_frontend_abs() + test_fortran_frontend_exp() From 7e508de67773a101a37e4f9253689148ccbca1c2 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 03:21:25 +0100 Subject: [PATCH 02/20] Support for additional math intrinsics --- dace/frontend/fortran/ast_transforms.py | 2 +- dace/frontend/fortran/fortran_parser.py | 3 +- dace/frontend/fortran/intrinsics.py | 317 ++++++++++++++++++------ dace/runtime/include/dace/math.h | 6 + tests/fortran/intrinsic_math.py | 157 ++++++++++++ 5 files changed, 403 insertions(+), 82 deletions(-) diff --git a/dace/frontend/fortran/ast_transforms.py b/dace/frontend/fortran/ast_transforms.py index c77d17beb4..71cd089e5f 100644 --- a/dace/frontend/fortran/ast_transforms.py +++ b/dace/frontend/fortran/ast_transforms.py @@ -241,7 +241,7 @@ def __init__(self, count=0): def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): from dace.frontend.fortran.intrinsics import FortranIntrinsics - if node.name.name in ["malloc", "min", "max", "pow", "cbrt", "tanh", "atan2", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: + if node.name.name in ["malloc", "pow", "cbrt", "tanh", "atan2", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: return self.generic_visit(node) if hasattr(node, "subroutine"): if node.subroutine is True: diff --git a/dace/frontend/fortran/fortran_parser.py b/dace/frontend/fortran/fortran_parser.py index a5b1f63cb1..0388f94688 100644 --- a/dace/frontend/fortran/fortran_parser.py +++ b/dace/frontend/fortran/fortran_parser.py @@ -1127,7 +1127,8 @@ def create_sdfg_from_string( program = ast_transforms.ArrayToLoop(program).visit(program) for transformation in own_ast.fortran_intrinsics().transformations(): - program = transformation(program).visit(program) + transformation.initialize(program) + program = transformation.visit(program) program = ast_transforms.ForDeclarer().visit(program) program = ast_transforms.IndexExtractor(program, normalize_offsets).visit(program) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index fc54b2b16a..3cfdd1807a 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -2,6 +2,7 @@ from abc import abstractmethod import copy import math +from collections import namedtuple from typing import Any, List, Optional, Set, Tuple, Type from dace.frontend.fortran import ast_internal_classes @@ -26,6 +27,20 @@ def replace(func_name: ast_internal_classes.Name_Node, args: ast_internal_classe def has_transformation() -> bool: return False +class IntrinsicNodeTransformer(NodeTransformer): + + def initialize(self, ast): + # We need to rerun the assignment because transformations could have created + # new AST nodes + ParentScopeAssigner().visit(ast) + self.scope_vars = ScopeVarsDeclarations() + self.scope_vars.visit(ast) + + @staticmethod + @abstractmethod + def func_name(self) -> str: + pass + class SelectedKind(IntrinsicTransformation): FUNCTIONS = { @@ -84,36 +99,58 @@ class LoopBasedReplacementVisitor(NodeVisitor): def __init__(self, func_name: str): self._func_name = func_name self.nodes: List[ast_internal_classes.FNode] = [] + self.calls: List[ast_internal_classes.FNode] = [] + #self.nodes = set() - def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): + #def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): + + # print(node.lval, node.rval) + # if isinstance(node.lval, ast_internal_classes.Array_Subscript_Node): + # print(node.lval) + # if isinstance(node.rval, ast_internal_classes.Call_Expr_Node): + # print(node.rval.name.name, type(node.rval)) + # if node.rval.name.name == self._func_name: + # self.nodes.append(node) + + # self.visit(node.lval) + # self.visit(node.rval) + + #def visit_Parenthesis_Expr_Node(self, node: ast_internal_classes.BinOp_Node): + + # print('unop', node, dir(node), node.expr) + # if isinstance(node.expr, ast_internal_classes.Call_Expr_Node): + # self.visit(node.expr) + def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): if isinstance(node.rval, ast_internal_classes.Call_Expr_Node): if node.rval.name.name == self._func_name: self.nodes.append(node) + self.calls.append(node.rval) + self.visit(node.lval) + self.visit(node.rval) + + def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): + + if node.name.name == self._func_name: + if node not in self.calls: + + #parent = node.parent + #if not isinstance(parent, ast_internal_classes.BinOp_Node): + # raise NotImplementedError() + self.nodes.append(node) def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): return -class LoopBasedReplacementTransformation(NodeTransformer): +class LoopBasedReplacementTransformation(IntrinsicNodeTransformer): """ Transforms the AST by removing intrinsic call and replacing it with loops """ - def __init__(self, ast): + def __init__(self): self.count = 0 - - # We need to rerun the assignment because transformations could have created - # new AST nodes - ParentScopeAssigner().visit(ast) - self.scope_vars = ScopeVarsDeclarations() - self.scope_vars.visit(ast) self.rvals = [] - @staticmethod - @abstractmethod - def func_name() -> str: - pass - @abstractmethod def _initialize(self): pass @@ -338,9 +375,6 @@ def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_No class SumProduct(LoopBasedReplacementTransformation): - def __init__(self, ast): - super().__init__(ast) - def _initialize(self): self.rvals = [] self.argument_variable = None @@ -414,9 +448,6 @@ class Sum(LoopBasedReplacement): class Transformation(SumProduct): - def __init__(self, ast): - super().__init__(ast) - @staticmethod def func_name() -> str: return "__dace_sum" @@ -440,9 +471,6 @@ class Product(LoopBasedReplacement): class Transformation(SumProduct): - def __init__(self, ast): - super().__init__(ast) - @staticmethod def func_name() -> str: return "__dace_product" @@ -455,9 +483,6 @@ def _result_update_op(self): class AnyAllCountTransformation(LoopBasedReplacementTransformation): - def __init__(self, ast): - super().__init__(ast) - def _initialize(self): self.rvals = [] @@ -575,9 +600,6 @@ class Any(LoopBasedReplacement): """ class Transformation(AnyAllCountTransformation): - def __init__(self, ast): - super().__init__(ast) - def _result_init_value(self): return "0" @@ -607,9 +629,6 @@ class All(LoopBasedReplacement): """ class Transformation(AnyAllCountTransformation): - def __init__(self, ast): - super().__init__(ast) - def _result_init_value(self): return "1" @@ -644,9 +663,6 @@ class Count(LoopBasedReplacement): """ class Transformation(AnyAllCountTransformation): - def __init__(self, ast): - super().__init__(ast) - def _result_init_value(self): return "0" @@ -675,9 +691,6 @@ def func_name() -> str: class MinMaxValTransformation(LoopBasedReplacementTransformation): - def __init__(self, ast): - super().__init__(ast) - def _initialize(self): self.rvals = [] self.argument_variable = None @@ -753,9 +766,6 @@ class MinVal(LoopBasedReplacement): """ class Transformation(MinMaxValTransformation): - def __init__(self, ast): - super().__init__(ast) - def _result_init_value(self, array: ast_internal_classes.Array_Subscript_Node): var_decl = self.scope_vars.get_var(array.parent, array.name.name) @@ -788,9 +798,6 @@ class MaxVal(LoopBasedReplacement): """ class Transformation(MinMaxValTransformation): - def __init__(self, ast): - super().__init__(ast) - def _result_init_value(self, array: ast_internal_classes.Array_Subscript_Node): var_decl = self.scope_vars.get_var(array.parent, array.name.name) @@ -817,9 +824,6 @@ class Merge(LoopBasedReplacement): class Transformation(LoopBasedReplacementTransformation): - def __init__(self, ast): - super().__init__(ast) - def _initialize(self): self.rvals = [] @@ -941,44 +945,190 @@ def _generate_loop_body(self, node: ast_internal_classes.FNode) -> ast_internal_ class MathFunctions(IntrinsicTransformation): + MathTransformation = namedtuple("MathTransformation", "function return_type") + INTRINSIC_TO_DACE = { - "MIN": "min", - "MAX": "max", - "SQRT": "sqrt", - "ABS": "abs", - "EXP": "exp", - "COSH": "cosh", - "TANH": "tanh", - "ATAN2": "atan2", + "MIN": MathTransformation("min", "FIRST_ARG"), + "MAX": MathTransformation("max", "FIRST_ARG"), + "SQRT": MathTransformation("sqrt", "FIRST_ARG"), + "ABS": MathTransformation("abs", "FIRST_ARG"), + "EXP": MathTransformation("exp", "FIRST_ARG"), + # Documentation states that the return type of LOG is always REAL, + # but the kind is the same as of the first argument. + # However, we already replaced kind with types used in DaCe. + # Thus, a REAL that is really DOUBLE will be double in the first argument. + "LOG": MathTransformation("log", "FIRST_ARG"), + "MOD": { + "INTEGER": MathTransformation("Mod", "INTEGER"), + "REAL": MathTransformation("Mod_float", "REAL"), + "DOUBLE": MathTransformation("Mod_float", "DOUBLE") + }, + "COSH": MathTransformation("cosh", "FIRST_ARG"), + "TANH": MathTransformation("tanh", "FIRST_ARG"), + "ATAN2": MathTransformation("atan2", "FIRST_ARG") } - FUNC_TYPE = { - "min": "DOUBLE", - "max": "DOUBLE", - "sqrt": "DOUBLE", - "abs": "DOUBLE", - "exp": "DOUBLE", - "tanh": "DOUBLE", - "cosh": "DOUBLE", - "atan2": "DOUBLE", - } + class TypeTransformer(IntrinsicNodeTransformer): + + def __init__(self, func_name: str): + self._func_name = func_name + + def func_name(self) -> str: + return self._func_name + + def func_type(self, node: ast_internal_classes.Call_Expr_Node): + + # take the first arg + arg = node.args[0] + if isinstance(arg, ast_internal_classes.Real_Literal_Node): + return 'REAL' + elif isinstance(arg, ast_internal_classes.Int_Literal_Node): + return 'INTEGER' + else: + input_type = self.scope_vars.get_var(node.parent, arg.name.name) + return input_type.type + + def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): + newbody = [] + + for child in node.execution: + lister = LoopBasedReplacementVisitor(f'__dace_{self.func_name()}') + lister.visit(child) + res = lister.nodes + + #newbody.append(self.visit(child)) + if res is None or len(res) == 0: + newbody.append(self.visit(child)) + continue + + for call in res: + + return_type = None + input_type = None + if isinstance(call, ast_internal_classes.BinOp_Node): + input_type = self.func_type(call.rval) + else: + input_type = self.func_type(call) + + + replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + if isinstance(replacement_rule, dict): + replacement_rule = replacement_rule[input_type] + if replacement_rule.return_type == "FIRST_ARG": + return_type = input_type + else: + return_type = replacement_rule.return_type + + + if isinstance(call, ast_internal_classes.BinOp_Node): + + call.rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + call.rval.type = return_type + #call.rval = ast_internal_classes.Call_Expr_Node(type=return_type, + # name=ast_internal_classes.Name_Node(name=replacement_rule.function), + # args=new_args, + # line_number=call.rval.line_number) + var = call.lval + name = None + if isinstance(var.name, ast_internal_classes.Name_Node): + name = var.name.name + else: + name = var.name + var_decl = self.scope_vars.get_var(var.parent, name) + var.type = input_type + var_decl.type = input_type + else: + + call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + call.type = return_type + + newbody.append(child) + + #print(res) + #if len(res) > 1: + # raise NotImplementedError() + + #print('VISIT') + ## check the type of the function + #input_type = self.func_type(res[0].rval) + #new_args = [] + #for i in res[0].rval.args: + # print('child', i) + # new_args.append(self.visit(i)) + + #return_type = None + + #replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + #if isinstance(replacement_rule, dict): + # replacement_rule = replacement_rule[input_type] + + #if replacement_rule.return_type == "FIRST_ARG": + # return_type = input_type + #else: + # return_type = replacement_rule.return_type + + #new_call = ast_internal_classes.Call_Expr_Node(type=return_type, + # name=ast_internal_classes.Name_Node(name=replacement_rule.function), + # args=new_args, + # line_number=res[0].rval.line_number) + + ## replace function name + ##res[0].rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + + #var = res[0].lval + #print('type', input_type) + #name = None + #if isinstance(var.name, ast_internal_classes.Name_Node): + # name = var.name.name + #else: + # name = var.name + #var_decl = self.scope_vars.get_var(var.parent, name) + #var.type = input_type + #var_decl.type = input_type + + #res[0].rval = new_call + + #newbody.append(child) + + return ast_internal_classes.Execution_Part_Node(execution=newbody) @staticmethod - def replacable(func_name: str) -> bool: - return func_name in MathFunctions.INTRINSIC_TO_DACE + def dace_functions(): + + # list of final dace functions which we create + funcs = list(MathFunctions.INTRINSIC_TO_DACE.values()) + res = [] + # flatten nested lists + for f in funcs: + if isinstance(f, dict): + res.extend([v.function for k, v in f.items()]) + else: + res.append(f.function) + return res @staticmethod - def replacable_type(func_name: str) -> bool: - return func_name in MathFunctions.FUNC_TYPE + def temporary_functions(): + + # temporary functions created by us -> f becomes __dace_f + # We provide this to tell Fortran parser that these are function calls, + # not array accesses + funcs = list(MathFunctions.INTRINSIC_TO_DACE.keys()) + return [f'__dace_{f}' for f in funcs] + + @staticmethod + def replacable(func_name: str) -> bool: + return func_name in MathFunctions.INTRINSIC_TO_DACE @staticmethod def replace(func_name: str) -> ast_internal_classes.FNode: - return ast_internal_classes.Name_Node(name=MathFunctions.INTRINSIC_TO_DACE[func_name]) + return ast_internal_classes.Name_Node(name=f'__dace_{func_name}') + + def has_transformation() -> bool: + return True @staticmethod - def replace_function_reference(name: ast_internal_classes.Name_Node, args: ast_internal_classes.Arg_List_Node, line): - call_type = MathFunctions.FUNC_TYPE[name.name] - return ast_internal_classes.Call_Expr_Node(name=name, type=call_type, args=args.args, line_number=line) + def get_transformation(func_name: str) -> TypeTransformer: + return MathFunctions.TypeTransformer(func_name) class FortranIntrinsics: @@ -1012,17 +1162,20 @@ def transformations(self) -> Set[Type[NodeTransformer]]: @staticmethod def function_names() -> List[str]: - return [*list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()), *list(MathFunctions.INTRINSIC_TO_DACE.values())] + # list of all functions that are created by initial transformation, before doing full replacement + # this prevents other parser components from replacing our function calls with array subscription nodes + return [*list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()), *MathFunctions.temporary_functions()] @staticmethod def retained_function_names() -> List[str]: - return list(MathFunctions.INTRINSIC_TO_DACE.values()) + # list of all DaCe functions that we use after full parsing + return MathFunctions.dace_functions() @staticmethod def call_extraction_exemptions() -> List[str]: return [ *[func.Transformation.func_name() for func in FortranIntrinsics.EXEMPTED_FROM_CALL_EXTRACTION], - *list(MathFunctions.INTRINSIC_TO_DACE.values()) + *MathFunctions.temporary_functions() ] def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Node: @@ -1037,10 +1190,16 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod if func_name in replacements: return ast_internal_classes.Name_Node(name=replacements[func_name]) elif MathFunctions.replacable(func_name): + + self._transformations_to_run.add(MathFunctions.get_transformation(func_name)) return MathFunctions.replace(func_name) if self.IMPLEMENTATIONS_AST[func_name].has_transformation(): - self._transformations_to_run.add(self.IMPLEMENTATIONS_AST[func_name].Transformation) + + if hasattr(self.IMPLEMENTATIONS_AST[func_name], "Transformation"): + self._transformations_to_run.add(self.IMPLEMENTATIONS_AST[func_name].Transformation()) + else: + self._transformations_to_run.add(self.IMPLEMENTATIONS_AST[func_name].get_transformation(func_name)) return ast_internal_classes.Name_Node(name=self.IMPLEMENTATIONS_AST[func_name].replaced_name(func_name)) @@ -1058,12 +1217,10 @@ def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: return ast_internal_classes.Call_Expr_Node(name=name, type=call_type, args=args.args, line_number=line) elif name.name in self.DIRECT_REPLACEMENTS: return self.DIRECT_REPLACEMENTS[name.name].replace(name, args, line) - elif MathFunctions.replacable_type(name.name): - return MathFunctions.replace_function_reference(name, args, line) else: # We will do the actual type replacement later # To that end, we need to know the input types - but these we do not know at the moment. return ast_internal_classes.Call_Expr_Node( - name=name, type="VOID", + name=name, type="INTEGER", args=args.args, line_number=line ) diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index 0a9d153767..546580f0a1 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -61,6 +61,12 @@ static DACE_CONSTEXPR DACE_HDFI T Mod(const T& value, const T2& modulus) { return value % modulus; } +// Fortran implements MOD for floating-point values as well +template +static DACE_CONSTEXPR DACE_HDFI T Mod_float(const T& value, const T& modulus) { + return value - static_cast(value / modulus) * modulus; +} + template static DACE_CONSTEXPR DACE_HDFI T int_ceil(const T& numerator, const T2& denominator) { return (numerator + denominator - 1) / denominator; diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index e4ddaf5cdc..507e211e3c 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -155,6 +155,160 @@ def test_fortran_frontend_exp(): for f_res, p_res in zip(res, py_res): assert abs(f_res - p_res) < 10**-9 +def test_fortran_frontend_log(): + test_string = """ + PROGRAM intrinsic_math_test_log + implicit none + double precision, dimension(2) :: d + double precision, dimension(2) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(2) :: d + double precision, dimension(2) :: res + + res(1) = LOG(d(1)) + res(2) = LOG(d(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 2.71 + d[1] = 4.5 + res = np.full([2], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + py_res = np.log(d) + + for f_res, p_res in zip(res, py_res): + assert abs(f_res - p_res) < 10**-9 + +def test_fortran_frontend_log(): + test_string = """ + PROGRAM intrinsic_math_test_log + implicit none + double precision, dimension(2) :: d + double precision, dimension(2) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(2) :: d + double precision, dimension(2) :: res + + res(1) = LOG(d(1)) + res(2) = LOG(d(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 2.71 + d[1] = 4.5 + res = np.full([2], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + py_res = np.log(d) + + for f_res, p_res in zip(res, py_res): + assert abs(f_res - p_res) < 10**-9 + +def test_fortran_frontend_mod_float(): + test_string = """ + PROGRAM intrinsic_math_test_mod + implicit none + double precision, dimension(8) :: d + double precision, dimension(4) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(8) :: d + double precision, dimension(4) :: res + + res(1) = MOD(d(1), d(2)) + res(2) = MOD(d(3), d(4)) + res(3) = MOD(d(5), d(6)) + res(4) = MOD(d(7), d(8)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 8 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 17 + d[1] = 3 + d[2] = -17 + d[3] = 3 + d[4] = 17 + d[5] = -3 + d[6] = -17 + d[7] = -3 + res = np.full([4], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + + assert res[0] == 2.0 + assert res[1] == -2.0 + assert res[2] == 2.0 + assert res[3] == -2.0 + +def test_fortran_frontend_mod_integer(): + test_string = """ + PROGRAM intrinsic_math_test_mod + implicit none + integer, dimension(8) :: d + integer, dimension(4) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + integer, dimension(8) :: d + integer, dimension(4) :: res + + res(1) = MOD(d(1), d(2)) + res(2) = MOD(d(3), d(4)) + res(3) = MOD(d(5), d(6)) + res(4) = MOD(d(7), d(8)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 8 + d = np.full([size], 42, order="F", dtype=np.int32) + d[0] = 17 + d[1] = 3 + d[2] = -17 + d[3] = 3 + d[4] = 17 + d[5] = -3 + d[6] = -17 + d[7] = -3 + res = np.full([4], 42, order="F", dtype=np.int32) + sdfg(d=d, res=res) + print(res) + + assert res[0] == 2 + assert res[1] == -2 + assert res[2] == 2 + assert res[3] == -2 if __name__ == "__main__": @@ -162,3 +316,6 @@ def test_fortran_frontend_exp(): test_fortran_frontend_sqrt() test_fortran_frontend_abs() test_fortran_frontend_exp() + test_fortran_frontend_log() + test_fortran_frontend_mod_float() + test_fortran_frontend_mod_integer() From 71c5746ae473d583c101f355099675c70ad05b25 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 16:37:12 +0100 Subject: [PATCH 03/20] Add Fortran MODULO intrinsic --- dace/frontend/fortran/intrinsics.py | 13 +-- dace/runtime/include/dace/math.h | 19 ++++ tests/fortran/intrinsic_math.py | 140 ++++++++++++++++++++++++---- 3 files changed, 145 insertions(+), 27 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 3cfdd1807a..b3e1b43ce3 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -100,15 +100,10 @@ def __init__(self, func_name: str): self._func_name = func_name self.nodes: List[ast_internal_classes.FNode] = [] self.calls: List[ast_internal_classes.FNode] = [] - #self.nodes = set() #def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): - # print(node.lval, node.rval) - # if isinstance(node.lval, ast_internal_classes.Array_Subscript_Node): - # print(node.lval) # if isinstance(node.rval, ast_internal_classes.Call_Expr_Node): - # print(node.rval.name.name, type(node.rval)) # if node.rval.name.name == self._func_name: # self.nodes.append(node) @@ -134,9 +129,6 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): if node.name.name == self._func_name: if node not in self.calls: - #parent = node.parent - #if not isinstance(parent, ast_internal_classes.BinOp_Node): - # raise NotImplementedError() self.nodes.append(node) def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): @@ -963,6 +955,11 @@ class MathFunctions(IntrinsicTransformation): "REAL": MathTransformation("Mod_float", "REAL"), "DOUBLE": MathTransformation("Mod_float", "DOUBLE") }, + "MODULO": { + "INTEGER": MathTransformation("Modulo", "INTEGER"), + "REAL": MathTransformation("Modulo_float", "REAL"), + "DOUBLE": MathTransformation("Modulo_float", "DOUBLE") + }, "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index 546580f0a1..cf633c9ffc 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -67,6 +67,25 @@ static DACE_CONSTEXPR DACE_HDFI T Mod_float(const T& value, const T& modulus) { return value - static_cast(value / modulus) * modulus; } +// Fortran implementation of MODULO +template +static DACE_CONSTEXPR DACE_HDFI T Modulo(const T& value, const T& modulus) { + // Fortran implementation for integers - find R such that value = Q * modulus + R + // However, R must be in [0, modulus) + // To achieve that, we need to cast the division to floats. + // Example: -17, 3 must produce 1 and not -2. + // If we don't use cast, the floor is called on -5, producing wrong value. + // Instead, we need to have floor(-5.6... ) to ensure it produces -6. + // Similarly, 17, -3 must produce -1 and not 2. + // This means that the default solution works if value and modulus have the same sign. + return value - floor(static_cast(value) / modulus) * modulus; +} + +template +static DACE_CONSTEXPR DACE_HDFI T Modulo_float(const T& value, const T& modulus) { + return value - floor(value / modulus) * modulus; +} + template static DACE_CONSTEXPR DACE_HDFI T int_ceil(const T& numerator, const T2& denominator) { return (numerator + denominator - 1) / denominator; diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index 507e211e3c..978f961cb7 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -227,44 +227,52 @@ def test_fortran_frontend_mod_float(): test_string = """ PROGRAM intrinsic_math_test_mod implicit none - double precision, dimension(8) :: d - double precision, dimension(4) :: res + double precision, dimension(12) :: d + double precision, dimension(6) :: res CALL intrinsic_math_test_function(d, res) end SUBROUTINE intrinsic_math_test_function(d, res) - double precision, dimension(8) :: d - double precision, dimension(4) :: res + double precision, dimension(12) :: d + double precision, dimension(6) :: res res(1) = MOD(d(1), d(2)) res(2) = MOD(d(3), d(4)) res(3) = MOD(d(5), d(6)) res(4) = MOD(d(7), d(8)) + res(5) = MOD(d(9), d(10)) + res(6) = MOD(d(11), d(12)) END SUBROUTINE intrinsic_math_test_function """ - sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_mod", False) sdfg.simplify(verbose=True) sdfg.compile() - size = 8 + size = 12 d = np.full([size], 42, order="F", dtype=np.float64) - d[0] = 17 - d[1] = 3 - d[2] = -17 - d[3] = 3 - d[4] = 17 - d[5] = -3 - d[6] = -17 - d[7] = -3 - res = np.full([4], 42, order="F", dtype=np.float64) + d[0] = 17. + d[1] = 3. + d[2] = -17. + d[3] = 3. + d[4] = 17. + d[5] = -3. + d[6] = -17. + d[7] = -3. + d[8] = 17.5 + d[9] = 5.5 + d[10] = -17.5 + d[11] = 5.5 + res = np.full([6], 42, order="F", dtype=np.float64) sdfg(d=d, res=res) assert res[0] == 2.0 assert res[1] == -2.0 assert res[2] == 2.0 assert res[3] == -2.0 + assert res[4] == 1 + assert res[5] == -1 def test_fortran_frontend_mod_integer(): test_string = """ @@ -287,11 +295,11 @@ def test_fortran_frontend_mod_integer(): END SUBROUTINE intrinsic_math_test_function """ - sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_exp", False) + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) sdfg.simplify(verbose=True) sdfg.compile() - size = 8 + size = 12 d = np.full([size], 42, order="F", dtype=np.int32) d[0] = 17 d[1] = 3 @@ -303,13 +311,105 @@ def test_fortran_frontend_mod_integer(): d[7] = -3 res = np.full([4], 42, order="F", dtype=np.int32) sdfg(d=d, res=res) - print(res) - assert res[0] == 2 assert res[1] == -2 assert res[2] == 2 assert res[3] == -2 +def test_fortran_frontend_modulo_float(): + test_string = """ + PROGRAM intrinsic_math_test_modulo + implicit none + double precision, dimension(12) :: d + double precision, dimension(6) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + double precision, dimension(12) :: d + double precision, dimension(6) :: res + + res(1) = MODULO(d(1), d(2)) + res(2) = MODULO(d(3), d(4)) + res(3) = MODULO(d(5), d(6)) + res(4) = MODULO(d(7), d(8)) + res(5) = MODULO(d(9), d(10)) + res(6) = MODULO(d(11), d(12)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 12 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 17. + d[1] = 3. + d[2] = -17. + d[3] = 3. + d[4] = 17. + d[5] = -3. + d[6] = -17. + d[7] = -3. + d[8] = 17.5 + d[9] = 5.5 + d[10] = -17.5 + d[11] = 5.5 + res = np.full([6], 42, order="F", dtype=np.float64) + sdfg(d=d, res=res) + + assert res[0] == 2.0 + assert res[1] == 1.0 + assert res[2] == -1.0 + assert res[3] == -2.0 + assert res[4] == 1.0 + assert res[5] == 4.5 + +def test_fortran_frontend_modulo_integer(): + test_string = """ + PROGRAM intrinsic_math_test_modulo + implicit none + integer, dimension(8) :: d + integer, dimension(4) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + integer, dimension(8) :: d + integer, dimension(4) :: res + + res(1) = MODULO(d(1), d(2)) + res(2) = MODULO(d(3), d(4)) + res(3) = MODULO(d(5), d(6)) + res(4) = MODULO(d(7), d(8)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 12 + d = np.full([size], 42, order="F", dtype=np.int32) + d[0] = 17 + d[1] = 3 + d[2] = -17 + d[3] = 3 + d[4] = 17 + d[5] = -3 + d[6] = -17 + d[7] = -3 + res = np.full([4], 42, order="F", dtype=np.int32) + sdfg(d=d, res=res) + + assert res[0] == 2 + assert res[1] == 1 + assert res[2] == -1 + assert res[3] == -2 + if __name__ == "__main__": test_fortran_frontend_min_max() @@ -319,3 +419,5 @@ def test_fortran_frontend_mod_integer(): test_fortran_frontend_log() test_fortran_frontend_mod_float() test_fortran_frontend_mod_integer() + test_fortran_frontend_modulo_float() + test_fortran_frontend_modulo_integer() From 8287abe57292817a013f0c8bf3b651b2cbced609 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 16:41:42 +0100 Subject: [PATCH 04/20] Implement Fortran FLOOR intrinsic --- dace/frontend/fortran/intrinsics.py | 4 ++ tests/fortran/intrinsic_math.py | 58 ++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index b3e1b43ce3..596ddfa0c2 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -960,6 +960,10 @@ class MathFunctions(IntrinsicTransformation): "REAL": MathTransformation("Modulo_float", "REAL"), "DOUBLE": MathTransformation("Modulo_float", "DOUBLE") }, + "FLOOR": { + "REAL": MathTransformation("floor", "INTEGER"), + "DOUBLE": MathTransformation("floor", "INTEGER") + }, "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index 978f961cb7..ce4176f9ad 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -410,14 +410,54 @@ def test_fortran_frontend_modulo_integer(): assert res[2] == -1 assert res[3] == -2 +def test_fortran_frontend_floor(): + test_string = """ + PROGRAM intrinsic_math_test_floor + implicit none + real, dimension(4) :: d + integer, dimension(4) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + real, dimension(4) :: d + integer, dimension(4) :: res + + res(1) = FLOOR(d(1)) + res(2) = FLOOR(d(2)) + res(3) = FLOOR(d(3)) + res(4) = FLOOR(d(4)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 4 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 3.5 + d[1] = 63.000001 + d[2] = -3.5 + d[3] = -63.00001 + res = np.full([4], 42, order="F", dtype=np.int32) + sdfg(d=d, res=res) + + assert res[0] == 3 + assert res[1] == 63 + assert res[2] == -4 + assert res[3] == -64 + if __name__ == "__main__": - test_fortran_frontend_min_max() - test_fortran_frontend_sqrt() - test_fortran_frontend_abs() - test_fortran_frontend_exp() - test_fortran_frontend_log() - test_fortran_frontend_mod_float() - test_fortran_frontend_mod_integer() - test_fortran_frontend_modulo_float() - test_fortran_frontend_modulo_integer() + #test_fortran_frontend_min_max() + #test_fortran_frontend_sqrt() + #test_fortran_frontend_abs() + #test_fortran_frontend_exp() + #test_fortran_frontend_log() + #test_fortran_frontend_mod_float() + #test_fortran_frontend_mod_integer() + #test_fortran_frontend_modulo_float() + #test_fortran_frontend_modulo_integer() + test_fortran_frontend_floor() From ffe6019b56bcba4ebbdc9d0be2cb07a35efcd898 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 17:35:17 +0100 Subject: [PATCH 05/20] Implement Fortran SCALE intrinsic and add direct replacement as tasklet --- dace/frontend/fortran/intrinsics.py | 303 ++++++++++++++++++---------- tests/fortran/intrinsic_math.py | 70 ++++++- 2 files changed, 260 insertions(+), 113 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 596ddfa0c2..9265176793 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -939,6 +939,33 @@ class MathFunctions(IntrinsicTransformation): MathTransformation = namedtuple("MathTransformation", "function return_type") + def generate_scale(arg: ast_internal_classes.Call_Expr_Node): + + # SCALE(X, I) becomes: X * pow(RADIX(X), I) + # In our case, RADIX(X) is always 2 + line = arg.line_number + x = arg.args[0] + i = arg.args[1] + const_two = ast_internal_classes.Int_Literal_Node(value="2") + + # I and RADIX(X) are both integers + rval = ast_internal_classes.Call_Expr_Node( + name=ast_internal_classes.Name_Node(name="pow"), + type="INTEGER", + args=[const_two, i], + line_number=line + ) + + mult = ast_internal_classes.BinOp_Node( + op="*", + lval=x, + rval=rval, + line_number=line + ) + + # pack it into parentheses, just to be sure + return ast_internal_classes.Parenthesis_Expr_Node(expr=mult) + INTRINSIC_TO_DACE = { "MIN": MathTransformation("min", "FIRST_ARG"), "MAX": MathTransformation("max", "FIRST_ARG"), @@ -964,6 +991,7 @@ class MathFunctions(IntrinsicTransformation): "REAL": MathTransformation("floor", "INTEGER"), "DOUBLE": MathTransformation("floor", "INTEGER") }, + "SCALE": MathTransformation(generate_scale, "FIRST_ARG"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") @@ -985,113 +1013,180 @@ def func_type(self, node: ast_internal_classes.Call_Expr_Node): return 'REAL' elif isinstance(arg, ast_internal_classes.Int_Literal_Node): return 'INTEGER' + elif isinstance(arg, ast_internal_classes.Call_Expr_Node): + return arg.type else: input_type = self.scope_vars.get_var(node.parent, arg.name.name) return input_type.type - def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): - newbody = [] - - for child in node.execution: - lister = LoopBasedReplacementVisitor(f'__dace_{self.func_name()}') - lister.visit(child) - res = lister.nodes - - #newbody.append(self.visit(child)) - if res is None or len(res) == 0: - newbody.append(self.visit(child)) - continue - - for call in res: - - return_type = None - input_type = None - if isinstance(call, ast_internal_classes.BinOp_Node): - input_type = self.func_type(call.rval) - else: - input_type = self.func_type(call) - - - replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] - if isinstance(replacement_rule, dict): - replacement_rule = replacement_rule[input_type] - if replacement_rule.return_type == "FIRST_ARG": - return_type = input_type - else: - return_type = replacement_rule.return_type - - - if isinstance(call, ast_internal_classes.BinOp_Node): - - call.rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - call.rval.type = return_type - #call.rval = ast_internal_classes.Call_Expr_Node(type=return_type, - # name=ast_internal_classes.Name_Node(name=replacement_rule.function), - # args=new_args, - # line_number=call.rval.line_number) - var = call.lval - name = None - if isinstance(var.name, ast_internal_classes.Name_Node): - name = var.name.name - else: - name = var.name - var_decl = self.scope_vars.get_var(var.parent, name) - var.type = input_type - var_decl.type = input_type - else: - - call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - call.type = return_type - - newbody.append(child) - - #print(res) - #if len(res) > 1: - # raise NotImplementedError() - - #print('VISIT') - ## check the type of the function - #input_type = self.func_type(res[0].rval) - #new_args = [] - #for i in res[0].rval.args: - # print('child', i) - # new_args.append(self.visit(i)) - - #return_type = None - - #replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] - #if isinstance(replacement_rule, dict): - # replacement_rule = replacement_rule[input_type] - - #if replacement_rule.return_type == "FIRST_ARG": - # return_type = input_type - #else: - # return_type = replacement_rule.return_type - - #new_call = ast_internal_classes.Call_Expr_Node(type=return_type, - # name=ast_internal_classes.Name_Node(name=replacement_rule.function), - # args=new_args, - # line_number=res[0].rval.line_number) - - ## replace function name - ##res[0].rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - - #var = res[0].lval - #print('type', input_type) - #name = None - #if isinstance(var.name, ast_internal_classes.Name_Node): - # name = var.name.name - #else: - # name = var.name - #var_decl = self.scope_vars.get_var(var.parent, name) - #var.type = input_type - #var_decl.type = input_type - - #res[0].rval = new_call - - #newbody.append(child) - - return ast_internal_classes.Execution_Part_Node(execution=newbody) + def replace_call(self, old_call: ast_internal_classes.Call_Expr_Node, new_call: ast_internal_classes.FNode): + + parent = old_call.parent + + # We won't need it if the CallExtractor will properly support nested function calls. + # Then, all function calls should be a binary op: val = func() + if isinstance(parent, ast_internal_classes.BinOp_Node): + if parent.lval == old_call: + parent.lval = new_call + else: + parent.rval = new_call + elif isinstance(parent, ast_internal_classes.UnOp_Node): + parent.lval = new_call + elif isinstance(parent, ast_internal_classes.Parenthesis_Expr_Node): + parent.expr = new_call + elif isinstance(parent, ast_internal_classes.Call_Expr_Node): + for idx, arg in enumerate(parent.args): + if arg == old_call: + parent.args[idx] = new_call + break + else: + raise NotImplementedError() + + def visit_Call_Expr_Node(self, node: ast_internal_classes.Execution_Part_Node): + + if node.name.name != f'__dace_{self.func_name()}': + return node + + # Visit all children before we expand this call. + # We need that to properly get the type. + for arg in node.args: + self.visit(arg) + + call = node + + return_type = None + input_type = None + if isinstance(call, ast_internal_classes.BinOp_Node): + input_type = self.func_type(call.rval) + else: + input_type = self.func_type(call) + + + replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + if isinstance(replacement_rule, dict): + replacement_rule = replacement_rule[input_type] + if replacement_rule.return_type == "FIRST_ARG": + return_type = input_type + else: + return_type = replacement_rule.return_type + + if isinstance(replacement_rule.function, str): + call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + call.type = return_type + + return call + else: + new_call = replacement_rule.function(call) + return new_call + + #def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): + # newbody = [] + + # for child in node.execution: + # lister = LoopBasedReplacementVisitor(f'__dace_{self.func_name()}') + # lister.visit(child) + # res = lister.nodes + + # #newbody.append(self.visit(child)) + # if res is None or len(res) == 0: + # newbody.append(self.visit(child)) + # continue + + # for call in res: + + # return_type = None + # input_type = None + # if isinstance(call, ast_internal_classes.BinOp_Node): + # input_type = self.func_type(call.rval) + # else: + # input_type = self.func_type(call) + + + # replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + # if isinstance(replacement_rule, dict): + # replacement_rule = replacement_rule[input_type] + # if replacement_rule.return_type == "FIRST_ARG": + # return_type = input_type + # else: + # return_type = replacement_rule.return_type + + + # if isinstance(call, ast_internal_classes.BinOp_Node): + + # if isinstance(replacement_rule.function, str): + # call.rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + # else: + # call.rval = replacement_rule.function(call.rval) + # call.rval.type = return_type + + # # replace types of return variable + # var = call.lval + # name = None + # if isinstance(var.name, ast_internal_classes.Name_Node): + # name = var.name.name + # else: + # name = var.name + # var_decl = self.scope_vars.get_var(var.parent, name) + # var.type = input_type + # var_decl.type = input_type + # else: + + # if isinstance(replacement_rule.function, str): + # call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + # else: + # new_call = replacement_rule.function(call) + # self.replace_call(call, new_call) + # call.type = return_type + + # newbody.append(child) + + # #print(res) + # #if len(res) > 1: + # # raise NotImplementedError() + + # #print('VISIT') + # ## check the type of the function + # #input_type = self.func_type(res[0].rval) + # #new_args = [] + # #for i in res[0].rval.args: + # # print('child', i) + # # new_args.append(self.visit(i)) + + # #return_type = None + + # #replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + # #if isinstance(replacement_rule, dict): + # # replacement_rule = replacement_rule[input_type] + + # #if replacement_rule.return_type == "FIRST_ARG": + # # return_type = input_type + # #else: + # # return_type = replacement_rule.return_type + + # #new_call = ast_internal_classes.Call_Expr_Node(type=return_type, + # # name=ast_internal_classes.Name_Node(name=replacement_rule.function), + # # args=new_args, + # # line_number=res[0].rval.line_number) + + # ## replace function name + # ##res[0].rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + + # #var = res[0].lval + # #print('type', input_type) + # #name = None + # #if isinstance(var.name, ast_internal_classes.Name_Node): + # # name = var.name.name + # #else: + # # name = var.name + # #var_decl = self.scope_vars.get_var(var.parent, name) + # #var.type = input_type + # #var_decl.type = input_type + + # #res[0].rval = new_call + + # #newbody.append(child) + + # return ast_internal_classes.Execution_Part_Node(execution=newbody) @staticmethod def dace_functions(): @@ -1222,6 +1317,6 @@ def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: # We will do the actual type replacement later # To that end, we need to know the input types - but these we do not know at the moment. return ast_internal_classes.Call_Expr_Node( - name=name, type="INTEGER", + name=name, type="VOID", args=args.args, line_number=line ) diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index ce4176f9ad..a1eb822364 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -449,15 +449,67 @@ def test_fortran_frontend_floor(): assert res[2] == -4 assert res[3] == -64 +def test_fortran_frontend_scale(): + test_string = """ + PROGRAM intrinsic_math_test_scale + implicit none + real, dimension(4) :: d + integer, dimension(4) :: d2 + real, dimension(5) :: res + CALL intrinsic_math_test_function(d, d2, res) + end + + SUBROUTINE intrinsic_math_test_function(d, d2, res) + real, dimension(4) :: d + integer, dimension(4) :: d2 + real, dimension(5) :: res + + res(1) = SCALE(d(1), d2(1)) + res(2) = SCALE(d(2), d2(2)) + res(3) = SCALE(d(3), d2(3)) + ! Verifies that we properly replace call even inside a complex expression + res(4) = (SCALE(d(4), d2(4))) + (SCALE(d(4), d2(4))*2) + res(5) = (SCALE(SCALE(d(4), d2(4)), d2(4))) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 4 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 178.1387e-4 + d[1] = 5.5 + d[2] = 5.5 + d[3] = 42.5 + d2 = np.full([size], 42, order="F", dtype=np.int32) + d2[0] = 5 + d2[1] = 5 + d2[2] = 7 + d2[3] = 9 + res = np.full([5], 42, order="F", dtype=np.float32) + sdfg(d=d, d2=d2, res=res) + print(d) + print(res) + + assert abs(res[0] - 0.570043862) < 10**-7 + assert res[1] == 176. + assert res[2] == 704. + assert res[3] == 65280. + assert res[4] == 11141120. + if __name__ == "__main__": - #test_fortran_frontend_min_max() - #test_fortran_frontend_sqrt() - #test_fortran_frontend_abs() - #test_fortran_frontend_exp() - #test_fortran_frontend_log() - #test_fortran_frontend_mod_float() - #test_fortran_frontend_mod_integer() - #test_fortran_frontend_modulo_float() - #test_fortran_frontend_modulo_integer() + test_fortran_frontend_min_max() + test_fortran_frontend_sqrt() + test_fortran_frontend_abs() + test_fortran_frontend_exp() + test_fortran_frontend_log() + test_fortran_frontend_mod_float() + test_fortran_frontend_mod_integer() + test_fortran_frontend_modulo_float() + test_fortran_frontend_modulo_integer() test_fortran_frontend_floor() + test_fortran_frontend_scale() From 451f91efae920118cf3a019fd8c53e2e2d9f5eca Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 17:59:17 +0100 Subject: [PATCH 06/20] Implement Fortran EXPONENT intrinsic --- dace/frontend/fortran/intrinsics.py | 1 + dace/runtime/include/dace/math.h | 8 ++++ tests/fortran/intrinsic_math.py | 63 ++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 9265176793..6d400eb41b 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -992,6 +992,7 @@ def generate_scale(arg: ast_internal_classes.Call_Expr_Node): "DOUBLE": MathTransformation("floor", "INTEGER") }, "SCALE": MathTransformation(generate_scale, "FIRST_ARG"), + "EXPONENT": MathTransformation("frexp", "INTEGER"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index cf633c9ffc..db7a43229e 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -86,6 +86,14 @@ static DACE_CONSTEXPR DACE_HDFI T Modulo_float(const T& value, const T& modulus) return value - floor(value / modulus) * modulus; } +// Implement to support a match wtih Fortran's intrinsic EXPONENT +template::value>* = nullptr> +static DACE_CONSTEXPR DACE_HDFI int frexp(const T& a) { + int exponent; + std::frexp(a, &exponent); + return exponent; +} + template static DACE_CONSTEXPR DACE_HDFI T int_ceil(const T& numerator, const T2& denominator) { return (numerator + denominator - 1) / denominator; diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index a1eb822364..b336170da1 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -500,16 +500,57 @@ def test_fortran_frontend_scale(): assert res[3] == 65280. assert res[4] == 11141120. +def test_fortran_frontend_exponent(): + test_string = """ + PROGRAM intrinsic_math_test_exponent + implicit none + real, dimension(4) :: d + integer, dimension(4) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + real, dimension(4) :: d + integer, dimension(4) :: res + + res(1) = EXPONENT(d(1)) + res(2) = EXPONENT(d(2)) + res(3) = EXPONENT(d(3)) + res(4) = EXPONENT(d(4)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 4 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 0.0 + d[1] = 1.0 + d[2] = 13.0 + d[3] = 390.0 + res = np.full([5], 42, order="F", dtype=np.int32) + sdfg(d=d, res=res) + + assert res[0] == 0 + assert res[1] == 1 + assert res[2] == 4 + assert res[3] == 9 + + if __name__ == "__main__": - test_fortran_frontend_min_max() - test_fortran_frontend_sqrt() - test_fortran_frontend_abs() - test_fortran_frontend_exp() - test_fortran_frontend_log() - test_fortran_frontend_mod_float() - test_fortran_frontend_mod_integer() - test_fortran_frontend_modulo_float() - test_fortran_frontend_modulo_integer() - test_fortran_frontend_floor() - test_fortran_frontend_scale() + #test_fortran_frontend_min_max() + #test_fortran_frontend_sqrt() + #test_fortran_frontend_abs() + #test_fortran_frontend_exp() + #test_fortran_frontend_log() + #test_fortran_frontend_mod_float() + #test_fortran_frontend_mod_integer() + #test_fortran_frontend_modulo_float() + #test_fortran_frontend_modulo_integer() + #test_fortran_frontend_floor() + #test_fortran_frontend_scale() + test_fortran_frontend_exponent() From 023ca16c44767aca7c7aaf08997a1950730e07e4 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 21:57:49 +0100 Subject: [PATCH 07/20] Implement Fortran INT/AINT intrinsics --- dace/frontend/fortran/intrinsics.py | 27 +++++++++++--- dace/runtime/include/dace/math.h | 2 +- tests/fortran/intrinsic_math.py | 57 +++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 6d400eb41b..94f5e48417 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -938,6 +938,7 @@ def _generate_loop_body(self, node: ast_internal_classes.FNode) -> ast_internal_ class MathFunctions(IntrinsicTransformation): MathTransformation = namedtuple("MathTransformation", "function return_type") + MathReplacement = namedtuple("MathReplacement", "function replacement_function return_type") def generate_scale(arg: ast_internal_classes.Call_Expr_Node): @@ -966,6 +967,17 @@ def generate_scale(arg: ast_internal_classes.Call_Expr_Node): # pack it into parentheses, just to be sure return ast_internal_classes.Parenthesis_Expr_Node(expr=mult) + def generate_aint(arg: ast_internal_classes.Call_Expr_Node): + + # The call to AINT can contain a second KIND parameter + # We ignore it a the moment. + # However, to map into C's trunc, we need to drop it. + if len(arg.args) > 1: + del arg.args[1] + arg.name = ast_internal_classes.Name_Node(name="trunc") + + return arg + INTRINSIC_TO_DACE = { "MIN": MathTransformation("min", "FIRST_ARG"), "MAX": MathTransformation("max", "FIRST_ARG"), @@ -991,8 +1003,10 @@ def generate_scale(arg: ast_internal_classes.Call_Expr_Node): "REAL": MathTransformation("floor", "INTEGER"), "DOUBLE": MathTransformation("floor", "INTEGER") }, - "SCALE": MathTransformation(generate_scale, "FIRST_ARG"), + "SCALE": MathReplacement(None, generate_scale, "FIRST_ARG"), "EXPONENT": MathTransformation("frexp", "INTEGER"), + "INT": MathTransformation("int", "INTEGER"), + "AINT": MathReplacement("trunc", generate_aint, "FIRST_ARG"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") @@ -1071,13 +1085,13 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Execution_Part_Node): else: return_type = replacement_rule.return_type - if isinstance(replacement_rule.function, str): + if isinstance(replacement_rule, MathFunctions.MathTransformation): call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) call.type = return_type return call else: - new_call = replacement_rule.function(call) + new_call = replacement_rule.replacement_function(call) return new_call #def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): @@ -1198,9 +1212,10 @@ def dace_functions(): # flatten nested lists for f in funcs: if isinstance(f, dict): - res.extend([v.function for k, v in f.items()]) + res.extend([v.function for k, v in f.items() if v.function is not None]) else: - res.append(f.function) + if f.function is not None: + res.append(f.function) return res @staticmethod @@ -1279,7 +1294,7 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod func_name = node.string replacements = { - "INT": "__dace_int", + #"INT": "__dace_int", "DBLE": "__dace_dble", "EPSILON": "__dace_epsilon", "SIGN": "__dace_sign", diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index db7a43229e..a9dcd5ee77 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -89,7 +89,7 @@ static DACE_CONSTEXPR DACE_HDFI T Modulo_float(const T& value, const T& modulus) // Implement to support a match wtih Fortran's intrinsic EXPONENT template::value>* = nullptr> static DACE_CONSTEXPR DACE_HDFI int frexp(const T& a) { - int exponent; + int exponent = 0; std::frexp(a, &exponent); return exponent; } diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index b336170da1..6f014c4c0d 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -491,8 +491,6 @@ def test_fortran_frontend_scale(): d2[3] = 9 res = np.full([5], 42, order="F", dtype=np.float32) sdfg(d=d, d2=d2, res=res) - print(d) - print(res) assert abs(res[0] - 0.570043862) < 10**-7 assert res[1] == 176. @@ -539,6 +537,58 @@ def test_fortran_frontend_exponent(): assert res[2] == 4 assert res[3] == 9 +def test_fortran_frontend_int(): + test_string = """ + PROGRAM intrinsic_math_test_int + implicit none + real, dimension(4) :: d + integer, dimension(4) :: res + real, dimension(4) :: res2 + CALL intrinsic_math_test_function(d, res, res2) + end + + SUBROUTINE intrinsic_math_test_function(d, res, res2) + real, dimension(4) :: d + integer, dimension(4) :: res + real, dimension(4) :: res2 + + res(1) = INT(d(1)) + res(2) = INT(d(2)) + res(3) = INT(d(3)) + res(4) = INT(d(4)) + + res2(1) = AINT(d(1)) + res2(2) = AINT(d(2)) + res2(3) = AINT(d(3)) + ! KIND parameter is ignored + res2(4) = AINT(d(4), 4) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 4 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 1.0 + d[1] = 1.5 + d[2] = 42.5 + d[3] = -42.5 + res = np.full([5], 42, order="F", dtype=np.int32) + res2 = np.full([5], 42, order="F", dtype=np.float32) + sdfg(d=d, res=res, res2=res2) + + assert res[0] == 1 + assert res[1] == 1 + assert res[2] == 42 + assert res[3] == -42 + + assert res2[0] == 1. + assert res2[1] == 1. + assert res2[2] == 42. + assert res2[3] == -42. if __name__ == "__main__": @@ -553,4 +603,5 @@ def test_fortran_frontend_exponent(): #test_fortran_frontend_modulo_integer() #test_fortran_frontend_floor() #test_fortran_frontend_scale() - test_fortran_frontend_exponent() + #test_fortran_frontend_exponent() + test_fortran_frontend_int() From 1088bf9c38c5a99f9971e6e8ac99226bb1c558ed Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 22:19:55 +0100 Subject: [PATCH 08/20] Implemnet Fortran NINT/NAINT intrinsics --- dace/frontend/fortran/intrinsics.py | 13 ++++- dace/runtime/include/dace/math.h | 6 +++ tests/fortran/intrinsic_math.py | 76 +++++++++++++++++++---------- 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 94f5e48417..aff1807ce6 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -974,7 +974,16 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): # However, to map into C's trunc, we need to drop it. if len(arg.args) > 1: del arg.args[1] - arg.name = ast_internal_classes.Name_Node(name="trunc") + + fname = arg.name.name.split('__dace_')[1] + if fname in "AINT": + arg.name = ast_internal_classes.Name_Node(name="trunc") + elif fname == "NINT": + arg.name = ast_internal_classes.Name_Node(name="round_to_int") + elif fname == "ANINT": + arg.name = ast_internal_classes.Name_Node(name="round") + else: + raise NotImplementedError() return arg @@ -1007,6 +1016,8 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): "EXPONENT": MathTransformation("frexp", "INTEGER"), "INT": MathTransformation("int", "INTEGER"), "AINT": MathReplacement("trunc", generate_aint, "FIRST_ARG"), + "NINT": MathReplacement("round_to_int", generate_aint, "INTEGER"), + "ANINT": MathReplacement("round", generate_aint, "FIRST_ARG"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index a9dcd5ee77..c560980706 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -94,6 +94,12 @@ static DACE_CONSTEXPR DACE_HDFI int frexp(const T& a) { return exponent; } +// Implement to support Fortran's intrinsic NINT - round, but return an integer +template::value>* = nullptr> +static DACE_CONSTEXPR DACE_HDFI int round_to_int(const T& a) { + return static_cast(round(a)); +} + template static DACE_CONSTEXPR DACE_HDFI T int_ceil(const T& numerator, const T2& denominator) { return (numerator + denominator - 1) / denominator; diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index 6f014c4c0d..70004ab8aa 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -542,15 +542,22 @@ def test_fortran_frontend_int(): PROGRAM intrinsic_math_test_int implicit none real, dimension(4) :: d + real, dimension(8) :: d2 integer, dimension(4) :: res real, dimension(4) :: res2 - CALL intrinsic_math_test_function(d, res, res2) + integer, dimension(8) :: res3 + real, dimension(8) :: res4 + CALL intrinsic_math_test_function(d, d2, res, res2, res3, res4) end - SUBROUTINE intrinsic_math_test_function(d, res, res2) + SUBROUTINE intrinsic_math_test_function(d, d2, res, res2, res3, res4) + integer :: n real, dimension(4) :: d + real, dimension(8) :: d2 integer, dimension(4) :: res real, dimension(4) :: res2 + integer, dimension(8) :: res3 + real, dimension(8) :: res4 res(1) = INT(d(1)) res(2) = INT(d(2)) @@ -563,6 +570,16 @@ def test_fortran_frontend_int(): ! KIND parameter is ignored res2(4) = AINT(d(4), 4) + DO n=1,8 + ! KIND parameter is ignored + res3(n) = NINT(d2(n), 4) + END DO + + DO n=1,8 + ! KIND parameter is ignored + res4(n) = ANINT(d2(n), 4) + END DO + END SUBROUTINE intrinsic_math_test_function """ @@ -576,32 +593,41 @@ def test_fortran_frontend_int(): d[1] = 1.5 d[2] = 42.5 d[3] = -42.5 - res = np.full([5], 42, order="F", dtype=np.int32) - res2 = np.full([5], 42, order="F", dtype=np.float32) - sdfg(d=d, res=res, res2=res2) + d2 = np.full([size*2], 42, order="F", dtype=np.float32) + d2[0] = 3.49 + d2[1] = 3.5 + d2[2] = 3.51 + d2[3] = 4 + d2[4] = -3.49 + d2[5] = -3.5 + d2[6] = -3.51 + d2[7] = -4 + res = np.full([4], 42, order="F", dtype=np.int32) + res2 = np.full([4], 42, order="F", dtype=np.float32) + res3 = np.full([8], 42, order="F", dtype=np.int32) + res4 = np.full([8], 42, order="F", dtype=np.float32) + sdfg(d=d, d2=d2, res=res, res2=res2, res3=res3, res4=res4) - assert res[0] == 1 - assert res[1] == 1 - assert res[2] == 42 - assert res[3] == -42 + assert np.array_equal(res, [1, 1, 42, -42]) + + assert np.array_equal(res2, [1., 1., 42., -42.]) + + assert np.array_equal(res3, [3, 4, 4, 4, -3, -4, -4, -4]) - assert res2[0] == 1. - assert res2[1] == 1. - assert res2[2] == 42. - assert res2[3] == -42. + assert np.array_equal(res4, [3., 4., 4., 4., -3., -4., -4., -4.]) if __name__ == "__main__": - #test_fortran_frontend_min_max() - #test_fortran_frontend_sqrt() - #test_fortran_frontend_abs() - #test_fortran_frontend_exp() - #test_fortran_frontend_log() - #test_fortran_frontend_mod_float() - #test_fortran_frontend_mod_integer() - #test_fortran_frontend_modulo_float() - #test_fortran_frontend_modulo_integer() - #test_fortran_frontend_floor() - #test_fortran_frontend_scale() - #test_fortran_frontend_exponent() + test_fortran_frontend_min_max() + test_fortran_frontend_sqrt() + test_fortran_frontend_abs() + test_fortran_frontend_exp() + test_fortran_frontend_log() + test_fortran_frontend_mod_float() + test_fortran_frontend_mod_integer() + test_fortran_frontend_modulo_float() + test_fortran_frontend_modulo_integer() + test_fortran_frontend_floor() + test_fortran_frontend_scale() + test_fortran_frontend_exponent() test_fortran_frontend_int() From 87501501b28637b18f22ad22e806d81e5ca6078c Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Fri, 24 Nov 2023 23:19:28 +0100 Subject: [PATCH 09/20] Implemnet Fortran DBLE/REAL intrinsics --- dace/frontend/fortran/intrinsics.py | 8 +--- tests/fortran/intrinsic_math.py | 60 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index aff1807ce6..ddd5c4115f 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -1018,6 +1018,8 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): "AINT": MathReplacement("trunc", generate_aint, "FIRST_ARG"), "NINT": MathReplacement("round_to_int", generate_aint, "INTEGER"), "ANINT": MathReplacement("round", generate_aint, "FIRST_ARG"), + "REAL": MathTransformation("float", "REAL"), + "DBLE": MathTransformation("double", "DOUBLE"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") @@ -1305,9 +1307,6 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod func_name = node.string replacements = { - #"INT": "__dace_int", - "DBLE": "__dace_dble", - "EPSILON": "__dace_epsilon", "SIGN": "__dace_sign", } if func_name in replacements: @@ -1329,9 +1328,6 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: ast_internal_classes.Arg_List_Node, line): func_types = { - "__dace_int": "INT", - "__dace_dble": "DOUBLE", - "__dace_epsilon": "DOUBLE", "__dace_sign": "DOUBLE", } if name.name in func_types: diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index 70004ab8aa..d2899240bb 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -616,6 +616,65 @@ def test_fortran_frontend_int(): assert np.array_equal(res4, [3., 4., 4., 4., -3., -4., -4., -4.]) +def test_fortran_frontend_real(): + test_string = """ + PROGRAM intrinsic_math_test_real + implicit none + double precision, dimension(2) :: d + real, dimension(2) :: d2 + integer, dimension(2) :: d3 + double precision, dimension(6) :: res + real, dimension(6) :: res2 + CALL intrinsic_math_test_function(d, d2, d3, res, res2) + end + + SUBROUTINE intrinsic_math_test_function(d, d2, d3, res, res2) + integer :: n + double precision, dimension(2) :: d + real, dimension(2) :: d2 + integer, dimension(2) :: d3 + double precision, dimension(6) :: res + real, dimension(6) :: res2 + + res(1) = DBLE(d(1)) + res(2) = DBLE(d(2)) + res(3) = DBLE(d2(1)) + res(4) = DBLE(d2(2)) + res(5) = DBLE(d3(1)) + res(6) = DBLE(d3(2)) + + res2(1) = REAL(d(1)) + res2(2) = REAL(d(2)) + res2(3) = REAL(d2(1)) + res2(4) = REAL(d2(2)) + res2(5) = REAL(d3(1)) + res2(6) = REAL(d3(2)) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 2 + d = np.full([size], 42, order="F", dtype=np.float64) + d[0] = 7.0 + d[1] = 13.11 + d2 = np.full([size], 42, order="F", dtype=np.float32) + d2[0] = 7.0 + d2[1] = 13.11 + d3 = np.full([size], 42, order="F", dtype=np.int32) + d3[0] = 7 + d3[1] = 13 + + res = np.full([size*3], 42, order="F", dtype=np.float64) + res2 = np.full([size*3], 42, order="F", dtype=np.float32) + sdfg(d=d, d2=d2, d3=d3, res=res, res2=res2) + + assert np.allclose(res, [7.0, 13.11, 7.0, 13.11, 7., 13.]) + assert np.allclose(res2, [7.0, 13.11, 7.0, 13.11, 7., 13.]) + if __name__ == "__main__": test_fortran_frontend_min_max() @@ -631,3 +690,4 @@ def test_fortran_frontend_int(): test_fortran_frontend_scale() test_fortran_frontend_exponent() test_fortran_frontend_int() + test_fortran_frontend_real() From 6fa1ec56eabb2be9ce3aac9bd5e5c805947758c8 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Sat, 25 Nov 2023 00:21:04 +0100 Subject: [PATCH 10/20] Implement Fortran trigonometric functions --- dace/frontend/fortran/intrinsics.py | 10 +- dace/runtime/include/dace/math.h | 2 +- tests/fortran/intrinsic_math.py | 152 ++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index ddd5c4115f..91d7b2fc36 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -979,7 +979,7 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): if fname in "AINT": arg.name = ast_internal_classes.Name_Node(name="trunc") elif fname == "NINT": - arg.name = ast_internal_classes.Name_Node(name="round_to_int") + arg.name = ast_internal_classes.Name_Node(name="iround") elif fname == "ANINT": arg.name = ast_internal_classes.Name_Node(name="round") else: @@ -1016,12 +1016,18 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): "EXPONENT": MathTransformation("frexp", "INTEGER"), "INT": MathTransformation("int", "INTEGER"), "AINT": MathReplacement("trunc", generate_aint, "FIRST_ARG"), - "NINT": MathReplacement("round_to_int", generate_aint, "INTEGER"), + "NINT": MathReplacement("iround", generate_aint, "INTEGER"), "ANINT": MathReplacement("round", generate_aint, "FIRST_ARG"), "REAL": MathTransformation("float", "REAL"), "DBLE": MathTransformation("double", "DOUBLE"), + "SIN": MathTransformation("sin", "FIRST_ARG"), + "COS": MathTransformation("cos", "FIRST_ARG"), + "SINH": MathTransformation("sinh", "FIRST_ARG"), "COSH": MathTransformation("cosh", "FIRST_ARG"), "TANH": MathTransformation("tanh", "FIRST_ARG"), + "ASIN": MathTransformation("asin", "FIRST_ARG"), + "ACOS": MathTransformation("acos", "FIRST_ARG"), + "ATAN": MathTransformation("atan", "FIRST_ARG"), "ATAN2": MathTransformation("atan2", "FIRST_ARG") } diff --git a/dace/runtime/include/dace/math.h b/dace/runtime/include/dace/math.h index c560980706..4dae494a8a 100644 --- a/dace/runtime/include/dace/math.h +++ b/dace/runtime/include/dace/math.h @@ -96,7 +96,7 @@ static DACE_CONSTEXPR DACE_HDFI int frexp(const T& a) { // Implement to support Fortran's intrinsic NINT - round, but return an integer template::value>* = nullptr> -static DACE_CONSTEXPR DACE_HDFI int round_to_int(const T& a) { +static DACE_CONSTEXPR DACE_HDFI int iround(const T& a) { return static_cast(round(a)); } diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math.py index d2899240bb..e1fc469beb 100644 --- a/tests/fortran/intrinsic_math.py +++ b/tests/fortran/intrinsic_math.py @@ -675,6 +675,155 @@ def test_fortran_frontend_real(): assert np.allclose(res, [7.0, 13.11, 7.0, 13.11, 7., 13.]) assert np.allclose(res2, [7.0, 13.11, 7.0, 13.11, 7., 13.]) +def test_fortran_frontend_trig(): + test_string = """ + PROGRAM intrinsic_math_test_trig + implicit none + real, dimension(3) :: d + real, dimension(6) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + integer :: n + real, dimension(3) :: d + real, dimension(6) :: res + + DO n=1,3 + res(n) = SIN(d(n)) + END DO + + DO n=1,3 + res(n+3) = COS(d(n)) + END DO + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 3 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 0 + d[1] = 3.14/2 + d[2] = 3.14 + + res = np.full([size*2], 42, order="F", dtype=np.float32) + sdfg(d=d, res=res) + + assert np.allclose(res, [0.0, 0.999999702, 1.59254798E-03, 1.0, 7.96274282E-04, -0.999998748]) + +def test_fortran_frontend_hyperbolic(): + test_string = """ + PROGRAM intrinsic_math_test_hyperbolic + implicit none + real, dimension(3) :: d + real, dimension(9) :: res + CALL intrinsic_math_test_function(d, res) + end + + SUBROUTINE intrinsic_math_test_function(d, res) + integer :: n + real, dimension(3) :: d + real, dimension(9) :: res + + DO n=1,3 + res(n) = SINH(d(n)) + END DO + + DO n=1,3 + res(n+3) = COSH(d(n)) + END DO + + DO n=1,3 + res(n+6) = TANH(d(n)) + END DO + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 3 + d = np.full([size], 42, order="F", dtype=np.float32) + d[0] = 0 + d[1] = 1 + d[2] = 3.14 + + res = np.full([size*3], 42, order="F", dtype=np.float32) + sdfg(d=d, res=res) + + assert np.allclose(res, [0.00000000, 1.17520118, 11.5302935, 1.00000000, 1.54308057, 11.5735760, 0.00000000, 0.761594176, 0.996260226]) + +def test_fortran_frontend_trig_inverse(): + test_string = """ + PROGRAM intrinsic_math_test_hyperbolic + implicit none + real, dimension(3) :: sincos_args + real, dimension(3) :: tan_args + real, dimension(6) :: tan2_args + real, dimension(12) :: res + CALL intrinsic_math_test_function(sincos_args, tan_args, tan2_args, res) + end + + SUBROUTINE intrinsic_math_test_function(sincos_args, tan_args, tan2_args, res) + integer :: n + real, dimension(3) :: sincos_args + real, dimension(3) :: tan_args + real, dimension(6) :: tan2_args + real, dimension(12) :: res + + DO n=1,3 + res(n) = ASIN(sincos_args(n)) + END DO + + DO n=1,3 + res(n+3) = ACOS(sincos_args(n)) + END DO + + DO n=1,3 + res(n+6) = ATAN(tan_args(n)) + END DO + + DO n=1,3 + res(n+9) = ATAN2(tan2_args(2*n - 1), tan2_args(2*n)) + END DO + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_modulo", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 3 + sincos_args = np.full([size], 42, order="F", dtype=np.float32) + sincos_args[0] = -0.5 + sincos_args[1] = 0.0 + sincos_args[2] = 1.0 + + atan_args = np.full([size], 42, order="F", dtype=np.float32) + atan_args[0] = 0.0 + atan_args[1] = 1.0 + atan_args[2] = 3.14 + + atan2_args = np.full([size*2], 42, order="F", dtype=np.float32) + atan2_args[0] = 0.0 + atan2_args[1] = 1.0 + atan2_args[2] = 1.0 + atan2_args[3] = 1.0 + atan2_args[4] = 1.0 + atan2_args[5] = 0.0 + + res = np.full([size*4], 42, order="F", dtype=np.float32) + sdfg(sincos_args=sincos_args, tan_args=atan_args, tan2_args=atan2_args, res=res) + + assert np.allclose(res, [-0.523598790, 0.00000000, 1.57079637, 2.09439516, 1.57079637, 0.00000000, 0.00000000, 0.785398185, 1.26248074, 0.00000000, 0.785398185, 1.57079637]) + if __name__ == "__main__": test_fortran_frontend_min_max() @@ -691,3 +840,6 @@ def test_fortran_frontend_real(): test_fortran_frontend_exponent() test_fortran_frontend_int() test_fortran_frontend_real() + test_fortran_frontend_trig() + test_fortran_frontend_hyperbolic() + test_fortran_frontend_trig_inverse() From 857c8a47fecb2ab040258bfbd2d516452fc49563 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Thu, 30 Nov 2023 14:33:14 +0100 Subject: [PATCH 11/20] Fix bug in CallExtractor's Fortran pass Previously, we didn't extract function calls that appear in function arguments. Now we can support that. To that end, we have to declare extracted calls in reverse order - first process arguments, then the main call. --- dace/frontend/fortran/ast_transforms.py | 19 ++++++++---- tests/fortran/call_extract.py | 39 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/fortran/call_extract.py diff --git a/dace/frontend/fortran/ast_transforms.py b/dace/frontend/fortran/ast_transforms.py index 71cd089e5f..57508d6d90 100644 --- a/dace/frontend/fortran/ast_transforms.py +++ b/dace/frontend/fortran/ast_transforms.py @@ -220,7 +220,7 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): from dace.frontend.fortran.intrinsics import FortranIntrinsics if not stop and node.name.name not in [ - "malloc", "pow", "cbrt", "atan2", "tanh", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions() + "malloc", "pow", "cbrt", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions() ]: self.nodes.append(node) return self.generic_visit(node) @@ -241,7 +241,7 @@ def __init__(self, count=0): def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): from dace.frontend.fortran.intrinsics import FortranIntrinsics - if node.name.name in ["malloc", "pow", "cbrt", "tanh", "atan2", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: + if node.name.name in ["malloc", "pow", "cbrt", "__dace_epsilon", *FortranIntrinsics.call_extraction_exemptions()]: return self.generic_visit(node) if hasattr(node, "subroutine"): if node.subroutine is True: @@ -251,6 +251,11 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): else: self.count = self.count + 1 tmp = self.count + + for i, arg in enumerate(node.args): + # Ensure we allow to extract function calls from arguments + node.args[i] = self.visit(arg) + return ast_internal_classes.Name_Node(name="tmp_call_" + str(tmp - 1)) def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): @@ -263,9 +268,13 @@ def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_No for i in res: if i == child: res.pop(res.index(i)) - temp = self.count if res is not None: - for i in range(0, len(res)): + # Variables are counted from 0...end, starting from main node, to all calls nested + # in main node arguments. + # However, we need to define nested ones first. + # We go in reverse order, counting from end-1 to 0. + temp = self.count + len(res) - 1 + for i in reversed(range(0, len(res))): newbody.append( ast_internal_classes.Decl_Stmt_Node(vardecl=[ @@ -282,7 +291,7 @@ def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_No type=res[i].type), rval=res[i], line_number=child.line_number)) - temp = temp + 1 + temp = temp - 1 if isinstance(child, ast_internal_classes.Call_Expr_Node): new_args = [] if hasattr(child, "args"): diff --git a/tests/fortran/call_extract.py b/tests/fortran/call_extract.py new file mode 100644 index 0000000000..eb1f2ac86d --- /dev/null +++ b/tests/fortran/call_extract.py @@ -0,0 +1,39 @@ +# Copyright 2019-2023 ETH Zurich and the DaCe authors. All rights reserved. + +import numpy as np +import pytest + +from dace.frontend.fortran import fortran_parser + +def test_fortran_frontend_call_extract(): + test_string = """ + PROGRAM intrinsic_call_extract + implicit none + real, dimension(2) :: d + real, dimension(2) :: res + CALL intrinsic_call_extract_test_function(d,res) + end + + SUBROUTINE intrinsic_call_extract_test_function(d,res) + real, dimension(2) :: d + real, dimension(2) :: res + + res(1) = SQRT(SIGN(EXP(d(1)), LOG(d(1)))) + res(2) = MIN(SQRT(EXP(d(1))), SQRT(EXP(d(1))) - 1) + + END SUBROUTINE intrinsic_call_extract_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_call_extract", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + input = np.full([2], 42, order="F", dtype=np.float32) + res = np.full([2], 42, order="F", dtype=np.float32) + sdfg(d=input, res=res) + assert np.allclose(res, [np.sqrt(np.exp(input[0])), np.sqrt(np.exp(input[0])) - 1]) + + +if __name__ == "__main__": + + test_fortran_frontend_call_extract() From aac534a717e12ee042a9800f27d8870bfbd93814 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Thu, 30 Nov 2023 14:52:58 +0100 Subject: [PATCH 12/20] Adapt Fortran intrinsics to the new CallExtractor --- dace/frontend/fortran/intrinsics.py | 275 ++++++++++------------------ 1 file changed, 95 insertions(+), 180 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 91d7b2fc36..b1b0235070 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -41,34 +41,71 @@ def initialize(self, ast): def func_name(self) -> str: pass -class SelectedKind(IntrinsicTransformation): +class DirectReplacement(IntrinsicTransformation): + + Replacement = namedtuple("Replacement", "function") + Transformation = namedtuple("Transformation", "function") + + class ASTTransformation(IntrinsicNodeTransformer): + pass + + + #self.scope_vars.get_var(node.parent, arg.name). + + def replace_bit_size(args: ast_internal_classes.Arg_List_Node, line): + + if len(args.args) != 1: + raise RuntimeError() + + def replace_int_kind(args: ast_internal_classes.Arg_List_Node, line): + return ast_internal_classes.Int_Literal_Node(value=str( + math.ceil((math.log2(math.pow(10, int(args.args[0].value))) + 1) / 8)), + line_number=line) + + def replace_real_kind(args: ast_internal_classes.Arg_List_Node, line): + if int(args.args[0].value) >= 9 or int(args.args[1].value) > 126: + return ast_internal_classes.Int_Literal_Node(value="8", line_number=line) + elif int(args.args[0].value) >= 3 or int(args.args[1].value) > 14: + return ast_internal_classes.Int_Literal_Node(value="4", line_number=line) + else: + return ast_internal_classes.Int_Literal_Node(value="2", line_number=line) + FUNCTIONS = { - "SELECTED_INT_KIND": "__dace_selected_int_kind", - "SELECTED_REAL_KIND": "__dace_selected_real_kind", + "SELECTED_INT_KIND": Replacement(replace_int_kind), + "SELECTED_REAL_KIND": Replacement(replace_real_kind), + "BIT_SIZE": Transformation(replace_bit_size) } @staticmethod - def replaced_name(func_name: str) -> str: - return SelectedKind.FUNCTIONS[func_name] + def replacable_name(func_name: str) -> bool: + return func_name in DirectReplacement.FUNCTIONS + + @staticmethod + def replace_name(func_name: str) -> str: + #return ast_internal_classes.Name_Node(name=DirectReplacement.FUNCTIONS[func_name][0]) + return ast_internal_classes.Name_Node(name=f'__dace_{func_name}') + + @staticmethod + def replacable(func_name: str) -> bool: + orig_name = func_name.split('__dace_') + if len(orig_name) > 1 and orig_name[1] in DirectReplacement.FUNCTIONS: + return isinstance(DirectReplacement.FUNCTIONS[orig_name[1]], DirectReplacement.Replacement) + return False @staticmethod def replace(func_name: ast_internal_classes.Name_Node, args: ast_internal_classes.Arg_List_Node, line) -> ast_internal_classes.FNode: - if func_name.name == "__dace_selected_int_kind": - return ast_internal_classes.Int_Literal_Node(value=str( - math.ceil((math.log2(math.pow(10, int(args.args[0].value))) + 1) / 8)), - line_number=line) - # This selects the smallest kind that can hold the given number of digits (fp64,fp32 or fp16) - elif func_name.name == "__dace_selected_real_kind": - if int(args.args[0].value) >= 9 or int(args.args[1].value) > 126: - return ast_internal_classes.Int_Literal_Node(value="8", line_number=line) - elif int(args.args[0].value) >= 3 or int(args.args[1].value) > 14: - return ast_internal_classes.Int_Literal_Node(value="4", line_number=line) - else: - return ast_internal_classes.Int_Literal_Node(value="2", line_number=line) + # Here we already have __dace_func + fname = func_name.split('__dace_')[1] + return DirectReplacement.FUNCTIONS[fname].function(args, line) + + def has_transformation() -> bool: + return isinstance(DirectReplacement.FUNCTIONS[fname], DirectReplacement.Transformation) - raise NotImplemented() + @staticmethod + def get_transformation(func_name: str) -> IntrinsicNodeTransformer: + return DirectReplacement.ASTTransformation(func_name) class LoopBasedReplacement: @@ -101,21 +138,6 @@ def __init__(self, func_name: str): self.nodes: List[ast_internal_classes.FNode] = [] self.calls: List[ast_internal_classes.FNode] = [] - #def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): - - # if isinstance(node.rval, ast_internal_classes.Call_Expr_Node): - # if node.rval.name.name == self._func_name: - # self.nodes.append(node) - - # self.visit(node.lval) - # self.visit(node.rval) - - #def visit_Parenthesis_Expr_Node(self, node: ast_internal_classes.BinOp_Node): - - # print('unop', node, dir(node), node.expr) - # if isinstance(node.expr, ast_internal_classes.Call_Expr_Node): - # self.visit(node.expr) - def visit_BinOp_Node(self, node: ast_internal_classes.BinOp_Node): if isinstance(node.rval, ast_internal_classes.Call_Expr_Node): if node.rval.name.name == self._func_name: @@ -128,7 +150,6 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Call_Expr_Node): if node.name.name == self._func_name: if node not in self.calls: - self.nodes.append(node) def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): @@ -1033,12 +1054,6 @@ def generate_aint(arg: ast_internal_classes.Call_Expr_Node): class TypeTransformer(IntrinsicNodeTransformer): - def __init__(self, func_name: str): - self._func_name = func_name - - def func_name(self) -> str: - return self._func_name - def func_type(self, node: ast_internal_classes.Call_Expr_Node): # take the first arg @@ -1049,6 +1064,9 @@ def func_type(self, node: ast_internal_classes.Call_Expr_Node): return 'INTEGER' elif isinstance(arg, ast_internal_classes.Call_Expr_Node): return arg.type + elif isinstance(arg, ast_internal_classes.Name_Node): + input_type = self.scope_vars.get_var(node.parent, arg.name) + return input_type.type else: input_type = self.scope_vars.get_var(node.parent, arg.name.name) return input_type.type @@ -1076,27 +1094,28 @@ def replace_call(self, old_call: ast_internal_classes.Call_Expr_Node, new_call: else: raise NotImplementedError() - def visit_Call_Expr_Node(self, node: ast_internal_classes.Execution_Part_Node): + def visit_BinOp_Node(self, binop_node: ast_internal_classes.BinOp_Node): - if node.name.name != f'__dace_{self.func_name()}': - return node + if not isinstance(binop_node.rval, ast_internal_classes.Call_Expr_Node): + return binop_node + + node = binop_node.rval + + name = node.name.name.split('__dace_') + if len(name) != 2 or name[1] not in MathFunctions.INTRINSIC_TO_DACE: + return binop_node + func_name = name[1] # Visit all children before we expand this call. # We need that to properly get the type. for arg in node.args: self.visit(arg) - call = node - return_type = None input_type = None - if isinstance(call, ast_internal_classes.BinOp_Node): - input_type = self.func_type(call.rval) - else: - input_type = self.func_type(call) - + input_type = self.func_type(node) - replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] + replacement_rule = MathFunctions.INTRINSIC_TO_DACE[func_name] if isinstance(replacement_rule, dict): replacement_rule = replacement_rule[input_type] if replacement_rule.return_type == "FIRST_ARG": @@ -1105,122 +1124,24 @@ def visit_Call_Expr_Node(self, node: ast_internal_classes.Execution_Part_Node): return_type = replacement_rule.return_type if isinstance(replacement_rule, MathFunctions.MathTransformation): - call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - call.type = return_type + node.name = ast_internal_classes.Name_Node(name=replacement_rule.function) + node.type = return_type - return call else: - new_call = replacement_rule.replacement_function(call) - return new_call - - #def visit_Execution_Part_Node(self, node: ast_internal_classes.Execution_Part_Node): - # newbody = [] - - # for child in node.execution: - # lister = LoopBasedReplacementVisitor(f'__dace_{self.func_name()}') - # lister.visit(child) - # res = lister.nodes - - # #newbody.append(self.visit(child)) - # if res is None or len(res) == 0: - # newbody.append(self.visit(child)) - # continue - - # for call in res: - - # return_type = None - # input_type = None - # if isinstance(call, ast_internal_classes.BinOp_Node): - # input_type = self.func_type(call.rval) - # else: - # input_type = self.func_type(call) - - - # replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] - # if isinstance(replacement_rule, dict): - # replacement_rule = replacement_rule[input_type] - # if replacement_rule.return_type == "FIRST_ARG": - # return_type = input_type - # else: - # return_type = replacement_rule.return_type - - - # if isinstance(call, ast_internal_classes.BinOp_Node): - - # if isinstance(replacement_rule.function, str): - # call.rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - # else: - # call.rval = replacement_rule.function(call.rval) - # call.rval.type = return_type - - # # replace types of return variable - # var = call.lval - # name = None - # if isinstance(var.name, ast_internal_classes.Name_Node): - # name = var.name.name - # else: - # name = var.name - # var_decl = self.scope_vars.get_var(var.parent, name) - # var.type = input_type - # var_decl.type = input_type - # else: - - # if isinstance(replacement_rule.function, str): - # call.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - # else: - # new_call = replacement_rule.function(call) - # self.replace_call(call, new_call) - # call.type = return_type - - # newbody.append(child) - - # #print(res) - # #if len(res) > 1: - # # raise NotImplementedError() - - # #print('VISIT') - # ## check the type of the function - # #input_type = self.func_type(res[0].rval) - # #new_args = [] - # #for i in res[0].rval.args: - # # print('child', i) - # # new_args.append(self.visit(i)) - - # #return_type = None - - # #replacement_rule = MathFunctions.INTRINSIC_TO_DACE[self.func_name()] - # #if isinstance(replacement_rule, dict): - # # replacement_rule = replacement_rule[input_type] - - # #if replacement_rule.return_type == "FIRST_ARG": - # # return_type = input_type - # #else: - # # return_type = replacement_rule.return_type - - # #new_call = ast_internal_classes.Call_Expr_Node(type=return_type, - # # name=ast_internal_classes.Name_Node(name=replacement_rule.function), - # # args=new_args, - # # line_number=res[0].rval.line_number) - - # ## replace function name - # ##res[0].rval.name = ast_internal_classes.Name_Node(name=replacement_rule.function) - - # #var = res[0].lval - # #print('type', input_type) - # #name = None - # #if isinstance(var.name, ast_internal_classes.Name_Node): - # # name = var.name.name - # #else: - # # name = var.name - # #var_decl = self.scope_vars.get_var(var.parent, name) - # #var.type = input_type - # #var_decl.type = input_type - - # #res[0].rval = new_call - - # #newbody.append(child) - - # return ast_internal_classes.Execution_Part_Node(execution=newbody) + binop_node.rval = replacement_rule.replacement_function(node) + + # replace types of return variable - LHS of the binary operator + var = binop_node.lval + name = None + if isinstance(var.name, ast_internal_classes.Name_Node): + name = var.name.name + else: + name = var.name + var_decl = self.scope_vars.get_var(var.parent, name) + var.type = input_type + var_decl.type = input_type + + return binop_node @staticmethod def dace_functions(): @@ -1258,14 +1179,12 @@ def has_transformation() -> bool: return True @staticmethod - def get_transformation(func_name: str) -> TypeTransformer: - return MathFunctions.TypeTransformer(func_name) + def get_transformation() -> TypeTransformer: + return MathFunctions.TypeTransformer() class FortranIntrinsics: IMPLEMENTATIONS_AST = { - "SELECTED_INT_KIND": SelectedKind, - "SELECTED_REAL_KIND": SelectedKind, "SUM": Sum, "PRODUCT": Product, "ANY": Any, @@ -1276,11 +1195,6 @@ class FortranIntrinsics: "MERGE": Merge } - DIRECT_REPLACEMENTS = { - "__dace_selected_int_kind": SelectedKind, - "__dace_selected_real_kind": SelectedKind - } - EXEMPTED_FROM_CALL_EXTRACTION = [ Merge ] @@ -1305,8 +1219,8 @@ def retained_function_names() -> List[str]: @staticmethod def call_extraction_exemptions() -> List[str]: return [ - *[func.Transformation.func_name() for func in FortranIntrinsics.EXEMPTED_FROM_CALL_EXTRACTION], - *MathFunctions.temporary_functions() + *[func.Transformation.func_name() for func in FortranIntrinsics.EXEMPTED_FROM_CALL_EXTRACTION] + #*MathFunctions.temporary_functions() ] def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Node: @@ -1317,9 +1231,10 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod } if func_name in replacements: return ast_internal_classes.Name_Node(name=replacements[func_name]) + elif DirectReplacement.replacable_name(func_name): + return DirectReplacement.replace_name(func_name) elif MathFunctions.replacable(func_name): - - self._transformations_to_run.add(MathFunctions.get_transformation(func_name)) + self._transformations_to_run.add(MathFunctions.get_transformation()) return MathFunctions.replace(func_name) if self.IMPLEMENTATIONS_AST[func_name].has_transformation(): @@ -1340,8 +1255,8 @@ def replace_function_reference(self, name: ast_internal_classes.Name_Node, args: # FIXME: this will be progressively removed call_type = func_types[name.name] return ast_internal_classes.Call_Expr_Node(name=name, type=call_type, args=args.args, line_number=line) - elif name.name in self.DIRECT_REPLACEMENTS: - return self.DIRECT_REPLACEMENTS[name.name].replace(name, args, line) + elif DirectReplacement.replacable(name.name): + return DirectReplacement.replace(name.name, args, line) else: # We will do the actual type replacement later # To that end, we need to know the input types - but these we do not know at the moment. From 81343e93a5ff46ab89fa1d006f704675b16923f7 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Wed, 6 Dec 2023 19:33:18 +0100 Subject: [PATCH 13/20] Implement SIZE intrinsic --- dace/frontend/fortran/intrinsics.py | 63 ++++++++++++++++++++++--- tests/fortran/intrinsic_basic.py | 73 +++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 tests/fortran/intrinsic_basic.py diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index b1b0235070..9364ba0934 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -47,11 +47,50 @@ class DirectReplacement(IntrinsicTransformation): Transformation = namedtuple("Transformation", "function") class ASTTransformation(IntrinsicNodeTransformer): - pass + + def visit_BinOp_Node(self, binop_node: ast_internal_classes.BinOp_Node): + + if not isinstance(binop_node.rval, ast_internal_classes.Call_Expr_Node): + return binop_node + + node = binop_node.rval + + name = node.name.name.split('__dace_') + if len(name) != 2 or name[1] not in DirectReplacement.FUNCTIONS: + return binop_node + func_name = name[1] + + replacement_rule = DirectReplacement.FUNCTIONS[func_name] + if isinstance(replacement_rule, DirectReplacement.Transformation): + + # FIXME: we do not have line number in binop? + binop_node.rval, input_type = replacement_rule.function(node, self.scope_vars, 0) #binop_node.line) + + # replace types of return variable - LHS of the binary operator + var = binop_node.lval + if isinstance(var.name, ast_internal_classes.Name_Node): + name = var.name.name + else: + name = var.name + var_decl = self.scope_vars.get_var(var.parent, name) + var.type = input_type + var_decl.type = input_type + + return binop_node #self.scope_vars.get_var(node.parent, arg.name). + def replace_size(var: ast_internal_classes.Call_Expr_Node, scope_vars: ScopeVarsDeclarations, line): + + if len(var.args) != 1: + raise RuntimeError() + + # get variable declaration for the first argument + var_decl = scope_vars.get_var(var.parent, var.args[0].name) + return (var_decl.sizes[0], "INTEGER") + + def replace_bit_size(args: ast_internal_classes.Arg_List_Node, line): if len(args.args) != 1: @@ -74,9 +113,19 @@ def replace_real_kind(args: ast_internal_classes.Arg_List_Node, line): FUNCTIONS = { "SELECTED_INT_KIND": Replacement(replace_int_kind), "SELECTED_REAL_KIND": Replacement(replace_real_kind), - "BIT_SIZE": Transformation(replace_bit_size) + "BIT_SIZE": Transformation(replace_bit_size), + "SIZE": Transformation(replace_size) } + @staticmethod + def temporary_functions(): + + # temporary functions created by us -> f becomes __dace_f + # We provide this to tell Fortran parser that these are function calls, + # not array accesses + funcs = list(DirectReplacement.FUNCTIONS.keys()) + return [f'__dace_{f}' for f in funcs] + @staticmethod def replacable_name(func_name: str) -> bool: return func_name in DirectReplacement.FUNCTIONS @@ -100,12 +149,12 @@ def replace(func_name: ast_internal_classes.Name_Node, args: ast_internal_classe fname = func_name.split('__dace_')[1] return DirectReplacement.FUNCTIONS[fname].function(args, line) - def has_transformation() -> bool: + def has_transformation(fname: str) -> bool: return isinstance(DirectReplacement.FUNCTIONS[fname], DirectReplacement.Transformation) @staticmethod - def get_transformation(func_name: str) -> IntrinsicNodeTransformer: - return DirectReplacement.ASTTransformation(func_name) + def get_transformation() -> IntrinsicNodeTransformer: + return DirectReplacement.ASTTransformation() class LoopBasedReplacement: @@ -1209,7 +1258,7 @@ def transformations(self) -> Set[Type[NodeTransformer]]: def function_names() -> List[str]: # list of all functions that are created by initial transformation, before doing full replacement # this prevents other parser components from replacing our function calls with array subscription nodes - return [*list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()), *MathFunctions.temporary_functions()] + return [*list(LoopBasedReplacement.INTRINSIC_TO_DACE.values()), *MathFunctions.temporary_functions(), *DirectReplacement.temporary_functions()] @staticmethod def retained_function_names() -> List[str]: @@ -1232,6 +1281,8 @@ def replace_function_name(self, node: FASTNode) -> ast_internal_classes.Name_Nod if func_name in replacements: return ast_internal_classes.Name_Node(name=replacements[func_name]) elif DirectReplacement.replacable_name(func_name): + if DirectReplacement.has_transformation(func_name): + self._transformations_to_run.add(DirectReplacement.get_transformation()) return DirectReplacement.replace_name(func_name) elif MathFunctions.replacable(func_name): self._transformations_to_run.add(MathFunctions.get_transformation()) diff --git a/tests/fortran/intrinsic_basic.py b/tests/fortran/intrinsic_basic.py new file mode 100644 index 0000000000..4cab05f8de --- /dev/null +++ b/tests/fortran/intrinsic_basic.py @@ -0,0 +1,73 @@ +# Copyright 2019-2023 ETH Zurich and the DaCe authors. All rights reserved. + +import numpy as np +import pytest + +from dace.frontend.fortran import fortran_parser + +def test_fortran_frontend_bit_size(): + test_string = """ + PROGRAM intrinsic_math_test_bit_size + implicit none + integer, dimension(4) :: res + CALL intrinsic_math_test_function(res) + end + + SUBROUTINE intrinsic_math_test_function(res) + integer, dimension(4) :: res + logical :: a = .TRUE. + integer :: b = 1 + real :: c = 1 + double precision :: d = 1 + + res(1) = BIT_SIZE(a) + res(2) = BIT_SIZE(b) + res(3) = BIT_SIZE(c) + res(4) = BIT_SIZE(d) + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_bit_size", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 4 + res1 = np.full([2], 42, order="F", dtype=np.int32) + sdfg(arg1=arg1, arg2=arg2, res1=res1, res2=res2) + print(res) + +def test_fortran_frontend_bit_size_symbolic(): + test_string = """ + PROGRAM intrinsic_math_test_bit_size + implicit none + integer :: arrsize = 2 + integer :: res(arrsize) + CALL intrinsic_math_test_function(arrsize, res) + end + + SUBROUTINE intrinsic_math_test_function(arrsize, res) + implicit none + integer :: arrsize + integer :: res(arrsize) + + res(1) = SIZE(res) + res(2) = SIZE(res)*2 + + END SUBROUTINE intrinsic_math_test_function + """ + + sdfg = fortran_parser.create_sdfg_from_string(test_string, "intrinsic_math_test_bit_size", False) + sdfg.simplify(verbose=True) + sdfg.compile() + + size = 24 + res = np.full([size], 42, order="F", dtype=np.int32) + sdfg(res=res, arrsize=size) + print(res) + + +if __name__ == "__main__": + + #test_fortran_frontend_bit_size() + test_fortran_frontend_bit_size_symbolic() From a72748a3a3b22b24ed5a7432fbb125010e2929fd Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Wed, 6 Dec 2023 22:50:26 +0100 Subject: [PATCH 14/20] Support DIM argument for SIZE intrinsic --- tests/fortran/intrinsic_basic.py | 35 +++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/fortran/intrinsic_basic.py b/tests/fortran/intrinsic_basic.py index 4cab05f8de..2bd0bbf895 100644 --- a/tests/fortran/intrinsic_basic.py +++ b/tests/fortran/intrinsic_basic.py @@ -41,18 +41,31 @@ def test_fortran_frontend_bit_size_symbolic(): test_string = """ PROGRAM intrinsic_math_test_bit_size implicit none - integer :: arrsize = 2 + integer :: arrsize + integer :: arrsize2 + integer :: arrsize3 integer :: res(arrsize) - CALL intrinsic_math_test_function(arrsize, res) + integer :: res2(arrsize, arrsize2, arrsize3) + integer :: res3(arrsize+arrsize2, arrsize2 * 5, arrsize3 + arrsize2*arrsize) + CALL intrinsic_math_test_function(arrsize, arrsize2, arrsize3, res, res2, res3) end - SUBROUTINE intrinsic_math_test_function(arrsize, res) + SUBROUTINE intrinsic_math_test_function(arrsize, arrsize2, arrsize3, res, res2, res3) implicit none integer :: arrsize + integer :: arrsize2 + integer :: arrsize3 integer :: res(arrsize) + integer :: res2(arrsize, arrsize2, arrsize3) + integer :: res3(arrsize+arrsize2, arrsize2 * 5, arrsize3 + arrsize2*arrsize) res(1) = SIZE(res) - res(2) = SIZE(res)*2 + res(2) = SIZE(res2) + res(3) = SIZE(res3) + res(4) = SIZE(res)*2 + res(5) = SIZE(res)*SIZE(res2)*SIZE(res3) + res(6) = SIZE(res2, 1) + SIZE(res2, 2) + SIZE(res2, 3) + res(7) = SIZE(res3, 1) + SIZE(res3, 2) + SIZE(res3, 3) END SUBROUTINE intrinsic_math_test_function """ @@ -62,10 +75,22 @@ def test_fortran_frontend_bit_size_symbolic(): sdfg.compile() size = 24 + size2 = 5 + size3 = 7 res = np.full([size], 42, order="F", dtype=np.int32) - sdfg(res=res, arrsize=size) + res2 = np.full([size, size2, size3], 42, order="F", dtype=np.int32) + res3 = np.full([size+size2, size2*5, size3 + size*size2], 42, order="F", dtype=np.int32) + sdfg(res=res, res2=res2, res3=res3, arrsize=size, arrsize2=size2, arrsize3=size3) print(res) + assert res[0] == size + assert res[1] == size*size2*size3 + assert res[2] == (size + size2) * (size2 * 5) * (size3 + size2*size) + assert res[3] == size * 2 + assert res[4] == res[0] * res[1] * res[2] + assert res[5] == size + size2 + size3 + assert res[6] == size + size2 + size2*5 + size3 + size*size2 + if __name__ == "__main__": From 0e7f743e13cd7428976ff7cdcfd2a106369c5241 Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Thu, 7 Dec 2023 08:11:04 +0100 Subject: [PATCH 15/20] Support DIM argument for SIZE intrinsic --- dace/frontend/fortran/intrinsics.py | 34 +++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 9364ba0934..8b217e42cd 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -83,12 +83,42 @@ def visit_BinOp_Node(self, binop_node: ast_internal_classes.BinOp_Node): def replace_size(var: ast_internal_classes.Call_Expr_Node, scope_vars: ScopeVarsDeclarations, line): - if len(var.args) != 1: + if len(var.args) not in [1, 2]: raise RuntimeError() # get variable declaration for the first argument var_decl = scope_vars.get_var(var.parent, var.args[0].name) - return (var_decl.sizes[0], "INTEGER") + + # one arg? compute the total number of elements + if len(var.args) == 1: + + if len(var_decl.sizes) == 1: + return (var_decl.sizes[0], "INTEGER") + + ret = ast_internal_classes.BinOp_Node( + lval=var_decl.sizes[0], + rval=None, + op="*" + ) + cur_node = ret + for i in range(1, len(var_decl.sizes) - 1): + + cur_node.rval = ast_internal_classes.BinOp_Node( + lval=var_decl.sizes[i], + rval=None, + op="*" + ) + cur_node = cur_node.rval + + cur_node.rval = var_decl.sizes[-1] + return (ret, "INTEGER") + + # we return number of elements in a given rank + rank = var.args[1] + if not isinstance(rank, ast_internal_classes.Int_Literal_Node): + raise NotImplementedError() + value = int(rank.value) + return (var_decl.sizes[value-1], "INTEGER") def replace_bit_size(args: ast_internal_classes.Arg_List_Node, line): From 62fb2d7ae82aae178071ae32e7ab130e2e2c4a2f Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Thu, 7 Dec 2023 08:54:49 +0100 Subject: [PATCH 16/20] Implement BIT_SIZE intrinsic --- dace/frontend/fortran/intrinsics.py | 19 +++++++++++++++---- tests/fortran/intrinsic_basic.py | 11 ++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/dace/frontend/fortran/intrinsics.py b/dace/frontend/fortran/intrinsics.py index 8b217e42cd..af44a8dfb5 100644 --- a/dace/frontend/fortran/intrinsics.py +++ b/dace/frontend/fortran/intrinsics.py @@ -65,6 +65,7 @@ def visit_BinOp_Node(self, binop_node: ast_internal_classes.BinOp_Node): # FIXME: we do not have line number in binop? binop_node.rval, input_type = replacement_rule.function(node, self.scope_vars, 0) #binop_node.line) + print(binop_node, binop_node.lval, binop_node.rval) # replace types of return variable - LHS of the binary operator var = binop_node.lval @@ -89,7 +90,7 @@ def replace_size(var: ast_internal_classes.Call_Expr_Node, scope_vars: ScopeVars # get variable declaration for the first argument var_decl = scope_vars.get_var(var.parent, var.args[0].name) - # one arg? compute the total number of elements + # one arg to SIZE? compute the total number of elements if len(var.args) == 1: if len(var_decl.sizes) == 1: @@ -113,19 +114,29 @@ def replace_size(var: ast_internal_classes.Call_Expr_Node, scope_vars: ScopeVars cur_node.rval = var_decl.sizes[-1] return (ret, "INTEGER") - # we return number of elements in a given rank + # two arguments? We return number of elements in a given rank rank = var.args[1] + # we do not support symbolic argument to DIM - it must be a literal if not isinstance(rank, ast_internal_classes.Int_Literal_Node): raise NotImplementedError() value = int(rank.value) return (var_decl.sizes[value-1], "INTEGER") - def replace_bit_size(args: ast_internal_classes.Arg_List_Node, line): + def replace_bit_size(var: ast_internal_classes.Call_Expr_Node, scope_vars: ScopeVarsDeclarations, line): - if len(args.args) != 1: + if len(var.args) != 1: raise RuntimeError() + # get variable declaration for the first argument + var_decl = scope_vars.get_var(var.parent, var.args[0].name) + + dace_type = fortrantypes2dacetypes[var_decl.type] + type_size = dace_type().itemsize * 8 + + return (ast_internal_classes.Int_Literal_Node(value=str(type_size)), "INTEGER") + + def replace_int_kind(args: ast_internal_classes.Arg_List_Node, line): return ast_internal_classes.Int_Literal_Node(value=str( math.ceil((math.log2(math.pow(10, int(args.args[0].value))) + 1) / 8)), diff --git a/tests/fortran/intrinsic_basic.py b/tests/fortran/intrinsic_basic.py index 2bd0bbf895..5392cf9148 100644 --- a/tests/fortran/intrinsic_basic.py +++ b/tests/fortran/intrinsic_basic.py @@ -33,9 +33,10 @@ def test_fortran_frontend_bit_size(): sdfg.compile() size = 4 - res1 = np.full([2], 42, order="F", dtype=np.int32) - sdfg(arg1=arg1, arg2=arg2, res1=res1, res2=res2) - print(res) + res = np.full([size], 42, order="F", dtype=np.int32) + sdfg(res=res) + + assert np.allclose(res, [32, 32, 32, 64]) def test_fortran_frontend_bit_size_symbolic(): test_string = """ @@ -94,5 +95,5 @@ def test_fortran_frontend_bit_size_symbolic(): if __name__ == "__main__": - #test_fortran_frontend_bit_size() - test_fortran_frontend_bit_size_symbolic() + test_fortran_frontend_bit_size() + #test_fortran_frontend_bit_size_symbolic() From 2e464d710b447e971928671fe9d70ad28aabcaef Mon Sep 17 00:00:00 2001 From: Marcin Copik Date: Thu, 7 Dec 2023 18:44:19 +0100 Subject: [PATCH 17/20] Fix handling of intrinsic transformations --- dace/frontend/fortran/fortran_parser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dace/frontend/fortran/fortran_parser.py b/dace/frontend/fortran/fortran_parser.py index 0388f94688..1cdecc99a8 100644 --- a/dace/frontend/fortran/fortran_parser.py +++ b/dace/frontend/fortran/fortran_parser.py @@ -1091,7 +1091,8 @@ def create_ast_from_string( program = ast_transforms.ArrayToLoop(program).visit(program) for transformation in own_ast.fortran_intrinsics().transformations(): - program = transformation(program).visit(program) + transformation.initialize(program) + program = transformation.visit(program) program = ast_transforms.ForDeclarer().visit(program) program = ast_transforms.IndexExtractor(program, normalize_offsets).visit(program) @@ -1174,7 +1175,8 @@ def create_sdfg_from_fortran_file(source_string: str, use_experimental_cfg_block program = ast_transforms.ArrayToLoop(program).visit(program) for transformation in own_ast.fortran_intrinsics(): - program = transformation(program).visit(program) + transformation.initialize(program) + program = transformation.visit(program) program = ast_transforms.ForDeclarer().visit(program) program = ast_transforms.IndexExtractor(program).visit(program) From ac89ecfe643fbdae81a11cbd9c5a6defc5cd21a0 Mon Sep 17 00:00:00 2001 From: Pratyai Mazumder Date: Tue, 5 Nov 2024 00:25:41 +0100 Subject: [PATCH 18/20] Rename the tests to get them on CI. --- tests/fortran/{call_extract.py => call_extract_test.py} | 0 tests/fortran/{intrinsic_basic.py => intrinsic_basic_test.py} | 0 tests/fortran/{intrinsic_math.py => intrinsic_math_test.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/fortran/{call_extract.py => call_extract_test.py} (100%) rename tests/fortran/{intrinsic_basic.py => intrinsic_basic_test.py} (100%) rename tests/fortran/{intrinsic_math.py => intrinsic_math_test.py} (100%) diff --git a/tests/fortran/call_extract.py b/tests/fortran/call_extract_test.py similarity index 100% rename from tests/fortran/call_extract.py rename to tests/fortran/call_extract_test.py diff --git a/tests/fortran/intrinsic_basic.py b/tests/fortran/intrinsic_basic_test.py similarity index 100% rename from tests/fortran/intrinsic_basic.py rename to tests/fortran/intrinsic_basic_test.py diff --git a/tests/fortran/intrinsic_math.py b/tests/fortran/intrinsic_math_test.py similarity index 100% rename from tests/fortran/intrinsic_math.py rename to tests/fortran/intrinsic_math_test.py From 3d2bf8747085c43e316e3f6cd36b6d6099ac6e54 Mon Sep 17 00:00:00 2001 From: Pratyai Mazumder Date: Tue, 5 Nov 2024 08:43:42 +0100 Subject: [PATCH 19/20] Make the program in `test_fortran_frontend_bit_size_symbolic()` a valid fortran program that passes under `gfortran -Wall`. --- tests/fortran/intrinsic_basic_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fortran/intrinsic_basic_test.py b/tests/fortran/intrinsic_basic_test.py index 5392cf9148..8e6adc4c15 100644 --- a/tests/fortran/intrinsic_basic_test.py +++ b/tests/fortran/intrinsic_basic_test.py @@ -42,9 +42,9 @@ def test_fortran_frontend_bit_size_symbolic(): test_string = """ PROGRAM intrinsic_math_test_bit_size implicit none - integer :: arrsize - integer :: arrsize2 - integer :: arrsize3 + integer, parameter :: arrsize = 2 + integer, parameter :: arrsize2 = 3 + integer, parameter :: arrsize3 = 4 integer :: res(arrsize) integer :: res2(arrsize, arrsize2, arrsize3) integer :: res3(arrsize+arrsize2, arrsize2 * 5, arrsize3 + arrsize2*arrsize) From 3488c58ecb689bdc3b2ada3c270b8c6b2a7b4b7d Mon Sep 17 00:00:00 2001 From: Pratyai Mazumder Date: Tue, 5 Nov 2024 13:38:20 +0100 Subject: [PATCH 20/20] Enable one test in `main()` that was accidentally disabled. --- tests/fortran/intrinsic_basic_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/fortran/intrinsic_basic_test.py b/tests/fortran/intrinsic_basic_test.py index 8e6adc4c15..9ef31dd108 100644 --- a/tests/fortran/intrinsic_basic_test.py +++ b/tests/fortran/intrinsic_basic_test.py @@ -94,6 +94,5 @@ def test_fortran_frontend_bit_size_symbolic(): if __name__ == "__main__": - test_fortran_frontend_bit_size() - #test_fortran_frontend_bit_size_symbolic() + test_fortran_frontend_bit_size_symbolic()