| #!/usr/bin/env python3 | |
| # Copyright 2025 The IREE Authors | |
| # | |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| # See https://llvm.org/LICENSE.txt for license information. | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| """Generate e2e tests of math ops.""" | |
| import argparse | |
| import dataclasses | |
| import enum | |
| import math | |
| import json | |
| import sys | |
| import typing | |
| def parse_arguments(): | |
| parser = argparse.ArgumentParser(description="Generate e2e tests of math ops") | |
| parser.add_argument( | |
| "--testcases", | |
| type=str, | |
| help="JSON file listing the ops to be tested.", | |
| required=True, | |
| ) | |
| return parser.parse_args() | |
| def evaluate_unary_float_op(op, x): | |
| if op == "cbrt": | |
| return x ** (1.0 / 3) | |
| if op == "exp2": | |
| return 2.0**x | |
| if op == "roundeven": | |
| return float(round(x)) | |
| if op == "round": | |
| if 2.0 * x == round(2.0 * x): | |
| return math.copysign(math.ceil(math.fabs(x)), x) | |
| return float(round(x)) | |
| if op == "rsqrt": | |
| return x**-0.5 | |
| return float(eval(f"math.{op}({x})")) | |
| def evaluate_binary_float_op(op, x, y): | |
| if op == "powf" or op == "fpowi": | |
| return 0.0 if x == 0 else math.pow(x, y) | |
| return float(eval(f"math.{op}({x}, {y})")) | |
| def generate_unary_float_op(op, type, atol, rtol, input_values): | |
| output_values = [evaluate_unary_float_op(op, x) for x in input_values] | |
| size = len(input_values) | |
| print( | |
| f""" | |
| func.func @test_{op}_{type}() -> () {{ | |
| %input = util.unfoldable_constant dense<{input_values}> : tensor<{size}x{type}> | |
| %result_empty = tensor.empty() : tensor<{size}x{type}> | |
| %result = linalg.generic {{indexing_maps = [ | |
| affine_map<(d0) -> (d0)>, | |
| affine_map<(d0) -> (d0)> | |
| ], iterator_types = ["parallel"]}} | |
| ins(%input : tensor<{size}x{type}>) outs(%result_empty : tensor<{size}x{type}>) {{ | |
| ^bb0(%in: {type}, %out: {type}): | |
| %result = math.{op} %in : {type} | |
| linalg.yield %result : {type} | |
| }} -> tensor<{size}x{type}> | |
| check.expect_almost_eq_const(%result, | |
| dense<{output_values}> : tensor<{size}x{type}>, | |
| atol {float(atol):.3e}, rtol {float(rtol):.3e}) : tensor<{size}x{type}> | |
| return | |
| }}""" | |
| ) | |
| def generate_binary_float_op(op, type, atol, rtol, input_values): | |
| input_values_x = [x for x, _ in input_values] | |
| input_values_y = [y for _, y in input_values] | |
| output_values = [evaluate_binary_float_op(op, x, y) for x, y in input_values] | |
| size = len(input_values) | |
| print( | |
| f""" | |
| func.func @test_{op}_{type}() -> () {{ | |
| %input_x = util.unfoldable_constant dense<{input_values_x}> : tensor<{size}x{type}> | |
| %input_y = util.unfoldable_constant dense<{input_values_y}> : tensor<{size}x{type}> | |
| %result_empty = tensor.empty() : tensor<{size}x{type}> | |
| %result = linalg.generic {{indexing_maps = [ | |
| affine_map<(d0) -> (d0)>, | |
| affine_map<(d0) -> (d0)>, | |
| affine_map<(d0) -> (d0)> | |
| ], iterator_types = ["parallel"]}} | |
| ins(%input_x, %input_y : tensor<{size}x{type}>, tensor<{size}x{type}>) outs(%result_empty : tensor<{size}x{type}>) {{ | |
| ^bb0(%in_x: {type}, %in_y : {type}, %out: {type}): | |
| %result = math.{op} %in_x, %in_y : {type} | |
| linalg.yield %result : {type} | |
| }} -> tensor<{size}x{type}> | |
| check.expect_almost_eq_const(%result, | |
| dense<{output_values}> : tensor<{size}x{type}>, | |
| atol {float(atol):.3e}, rtol {float(rtol):.3e}) : tensor<{size}x{type}> | |
| return | |
| }}""" | |
| ) | |
| def generate_unary_float_input_values(predicate): | |
| # Some functions like math.round and math.roundeven care | |
| # specifically about half-integral values. To ensure coverage of | |
| # half-integral cases, unit_subdivisions should be even. | |
| # Note that the correctness of the logic here rests on exact representability | |
| # of small integer values scaled by small powers of two. | |
| unit_subdivisions = 4 | |
| float_range_radius = 16 | |
| index_range_radius = int(float_range_radius * unit_subdivisions) | |
| values = [] | |
| for x in range(-index_range_radius, index_range_radius): | |
| val = float(x) / unit_subdivisions | |
| if predicate(val): | |
| values.append(val) | |
| return values | |
| def generate_binary_float_input_values(predicate): | |
| # Be conservative due to quadratic growth. | |
| unit_subdivisions = 4 | |
| float_range_radius = 4 | |
| index_range_radius = int(float_range_radius * unit_subdivisions) | |
| values = [] | |
| for x in range(-index_range_radius, index_range_radius): | |
| val_x = float(x) / unit_subdivisions | |
| for y in range(-index_range_radius, index_range_radius): | |
| val_y = float(y) / unit_subdivisions | |
| if predicate(val_x, val_y): | |
| values.append((val_x, val_y)) | |
| return values | |
| @enum.unique | |
| class MathOpKind(enum.Enum): | |
| UNARY_FLOAT = 1 | |
| BINARY_FLOAT = 2 | |
| @dataclasses.dataclass | |
| class MathOpInfo: | |
| kind: MathOpKind | |
| domain: typing.Callable | |
| def get_math_op_info(): | |
| return { | |
| "acos": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: -1 <= x <= 1, | |
| ), | |
| "acosh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x >= 1, | |
| ), | |
| "asin": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: -1 <= x <= 1, | |
| ), | |
| "asinh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "atan": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "atan2": MathOpInfo( | |
| kind=MathOpKind.BINARY_FLOAT, | |
| domain=lambda x, y: x != 0 or y != 0, | |
| ), | |
| "atanh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: -1 < x < 1, | |
| ), | |
| "cbrt": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x >= 0, | |
| ), | |
| "ceil": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "cos": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "cosh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "erf": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "erfc": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "exp": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "exp2": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "expm1": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "floor": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "log": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > 0, | |
| ), | |
| "log10": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > 0, | |
| ), | |
| "log1p": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > -1, | |
| ), | |
| "log2": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > 0, | |
| ), | |
| "powf": MathOpInfo( | |
| kind=MathOpKind.BINARY_FLOAT, | |
| domain=lambda x, y: x > 0, | |
| ), | |
| "round": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "roundeven": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "rsqrt": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > 0, | |
| ), | |
| "sin": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "sinh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "sqrt": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: x > 0, | |
| ), | |
| "tan": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "tanh": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| "trunc": MathOpInfo( | |
| kind=MathOpKind.UNARY_FLOAT, | |
| domain=lambda x: True, | |
| ), | |
| } | |
| def main(args): | |
| with open(args.testcases, "r") as testcases_file: | |
| testcases = json.load(testcases_file) | |
| math_op_info = get_math_op_info() | |
| ops_not_yet_encountered = {op for op in math_op_info} | |
| for testcase in testcases: | |
| op = testcase["op"] | |
| ops_not_yet_encountered.discard(op) | |
| if "disabled" in testcase: | |
| continue | |
| info = math_op_info[op] | |
| kind = info.kind | |
| type = testcase["type"] | |
| atol = testcase["atol"] | |
| rtol = testcase["rtol"] | |
| if kind == MathOpKind.UNARY_FLOAT: | |
| # Combine the op's inherent domain (info.domain) with optional | |
| # testcase-specific restrictions. | |
| domain = lambda x: ( | |
| info.domain(x) | |
| and (x >= testcase["xmin"] if "xmin" in testcase else True) | |
| and (x <= testcase["xmax"] if "xmax" in testcase else True) | |
| ) | |
| input_values = generate_unary_float_input_values(domain) | |
| generate_unary_float_op(op, type, atol, rtol, input_values) | |
| elif kind == MathOpKind.BINARY_FLOAT: | |
| # Combine the op's inherent domain (info.domain) with optional | |
| # testcase-specific restrictions. | |
| domain = lambda x, y: ( | |
| info.domain(x, y) | |
| and (x >= testcase["xmin"] if "xmin" in testcase else True) | |
| and (x <= testcase["xmax"] if "xmax" in testcase else True) | |
| and (y >= testcase["ymin"] if "ymin" in testcase else True) | |
| and (y <= testcase["ymax"] if "ymax" in testcase else True) | |
| ) | |
| input_values = generate_binary_float_input_values(domain) | |
| generate_binary_float_op(op, type, atol, rtol, input_values) | |
| else: | |
| raise ValueError(f"Unhandled op kind: {info.kind}") | |
| if ops_not_yet_encountered: | |
| print( | |
| f"Warning: did not find testcases covering {', '.join(ops_not_yet_encountered)} in {args.testcases}", | |
| file=sys.stderr, | |
| ) | |
| if __name__ == "__main__": | |
| main(parse_arguments()) |