| #!/usr/bin/env python3 | |
| # Copyright 2021 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 | |
| """iree_generated_e2e_matmul_test generator for e2e matmul tests.""" | |
| from typing import Optional | |
| import argparse | |
| import dataclasses | |
| import typing | |
| from tests.e2e.matmul.common import * | |
| from tests.e2e.matmul.compilation_info import * | |
| from tests.e2e.matmul.generate_code import * | |
| # Returns the list of TestShape's to use for the collection of shapes | |
| # identified by shapes_id. Also for custom tests, optionally removes tests with | |
| # an existing accumulator when accumulate is false. | |
| def get_test_shapes(shapes_id: ShapesId, accumulate=True): | |
| # 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 cubicly i.e. | |
| # linearly with m*k*n. | |
| # 2. Some shapes are commented out: they used to be tested but have been | |
| # disabled to improve the trade-off between test coverage and build | |
| # latency. | |
| if shapes_id == ShapesId.DEFAULT: | |
| return get_test_shapes(ShapesId.SMALL) + get_test_shapes(ShapesId.LARGE) | |
| if shapes_id == ShapesId.SMALL: | |
| return [ | |
| # square matrices. Start by the simplest case of 1x1x1. | |
| TestShape(m=1, k=1, n=1, accumulate=True), | |
| TestShape(m=1, k=1, n=1, accumulate=False), | |
| # Test some small powers of two, that exercise in particular the | |
| # adjustment of data-tiling tile sizes to narrow cases. | |
| TestShape(m=2, k=2, n=2, accumulate=True), | |
| TestShape(m=4, k=4, n=4, accumulate=True), | |
| TestShape(m=8, k=8, n=8, accumulate=True), | |
| # test 9x9x9 because as many kernel M0/K0/N0 dims are equal to 8, | |
| # this will often be the smallest value that exercises something above | |
| # the kernel's size. | |
| TestShape(m=9, k=9, n=9, accumulate=True), | |
| # rectangular matrices. | |
| # >= 2x differences between M/N/K dims may exercise tiling corner cases | |
| # not exercised by nearly-square matrices. | |
| TestShape(m=6, k=13, n=3, accumulate=True), | |
| TestShape(m=15, k=37, n=7, accumulate=False), | |
| TestShape(m=81, k=19, n=41, accumulate=True), | |
| # shapes involving vectors (i.e. most rectangular cases) | |
| # This is particularly relevant because we have dedicated kernels for | |
| # the matrix*vector / vector*matrix case. | |
| TestShape(m=1, k=10, n=10, accumulate=True), # vector*matrix | |
| TestShape(m=1, k=10, n=10, accumulate=False), # vector*matrix | |
| TestShape(m=10, k=1, n=10, accumulate=True), # outer-product | |
| TestShape(m=10, k=10, n=1, accumulate=True), # matrix*vector | |
| TestShape(m=10, k=10, n=1, accumulate=False), # matrix*vector | |
| ] | |
| if shapes_id == ShapesId.LARGE: | |
| return [ | |
| # unaligned cases. | |
| TestShape(m=457, k=330, n=512, accumulate=False), | |
| TestShape(m=438, k=331, n=513, accumulate=False), | |
| TestShape(m=540, k=332, n=516, accumulate=False), | |
| TestShape(m=1000, k=4, n=512, accumulate=False), | |
| TestShape(m=4, k=1000, n=512, accumulate=False), | |
| TestShape(m=512, k=1000, n=4, accumulate=False), | |
| TestShape(m=513, k=128, n=55, accumulate=False), | |
| TestShape(m=7, k=160, n=31, accumulate=False), | |
| TestShape(m=512, k=330, n=33, accumulate=False), | |
| # shapes involving vectors (i.e. most rectangular cases) | |
| TestShape(m=1, k=1000, n=1000, accumulate=True), # large vector*matrix | |
| TestShape(m=1000, k=1000, n=1, accumulate=True), # large matrix*vector | |
| TestShape(m=1000, k=1000, n=1, accumulate=False), # large matrix*vector | |
| # Be conservative in adding larger shapes. They can result in | |
| # high latency tests. If you have to, consider splitting them | |
| # out in a way that constrains the latency impact, e.g. by | |
| # running on fewer backends/drivers or with fewer generators | |
| # (see get_test_generators). | |
| ] | |
| if shapes_id == ShapesId.EASY_LARGE_STATIC: | |
| return [ | |
| TestShape(m=512, k=128, n=512, accumulate=True), | |
| TestShape(m=512, k=128, n=512, accumulate=False), | |
| ] | |
| if shapes_id == ShapesId.CUSTOM_MNK: | |
| # This is used for custom shapes specified by the --mnk= flag. | |
| # It is expected that the caller will set the m, n, k values | |
| # in the TestShape instances. | |
| if ShapesId.custom_mnk_values is None: | |
| raise ValueError("Custom MNK values not set. Use --mnk=m,n,k") | |
| m, n, k = ShapesId.custom_mnk_values | |
| test_shapes = [TestShape(m=m, k=k, n=n, accumulate=False)] | |
| if accumulate: | |
| test_shapes += [ | |
| TestShape(m=m, k=k, n=n, accumulate=True), | |
| ] | |
| return test_shapes | |
| raise ValueError(shapes_id) | |
| # Generates all output files' contents as strings. | |
| def generate( | |
| lhs_type: MatrixElemTypeId, | |
| rhs_type: MatrixElemTypeId, | |
| acc_type: MatrixElemTypeId, | |
| mx_scale_type: MatrixElemTypeId, | |
| mx_block_size: int, | |
| shapes_id: ShapesId, | |
| transpose_rhs: bool, | |
| compilation_info_id: CompilationInfoId, | |
| accumulate: bool, | |
| ): | |
| functions = {} | |
| calls = [] | |
| for compilation_info in get_test_compilation_infos( | |
| compilation_info_id, lhs_type, acc_type | |
| ): | |
| for shape in get_test_shapes(shapes_id, accumulate): | |
| for dynamicities in get_dynamicities(shapes_id): | |
| function = generate_function( | |
| lhs_type=lhs_type, | |
| rhs_type=rhs_type, | |
| acc_type=acc_type, | |
| mx_scale_type=mx_scale_type, | |
| mx_block_size=mx_block_size, | |
| shape=shape, | |
| transpose_rhs=transpose_rhs, | |
| dynamicities=dynamicities, | |
| compilation_info=compilation_info, | |
| ) | |
| # Different testcases may differ only by runtime parameters but | |
| # share the same code. For example, dynamic-shapes testcases | |
| # share the same code involving 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=function, | |
| lhs_type=lhs_type, | |
| rhs_type=rhs_type, | |
| acc_type=acc_type, | |
| mx_scale_type=mx_scale_type, | |
| mx_block_size=mx_block_size, | |
| shape=shape, | |
| transpose_rhs=transpose_rhs, | |
| ) | |
| ) | |
| return (functions, calls) | |
| def parse_arguments(): | |
| parser = argparse.ArgumentParser(description="Generator of e2e matmul tests") | |
| parser.add_argument( | |
| "--output_matmul_mlir", | |
| type=str, | |
| help="Path of output .mlir file containing the generated matmuls", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--output_calls_mlir", | |
| type=str, | |
| help="Path of output .mlir file containing the calls", | |
| required=True, | |
| ) | |
| elem_type_choices = [ | |
| "i32", | |
| "i8", | |
| "ui8", | |
| "f64", | |
| "f32", | |
| "f16", | |
| "bf16", | |
| "f8E5M2", | |
| "f8E4M3FN", | |
| "f8E5M2FNUZ", | |
| "f8E4M3FNUZ", | |
| "f6E3M2FN", | |
| "f6E2M3FN", | |
| "f4E2M1FN", | |
| ] | |
| # Legacy single-flag form: same type for LHS and RHS. Mutually exclusive | |
| # with the per-operand --lhs / --rhs pair below; exactly one set of flags | |
| # must be provided. | |
| parser.add_argument( | |
| "--lhs_rhs_type", | |
| type=str, | |
| choices=elem_type_choices, | |
| help="Numeric type of input LHS and RHS matrices (when both are equal)", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--lhs", | |
| type=str, | |
| choices=elem_type_choices, | |
| help="Numeric type of the LHS matrix. Use with --rhs.", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--rhs", | |
| type=str, | |
| choices=elem_type_choices, | |
| help="Numeric type of the RHS matrix. Use with --lhs.", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--acc_type", | |
| type=str, | |
| choices=["i32", "f64", "f32", "f16", "bf16"], | |
| help="Numeric type of the accumulator and result matrices", | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "--mx_scale_type", | |
| type=str, | |
| choices=[ | |
| "f8E8M0FNU", | |
| ], | |
| help="Numeric type of input microscaling scales matrices", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--mx_block_size", | |
| type=int, | |
| choices=[ | |
| 32, | |
| ], | |
| help="Numeric type of input microscaling scales matrices", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--shapes", | |
| type=str, | |
| choices=[s.value for s in ShapesId], | |
| help="Collection of matrix shapes to test", | |
| default="default", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--transpose_rhs", | |
| action="store_true", | |
| help="Whether to transpose RHS", | |
| default=False, | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--compilation_info", | |
| type=str, | |
| choices=[i.value for i in CompilationInfoId], | |
| help="Collection of compilation info setups to test", | |
| default="", | |
| 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, | |
| ) | |
| parser.add_argument( | |
| "--mnk", | |
| type=str, | |
| help="Custom MNK values for CUSTOM_MNK shape. Format: m,n,k (e.g., --mnk=64,128,256)", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--mnk_dynamicities", | |
| type=str, | |
| help="Custom dynamicity mask for m,n,k. Format: dynamic|static,dynamic|static,dynamic|static (e.g., --mnk_dynamicities=dynamic,static,static)", | |
| required=False, | |
| ) | |
| parser.add_argument( | |
| "--accumulate", | |
| action=argparse.BooleanOptionalAction, | |
| help="Remove/add custom shape tests with existing accumulators, useful to set for extremely large shapes that may cause memory issues", | |
| 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 + ( | |
| "util.func private @matmul_test.generate_random_matrix(%device: !hal.device, %dim0: i64, %dim1: i64, %element_type: i32, %seed: i32) -> !hal.buffer_view\n" | |
| "util.func private @matmul_test.check_matmul_results(%device: !hal.device, %m: i64, %k: i64, %n: i64, %transpose_rhs: i32, %lhs: !hal.buffer_view, %rhs: !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): | |
| # Parse custom MNK values if provided | |
| shapes_id = ShapesId(args.shapes) | |
| ShapesId.set_custom_mnk(shapes_id, args.mnk) | |
| ShapesId.set_dynamicity(shapes_id, args.mnk_dynamicities) | |
| # Resolve LHS/RHS types: exactly one of {--lhs_rhs_type} or {--lhs and --rhs}. | |
| lhs_rhs_set = args.lhs_rhs_type is not None | |
| per_op_set = args.lhs is not None or args.rhs is not None | |
| if lhs_rhs_set == per_op_set: | |
| raise ValueError( | |
| "specify exactly one of --lhs_rhs_type or the (--lhs, --rhs) pair" | |
| ) | |
| if per_op_set: | |
| if args.lhs is None or args.rhs is None: | |
| raise ValueError("--lhs and --rhs must be specified together") | |
| lhs_type = MatrixElemTypeId(args.lhs) | |
| rhs_type = MatrixElemTypeId(args.rhs) | |
| else: | |
| lhs_type = MatrixElemTypeId(args.lhs_rhs_type) | |
| rhs_type = lhs_type | |
| (functions, calls) = generate( | |
| lhs_type=lhs_type, | |
| rhs_type=rhs_type, | |
| acc_type=MatrixElemTypeId(args.acc_type), | |
| mx_scale_type=args.mx_scale_type, | |
| mx_block_size=args.mx_block_size, | |
| shapes_id=shapes_id, | |
| transpose_rhs=args.transpose_rhs, | |
| compilation_info_id=CompilationInfoId(args.compilation_info), | |
| accumulate=args.accumulate, | |
| ) | |
| write_code_file(functions, args.output_matmul_mlir) | |
| write_calls_file( | |
| functions, | |
| calls, | |
| args.output_calls_mlir, | |
| args.requirements, | |
| ) | |
| if __name__ == "__main__": | |
| main(parse_arguments()) |