| #!/usr/bin/env python3 | |
| # Copyright 2024 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 | |
| """Generator for e2e conv2d tests. | |
| """ | |
| import argparse | |
| import enum | |
| import dataclasses | |
| import typing | |
| import math | |
| # Data type of kernel entries. The string values must match MLIR data types. | |
| @enum.unique | |
| class KernelElemTypeId(enum.Enum): | |
| NONE = "" | |
| F32 = "f32" | |
| F16 = "f16" | |
| # Data type of input entries. The string values must match MLIR data types. | |
| @enum.unique | |
| class InputElemTypeId(enum.Enum): | |
| NONE = "" | |
| F32 = "f32" | |
| F16 = "f16" | |
| # Enumerates of the collections of shapes that we can generate tests for. | |
| # The values are the accepted values for the --shapes= flag. | |
| @enum.unique | |
| class ShapesId(enum.Enum): | |
| SMALL = "small" | |
| MEDIUM = "medium" | |
| LARGE = "large" | |
| # Enumerates ways to construct MLIR tensor types. | |
| # TODO: Enable dynamic dimensions once the tests start passing. | |
| @enum.unique | |
| class Dynamicity(enum.Enum): | |
| DYNAMIC = "dynamic" # Use '?' everywhere. Example: tensor<?x?xf32>. | |
| STATIC = "static" # Use fixed values everywhere. Example: tensor<4x6xf32>. | |
| MIXED = "mixed" # Randomly mix '?' and values. Example: tensor<?x4xf32>. | |
| # Enumerates ways to initialize input buffer contents. | |
| @enum.unique | |
| class InputGenerator(enum.Enum): | |
| ZERO = "zero" # Fill with zeros | |
| RANDOM = "random" # Fill with (deterministic) pseudorandom values. | |
| # Enumerates ways to initialize kernel buffer contents. | |
| @enum.unique | |
| class KernelGenerator(enum.Enum): | |
| ZERO = "zero" # Fill with zeros | |
| RANDOM = "random" # Fill with (deterministic) pseudorandom values. | |
| # TODO: Add more input layouts as needed. The layout determines the dim of input and kernel. | |
| @enum.unique | |
| class InputLayout(enum.Enum): | |
| NCHW = "nchw" | |
| NHWC = "nhwc" | |
| # TODO: Add more kernel layouts as needed. | |
| @enum.unique | |
| class KernelLayout(enum.Enum): | |
| FCHW = "fchw" | |
| HWCF = "hwcf" | |
| # Describes the shape of a tensor conv2d in the usual convention: | |
| # the input is {n}x{c}x{h}x{w}, the kernel is {f}x{c}x{kh}x{kw}, the accumulator/result is | |
| # {n}x{f}x{oh}x{ow}. | |
| # The extra `accumulate` boolean tells whether the conv2d is accumulating into | |
| # an existing accumulator (C += A * B) or just overwriting the result | |
| # (C = A * B). | |
| @dataclasses.dataclass | |
| class TestShape: | |
| n: int | |
| c: int | |
| h: int | |
| w: int | |
| kh: int | |
| kw: int | |
| f: int | |
| accumulate: bool | |
| # Attributes for the linalg.conv2d operation. | |
| @dataclasses.dataclass | |
| class ConvAttrs: | |
| STRIDE: typing.Tuple[int, int] = (1, 1) | |
| DILATION: typing.Tuple[int, int] = (1, 1) | |
| # Returns the list of TestShape's to use for the collection of shapes | |
| # identified by shapes_id. | |
| def get_test_shapes(shapes_id: ShapesId): | |
| # Notes: | |
| # 1. Be conservative in adding more shapes, as that can increase both the | |
| # build and execution latency of tests. The build latency is nearly the | |
| # same for all shapes, while execution latency grows linearly with | |
| # n*f*ow*oh*kh*kw. | |
| if shapes_id == ShapesId.SMALL: | |
| return [ | |
| TestShape(n=1, c=1, h=1, w=1, kh=1, kw=1, f=1, accumulate=True), | |
| TestShape(n=1, c=1, h=16, w=16, kh=2, kw=2, f=1, accumulate=True), | |
| TestShape(n=2, c=2, h=32, w=32, kh=3, kw=3, f=2, accumulate=True), | |
| ] | |
| if shapes_id == ShapesId.MEDIUM: | |
| return [ | |
| TestShape(n=2, h=32, w=32, c=32, kh=3, kw=3, f=64, accumulate=True), | |
| ] | |
| if shapes_id == ShapesId.LARGE: | |
| return [ | |
| TestShape(n=2, c=4, h=128, w=128, kh=3, kw=3, f=8, accumulate=True), | |
| TestShape(n=2, c=3, h=128, w=128, kh=3, kw=3, f=12, accumulate=True), | |
| ] | |
| raise ValueError(shapes_id) | |
| # Returns the list of Dynamicity's to use for the collection of shapes | |
| # identified by shapes_id. | |
| def get_dynamicities(shapes_id: ShapesId): | |
| if shapes_id == ShapesId.LARGE: | |
| return [ | |
| Dynamicity.STATIC, | |
| ] | |
| # TODO: Enable dynamic dimensions once the tests start passing. | |
| else: | |
| return [ | |
| Dynamicity.STATIC, | |
| ] | |
| raise ValueError(shapes_id) | |
| # Intentionally fixed seed! We want full reproducibility here, both across runs | |
| # and across machines. | |
| # Intentionally not shared with pseudorandom_generator_seed to limit the ways | |
| # in which shuffling testcases changes which random values are generated. | |
| local_pseudorandom_state = 1 | |
| # A shape dimension value, i.e. a size value that could appear in a MLIR type | |
| # such as 'tensor<?x4xf32>'. None means a dynamic size, similar to '?' in MLIR. | |
| @dataclasses.dataclass | |
| class DimSize: | |
| value: typing.Optional[int] | |
| # Generates a compile-time MLIR size value, i.e. either a fixed positive integer | |
| # or None (which maps to MLIR '?') depending on dynamicity. | |
| def shape_dim(x: int, dynamicity: Dynamicity): | |
| if dynamicity == Dynamicity.DYNAMIC: | |
| return DimSize(None) | |
| elif dynamicity == Dynamicity.STATIC: | |
| return DimSize(x) | |
| else: | |
| raise ValueError(dynamicity) | |
| # Stringification used for generating MLIR types, e.g. tensor<?x?xf32>. | |
| def int_or_question_mark(s: DimSize): | |
| return s.value or "?" | |
| # Stringification used for generating alphanumeric identifiers, e.g. | |
| # func.func @somefunction_DYNxDYNxf32, where we can't use "?" characters. | |
| def int_or_DYN(s: DimSize): | |
| return s.value or "DYN" | |
| # Determines the shape of input and kernel tensors. | |
| @dataclasses.dataclass | |
| class TestInputTensorShapes: | |
| n: DimSize | |
| c: DimSize | |
| h: DimSize | |
| w: DimSize | |
| kh: DimSize | |
| kw: DimSize | |
| f: DimSize | |
| # Helper for generate_function. Generates TestInputTensorShapes, i.e. | |
| # converts from the runtime shape dimensions in TestShape and given dynamicity to | |
| # the set of shapes to be used in a test function's input tensors. | |
| def generate_shapes(shape: TestShape, dynamicity: Dynamicity): | |
| n = shape_dim(shape.n, dynamicity) | |
| c = shape_dim(shape.c, dynamicity) | |
| h = shape_dim(shape.h, dynamicity) | |
| w = shape_dim(shape.w, dynamicity) | |
| kh = shape_dim(shape.kh, dynamicity) | |
| kw = shape_dim(shape.kw, dynamicity) | |
| f = shape_dim(shape.f, dynamicity) | |
| shapes = TestInputTensorShapes( | |
| n=n, | |
| c=c, | |
| h=h, | |
| w=w, | |
| kh=kh, | |
| kw=kw, | |
| f=f, | |
| ) | |
| return shapes | |
| # Helper to calculate the output shape based on the input shape, kernel shape, | |
| # dilation and stride. | |
| def calc_out_shape(i_shape: int, k_shape: int, dilation_val: int, stride_val: int): | |
| x = (k_shape - 1) * (dilation_val - 1) | |
| x = i_shape - k_shape - x | |
| return math.floor(x / stride_val) + 1 | |
| # Helper to return input, kernel and output shapes based on the layout and Conv2dParams. | |
| def get_tensor_shape( | |
| shapes: TestShape, | |
| kernel_layout: KernelLayout, | |
| input_layout: InputLayout, | |
| conv_attr: ConvAttrs, | |
| ): | |
| n = shapes.n | |
| c = shapes.c | |
| h = shapes.h | |
| w = shapes.w | |
| kh = shapes.kh | |
| kw = shapes.kw | |
| f = shapes.f | |
| # Extract input dimensions | |
| input_height, input_width = h, w | |
| # Extract kernel dimensions | |
| kernel_height, kernel_width = kh, kw | |
| # Get the dilation and stride | |
| dilation = conv_attr.DILATION | |
| stride = conv_attr.STRIDE | |
| # Calculate output height. | |
| oh = calc_out_shape(input_height, kernel_height, dilation[0], stride[0]) | |
| # Calculate output width. | |
| ow = calc_out_shape(input_width, kernel_width, dilation[1], stride[1]) | |
| input_tensor_shape, kernel_tensor_shape, output_tensor_shape = [], [], [] | |
| if input_layout == InputLayout.NCHW: | |
| input_tensor_shape = [n, c, h, w] | |
| output_tensor_shape = [n, f, oh, ow] | |
| elif input_layout == InputLayout.NHWC: | |
| input_tensor_shape = [n, h, w, c] | |
| output_tensor_shape = [n, oh, ow, f] | |
| else: | |
| raise ValueError(input_layout) | |
| if kernel_layout == KernelLayout.FCHW: | |
| kernel_tensor_shape = [f, c, kh, kw] | |
| elif kernel_layout == KernelLayout.HWCF: | |
| kernel_tensor_shape = [f, c, kh, kw] | |
| else: | |
| raise ValueError(kernel_layout) | |
| return input_tensor_shape, kernel_tensor_shape, output_tensor_shape | |
| # Helper for generate_function. | |
| # Generates a name for a test function in the generated MLIR code. | |
| def generate_function_name( | |
| input_type: InputElemTypeId, | |
| kernel_type: KernelElemTypeId, | |
| output_type: InputElemTypeId, | |
| shapes: TestInputTensorShapes, | |
| accumulate: bool, | |
| ): | |
| input_t = input_type.value | |
| kernel_t = kernel_type.value | |
| acc_t = output_type.value | |
| n = int_or_DYN(shapes.n) | |
| c = int_or_DYN(shapes.c) | |
| h = int_or_DYN(shapes.h) | |
| w = int_or_DYN(shapes.w) | |
| kh = int_or_DYN(shapes.kh) | |
| kw = int_or_DYN(shapes.kw) | |
| f = int_or_DYN(shapes.f) | |
| conv2d_kind = "conv2d_accumulate" if accumulate else "conv2d" | |
| return ( | |
| f"{conv2d_kind}_{n}_{c}_{h}_{w}_times_" | |
| + f"{kh}_{kw}_{f}_dtype_{input_t}_{kernel_t}_{acc_t}" | |
| ) | |
| # Represents a generated test function. | |
| @dataclasses.dataclass | |
| class MLIRFunction: | |
| name: str | |
| signature: str | |
| import_declaration: str | |
| definition: str | |
| # Generates a test function in the generated MLIR code. | |
| # The generated function will take the same arguments as linalg.conv2d variants | |
| # and will just call linalg.conv2d variants with them, returning its result. | |
| def generate_function( | |
| input_type: InputElemTypeId, | |
| input_layout: InputLayout, | |
| kernel_type: KernelElemTypeId, | |
| kernel_layout: KernelLayout, | |
| acc_type: InputElemTypeId, | |
| conv2d_attr: ConvAttrs, | |
| shape: TestShape, | |
| dynamicity: Dynamicity, | |
| ): | |
| shapes = generate_shapes(shape, dynamicity) | |
| func_name = generate_function_name( | |
| input_type, | |
| kernel_type, | |
| acc_type, | |
| shapes, | |
| shape.accumulate, | |
| ) | |
| input_shape, kernel_shape, output_shape = get_tensor_shape( | |
| shape, kernel_layout, input_layout, conv2d_attr | |
| ) | |
| input_tensor_type = f"tensor<{input_shape[0]}x{input_shape[1]}x{input_shape[2]}x{input_shape[3]}x{input_type.value}>" | |
| kernel_tensor_type = f"tensor<{kernel_shape[0]}x{kernel_shape[1]}x{kernel_shape[2]}x{kernel_shape[3]}x{kernel_type.value}>" | |
| acc_tensor_type = f"tensor<{output_shape[0]}x{output_shape[1]}x{output_shape[2]}x{output_shape[3]}x{input_type.value}>" | |
| op_name = None | |
| if input_layout == InputLayout.NCHW: | |
| if kernel_layout == KernelLayout.FCHW: | |
| op_name = "linalg.conv_2d_nchw_fchw" | |
| if kernel_layout == KernelLayout.HWCF: | |
| op_name = "linalg.conv_2d_nchw_hwcf" | |
| elif input_layout == InputLayout.NHWC: | |
| if kernel_layout == KernelLayout.HWCF: | |
| op_name = "linalg.conv_2d_nhwc_hwcf" | |
| conv_attr = f"{{dilations = dense<{list(conv2d_attr.DILATION)}> : tensor<2xi64>, strides = dense<{list(conv2d_attr.STRIDE)}> : tensor<2xi64>}}" | |
| # Compilation info is optional; prints empty string by default. | |
| func_definition = "" | |
| signature = f"({input_tensor_type}, {kernel_tensor_type}, {acc_tensor_type}) -> {acc_tensor_type}" | |
| import_declaration = f"func.func private @module.{func_name}(%input: !hal.buffer_view, %kernel: !hal.buffer_view, %acc: !hal.buffer_view) -> !hal.buffer_view" | |
| func_definition = func_definition + ( | |
| f"func.func @{func_name}(%lhs: {input_tensor_type}, %rhs: {kernel_tensor_type}, %acc: {acc_tensor_type}) -> {acc_tensor_type} {{\n" | |
| f" %result = {op_name} {conv_attr} ins(%lhs, %rhs: {input_tensor_type}, {kernel_tensor_type}) outs(%acc: {acc_tensor_type}) -> {acc_tensor_type}\n" | |
| f" return %result: {acc_tensor_type}\n" | |
| f"}}\n" | |
| ) | |
| return MLIRFunction( | |
| name=func_name, | |
| signature=signature, | |
| import_declaration=import_declaration, | |
| definition=func_definition, | |
| ) | |
| # Represents a call to a generated test function. | |
| @dataclasses.dataclass | |
| class TestCall: | |
| function: MLIRFunction | |
| op: str | |
| # Enumerates ways to initialize tensor buffer contents. | |
| @enum.unique | |
| class TensorGenerator(enum.Enum): | |
| ZERO = "zero" # Fill with zeros | |
| RANDOM = "random" # Fill with (deterministic) pseudorandom values. | |
| # Intentionally fixed seed! We want full reproducibility here, both across runs | |
| # and across machines. | |
| # Intentionally not shared with local_pseudorandom_state to limit the ways | |
| # in which shuffling testcases changes which random values are generated. | |
| pseudorandom_generator_seed = 1 | |
| def contents_generator_tag(generator: TensorGenerator): | |
| if generator == TensorGenerator.ZERO: | |
| return "" | |
| elif generator == TensorGenerator.RANDOM: | |
| global pseudorandom_generator_seed | |
| pseudorandom_generator_seed = pseudorandom_generator_seed + 1 | |
| return f"!tag:iree:fully_specified_pseudorandom {pseudorandom_generator_seed}" | |
| else: | |
| raise ValueError(generator) | |
| # Generate a 4d tensor function argument of the given size as `%name`. | |
| def generate_random_4d_tensor( | |
| name: str, | |
| tensor_shape: list, | |
| element_type: typing.Union[InputElemTypeId, KernelElemTypeId], | |
| ): | |
| global pseudorandom_generator_seed | |
| pseudorandom_generator_seed = pseudorandom_generator_seed + 1 | |
| return ( | |
| f" %{name}_dim0 = arith.constant {tensor_shape[0]} : i64\n" | |
| f" %{name}_dim1 = arith.constant {tensor_shape[1]} : i64\n" | |
| f" %{name}_dim2 = arith.constant {tensor_shape[2]} : i64\n" | |
| f" %{name}_dim3 = arith.constant {tensor_shape[3]} : i64\n" | |
| f" %{name}_element_type = hal.element_type<{element_type.value}> : i32\n" | |
| f" %{name}_seed = arith.constant {pseudorandom_generator_seed} : i32\n" | |
| f" %{name} = call @conv2d_test.generate_random_tensor(%device, %{name}_dim0, %{name}_dim1, %{name}_dim2, %{name}_dim3, %{name}_element_type, %{name}_seed) : (!hal.device, i64, i64, i64, i64, i32, i32) -> !hal.buffer_view\n" | |
| ) | |
| call_id = 0 | |
| def generate_call( | |
| function: MLIRFunction, | |
| input_type: InputElemTypeId, | |
| input_layout: InputLayout, | |
| kernel_type: KernelElemTypeId, | |
| kernel_layout: KernelLayout, | |
| conv2d_attr: ConvAttrs, | |
| acc_type: InputElemTypeId, | |
| shape: TestShape, | |
| ): | |
| global call_id | |
| func_name = f"{function.name}_{shape.n}_{shape.c}_{shape.h}_{shape.w}_{shape.f}_{shape.kh}_{shape.kw}" | |
| if shape.accumulate: | |
| func_name = f"{func_name}_acc" | |
| func_name = f"{func_name}_{call_id}" | |
| call_id = call_id + 1 | |
| description = f"Conv2d shape (NxCxHxWxFxKHxKW): {shape.n}x{shape.c}x{shape.h}x{shape.w}x{shape.f}x{shape.kh}x{shape.kw}" | |
| op = ( | |
| f"func.func @{func_name}() attributes {{\n" | |
| f' iree.reflection = {{description = "{description}"}}\n' | |
| "} {\n" | |
| " %device_index = arith.constant 0 : index\n" | |
| " %device = hal.devices.get %device_index : !hal.device\n" | |
| ) | |
| inp_shape, kernel_shape, out_shape = get_tensor_shape( | |
| shape, | |
| kernel_layout, | |
| input_layout, | |
| conv2d_attr, | |
| ) | |
| op = op + generate_random_4d_tensor("input", inp_shape, input_type) | |
| op = op + generate_random_4d_tensor("kernel", kernel_shape, kernel_type) | |
| if shape.accumulate: | |
| op = op + generate_random_4d_tensor("acc", out_shape, acc_type) | |
| # TODO(#16168): there's a bug with in-place input->output aliasing and | |
| # we work around it here by passing in a unique copy. | |
| global pseudorandom_generator_seed | |
| pseudorandom_generator_seed = pseudorandom_generator_seed - 1 | |
| op = op + generate_random_4d_tensor("acc_copy", out_shape, acc_type) | |
| op = op + ( | |
| f" %result = call @module.{function.name}(%input, %kernel, %acc_copy) : (!hal.buffer_view, !hal.buffer_view, !hal.buffer_view) -> !hal.buffer_view\n" | |
| ) | |
| else: | |
| op = op + ( | |
| f" %acc = util.null : !hal.buffer_view\n" | |
| f" %result = call @module.{function.name}(%input, %kernel) : (!hal.buffer_view, !hal.buffer_view) -> !hal.buffer_view\n" | |
| ) | |
| op = op + ( | |
| f" %n = arith.constant {shape.n} : i64\n" | |
| f" %c = arith.constant {shape.c} : i64\n" | |
| f" %h = arith.constant {shape.h} : i64\n" | |
| f" %w = arith.constant {shape.w} : i64\n" | |
| f" %f = arith.constant {shape.f} : i64\n" | |
| f" %kh = arith.constant {shape.kh} : i64\n" | |
| f" %kw = arith.constant {shape.kw} : i64\n" | |
| f" %sh = arith.constant {conv2d_attr.STRIDE[0]} : i64\n" | |
| f" %sw = arith.constant {conv2d_attr.STRIDE[1]} : i64\n" | |
| f" %dh = arith.constant {conv2d_attr.DILATION[0]} : i64\n" | |
| f" %dw = arith.constant {conv2d_attr.DILATION[1]} : i64\n" | |
| f" call @conv2d_test.check_conv2d_results(%device, %n, %c, %h, %w, %f, %kh, %kw, %sh, %sw, %dh, %dw, %input, %kernel, %acc, %result) : (!hal.device, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, !hal.buffer_view, !hal.buffer_view, !hal.buffer_view, !hal.buffer_view) -> ()\n" | |
| ) | |
| op = op + " return\n" | |
| op = op + "}\n" | |
| return TestCall(function=function, op=op) | |
| # Generates all output files' contents as strings. | |
| def generate( | |
| input_elem_type: InputElemTypeId, | |
| input_layout: InputLayout, | |
| kernel_elem_type: KernelElemTypeId, | |
| kernel_layout: KernelLayout, | |
| conv2d_attr: ConvAttrs, | |
| acc_type: InputElemTypeId, | |
| shapes_id: ShapesId, | |
| ): | |
| functions = {} | |
| calls = [] | |
| for shape in get_test_shapes(shapes_id): | |
| for dynamicity in get_dynamicities(shapes_id): | |
| function = generate_function( | |
| input_elem_type, | |
| input_layout, | |
| kernel_elem_type, | |
| kernel_layout, | |
| acc_type, | |
| conv2d_attr, | |
| shape, | |
| dynamicity, | |
| ) | |
| # Different testcases may differ only by runtime parameters but | |
| # share the same code. For example, dynamic-shapes testcases | |
| # share the same code involing tensor<?x?xf32> even though the runtime | |
| # value in the trace are different. That's why we append conditionally | |
| # to calls, but unconditionally to function_definitions. | |
| if function.name not in functions: | |
| functions[function.name] = function | |
| calls.append( | |
| generate_call( | |
| function, | |
| input_elem_type, | |
| input_layout, | |
| kernel_elem_type, | |
| kernel_layout, | |
| conv2d_attr, | |
| acc_type, | |
| shape, | |
| ) | |
| ) | |
| return (functions, calls) | |
| def parse_arguments(): | |
| parser = argparse.ArgumentParser(description="Generator of e2e conv2d tests") | |
| parser.add_argument( | |
| "--output_conv2d_mlir", | |
| type=str, | |
| help="Path of output .mlir file containing the generated conv2d functions", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--output_calls_mlir", | |
| type=str, | |
| help="Path of output .mlir file containing the calls", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--input_type", | |
| type=str, | |
| choices=["f32", "f16"], | |
| help="Numeric type of input tensors", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--input_layout", | |
| type=str, | |
| default="nchw", | |
| choices=["nchw", "nhwc"], | |
| help="Layout of the input tensor. Currently, only nchw is supported.", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--kernel_type", | |
| type=str, | |
| choices=["f32", "f16"], | |
| help="Numeric type of input tensors", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--kernel_layout", | |
| type=str, | |
| default="fchw", | |
| choices=["fchw", "hwcf"], | |
| help="Layout of kernel tensor. Currently, only fchw is supported.", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--acc_type", | |
| type=str, | |
| choices=["f32", "f16"], | |
| help="Numeric type of input tensors", | |
| default="", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--shapes", | |
| type=str, | |
| choices=[s.value for s in ShapesId], | |
| help="Collection of tensor shapes to test", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--dilation", | |
| type=str, | |
| default="1,1", | |
| help="The dilation factor for the convolution operation. Comma-separated. As in 1,1", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--stride", | |
| type=str, | |
| default="1,1", | |
| help="The stride factor for the convolution operation. Comma-separated. As in 1,1", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--requirements", | |
| type=str, | |
| help="Target requirements for this module. Comma-separated. As in -iree-llvmcpu-target-cpu-features. If the target device does not meet all of the requirements, the test will be skipped.", | |
| required=False, | |
| ) | |
| return parser.parse_args() | |
| def write_code_file(functions, filename): | |
| with open(filename, "w") as file: | |
| for function in functions.values(): | |
| file.write(function.definition + "\n") | |
| def write_calls_file(functions, calls, filename, requirements): | |
| # Module-level reflection information used to control the test tool. | |
| reflection = "" | |
| if requirements: | |
| reflection = ( | |
| "iree.reflection = {" | |
| 'target_features = "' | |
| + ",".join([req.lstrip("+") for req in requirements.split(",")]) | |
| + '"' | |
| "}" | |
| ) | |
| module_definition = ( | |
| f"builtin.module @calls attributes {{\n" f" {reflection}\n" f"}} {{\n\n" | |
| ) | |
| # Declare the custom module that generates arguments. | |
| module_definition = module_definition + ( | |
| "func.func private @conv2d_test.generate_random_tensor(%device: !hal.device, %dim0: i64, %dim1: i64, %dim2: i64, %dim3: i64, %element_type: i32, %seed: i32) -> !hal.buffer_view\n" | |
| "func.func private @conv2d_test.check_conv2d_results(%device: !hal.device, %n: i64, %c: i64, %h: i64, %w: i64, %f:i64, %kh:i64, %kw:i64, %sh:i64, %sw:i64, %dh:i64, %dw:i64, %input: !hal.buffer_view, %kernel: !hal.buffer_view, %acc: !hal.buffer_view, %actual_result: !hal.buffer_view)\n" | |
| "\n" | |
| ) | |
| # Declare the functions that will be called. | |
| for function in functions.values(): | |
| module_definition = module_definition + function.import_declaration + "\n" | |
| module_definition = module_definition + "\n" | |
| # Emit the test cases for each call. | |
| for call in calls: | |
| module_definition = module_definition + call.op + "\n" | |
| module_definition = module_definition + "\n}\n" | |
| with open(filename, "w") as file: | |
| file.write(module_definition) | |
| def main(args): | |
| input_type = InputElemTypeId(args.input_type) | |
| input_layout = InputLayout(args.input_layout) | |
| kernel_type = KernelElemTypeId(args.kernel_type) | |
| kernel_layout = KernelLayout(args.kernel_layout) | |
| # TODO: The output type is same as the input type for now. | |
| acc_type = input_type | |
| shapes_id = ShapesId(args.shapes) | |
| conv2d_attr = ConvAttrs( | |
| tuple(map(int, args.stride.split(","))), | |
| tuple(map(int, args.dilation.split(","))), | |
| ) | |
| (functions, calls) = generate( | |
| input_type, | |
| input_layout, | |
| kernel_type, | |
| kernel_layout, | |
| conv2d_attr, | |
| acc_type, | |
| shapes_id, | |
| ) | |
| write_code_file(functions, args.output_conv2d_mlir) | |
| write_calls_file( | |
| functions, | |
| calls, | |
| args.output_calls_mlir, | |
| args.requirements, | |
| ) | |
| if __name__ == "__main__": | |
| main(parse_arguments()) |