| #!/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 | |
| """common utilities and data structures for e2e matmul tests""" | |
| import enum | |
| import dataclasses | |
| import re | |
| import typing | |
| # Data type of matrix entries. The string values must match MLIR data types. | |
| # This is a superset of the values accepted for the --lhs_rhs_types= flag, | |
| # as this also includes accumulator-specific types like i32. | |
| @enum.unique | |
| class MatrixElemTypeId(enum.Enum): | |
| NONE = "" | |
| UI8 = "ui8" | |
| I8 = "i8" | |
| I32 = "i32" | |
| F64 = "f64" | |
| F32 = "f32" | |
| F16 = "f16" | |
| BF16 = "bf16" | |
| F8E5M2 = "f8E5M2" | |
| F8E4M3FN = "f8E4M3FN" | |
| F8E5M2FNUZ = "f8E5M2FNUZ" | |
| F8E4M3FNUZ = "f8E4M3FNUZ" | |
| F8E8M0FNU = "f8E8M0FNU" | |
| F6E3M2FN = "f6E3M2FN" | |
| F6E2M3FN = "f6E2M3FN" | |
| F4E2M1FN = "f4E2M1FN" | |
| def get_size_in_bits(type_id: MatrixElemTypeId): | |
| return int(re.search(r"\d+", str(type_id)).group()) | |
| # 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): | |
| DEFAULT = "default" | |
| SMALL = "small" | |
| LARGE = "large" | |
| EASY_LARGE_STATIC = "easy_large_static" | |
| CUSTOM_MNK = "custom_mnk" # Used for custom shapes specified by --mnk= flag. | |
| @classmethod | |
| def set_custom_mnk(cls, shapes_id, mnk_string): | |
| """Parse and set custom MNK values from command line argument.""" | |
| if shapes_id != cls.CUSTOM_MNK: | |
| if mnk_string: | |
| raise ValueError("--mnk can only be used with --shapes=custom_mnk") | |
| return | |
| # shapes_id is CUSTOM_MNK | |
| if not mnk_string: | |
| raise ValueError("--mnk must be specified when using --shapes=custom_mnk") | |
| try: | |
| mnk_parts = mnk_string.split(",") | |
| if len(mnk_parts) != 3: | |
| raise ValueError("--mnk must have exactly 3 values: m,n,k") | |
| cls.custom_mnk_values = tuple(int(x) for x in mnk_parts) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid --mnk format: {e}") | |
| @classmethod | |
| def set_dynamicity(cls, shapes_id, dynamicity_string): | |
| """Parse and set MNK dynamicity from command line argument.""" | |
| if shapes_id != cls.CUSTOM_MNK: | |
| if dynamicity_string: | |
| raise ValueError( | |
| "--mnk_dynamicities can only be used with --shapes=custom_mnk" | |
| ) | |
| return | |
| if dynamicity_string: | |
| try: | |
| dynamicity_parts = dynamicity_string.split(",") | |
| if len(dynamicity_parts) != 3: | |
| raise ValueError( | |
| "--mnk_dynamicities must have exactly 3 values, each being either 'dynamic' or 'static'" | |
| ) | |
| allowed_values = {"dynamic", "static"} | |
| for x in dynamicity_parts: | |
| if x.lower() not in allowed_values: | |
| raise ValueError(f"Invalid dynamicity value: {x}") | |
| cls.custom_dynamicity = tuple( | |
| Dynamicity.DYNAMIC if x.lower() == "dynamic" else Dynamicity.STATIC | |
| for x in dynamicity_parts | |
| ) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid --mnk_dynamicities format: {e}") | |
| # Class attribute to store custom MNK values | |
| ShapesId.custom_mnk_values = None | |
| # Class attribute to store custom dynamicities for MNK | |
| ShapesId.custom_dynamicity = None | |
| # 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.EASY_LARGE_STATIC: | |
| return [(Dynamicity.STATIC, Dynamicity.STATIC, Dynamicity.STATIC)] | |
| elif shapes_id.custom_dynamicity: | |
| return [shapes_id.custom_dynamicity] | |
| else: | |
| return [ | |
| (Dynamicity.DYNAMIC, Dynamicity.DYNAMIC, Dynamicity.DYNAMIC), | |
| (Dynamicity.STATIC, Dynamicity.STATIC, Dynamicity.STATIC), | |
| ] | |
| raise ValueError(shapes_id) | |
| # Enumerates ways to construct MLIR tensor types. | |
| @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 matrix buffer contents. | |
| @enum.unique | |
| class MatrixGenerator(enum.Enum): | |
| ZERO = "zero" # Fill with zeros | |
| RANDOM = "random" # Fill with (deterministic) pseudorandom values. | |
| # Describes the shape of a matrix multiplication in the usual convention: | |
| # the LHS is {m}x{k}, the RHS is {k}x{n}, the accumulator/result is {m}x{n}. | |
| # The extra `accumulate` boolean tells whether the matmul is accumulating into | |
| # an existing accumulator (C += A * B) or just overwriting the result | |
| # (C = A * B). | |
| @dataclasses.dataclass | |
| class TestShape: | |
| m: int | |
| k: int | |
| n: int | |
| accumulate: bool | |
| # 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. | |
| # util.func @somefunction_DYNxDYNxf32, where we can't use "?" characters. | |
| def int_or_DYN(s: DimSize): | |
| return s.value or "DYN" | |
| # Describes the fully resolved shape dimensions of all 3 input matrices, | |
| # LHS, RHS, and Accumulator, in a testcase. | |
| # Each value is a string, which may either represent a positive integer such as "123", | |
| # or a "?" string, meaning a dynamic dimension as in MLIR. | |
| # These string values are used to generate MLIR function names and tensor shapes. | |
| @dataclasses.dataclass | |
| class TestInputMatricesShapes: | |
| lhs_rows: DimSize | |
| lhs_cols: DimSize | |
| rhs_rows: DimSize | |
| rhs_cols: DimSize | |
| acc_rows: DimSize | |
| acc_cols: DimSize | |
| # Represents a generated test function. | |
| @dataclasses.dataclass | |
| class MLIRFunction: | |
| name: str | |
| signature: str | |
| import_declaration: str | |
| definition: str | |
| # Represents a call to a generated test function. | |
| @dataclasses.dataclass | |
| class TestCall: | |
| function: MLIRFunction | |
| op: str | |
| # Helper for generate_function. Generates TestInputMatricesShapes, i.e. | |
| # converts from the runtime shape dimensions in TestShape and given dynamicities(m,n,k) to | |
| # the set of shapes to be used in a test function's input tensors. | |
| def generate_shapes( | |
| shape: TestShape, | |
| transpose_rhs: bool, | |
| dynamicities: tuple[Dynamicity, Dynamicity, Dynamicity], | |
| ): | |
| dynamicity_m, dynamicity_n, dynamicity_k = dynamicities | |
| lhs_rows = shape_dim(shape.m, dynamicity_m) | |
| lhs_cols = shape_dim(shape.k, dynamicity_k) | |
| acc_rows = shape_dim(shape.m, dynamicity_m) | |
| acc_cols = shape_dim(shape.n, dynamicity_n) | |
| if transpose_rhs: | |
| rhs_rows = shape_dim(shape.n, dynamicity_n) | |
| rhs_cols = shape_dim(shape.k, dynamicity_k) | |
| else: | |
| rhs_rows = shape_dim(shape.k, dynamicity_k) | |
| rhs_cols = shape_dim(shape.n, dynamicity_n) | |
| shapes = TestInputMatricesShapes( | |
| lhs_rows=lhs_rows, | |
| lhs_cols=lhs_cols, | |
| rhs_rows=rhs_rows, | |
| rhs_cols=rhs_cols, | |
| acc_rows=acc_rows, | |
| acc_cols=acc_cols, | |
| ) | |
| return shapes | |
| random_matrix_seed = 0 | |
| # Generate a matrix function argument of the given size as `%name`. | |
| def generate_random_matrix( | |
| name: str, matrix_shape: list, element_type: MatrixElemTypeId, increment_seed=True | |
| ): | |
| global random_matrix_seed | |
| if increment_seed: | |
| random_matrix_seed += 1 | |
| return ( | |
| f" %{name}_dim0 = arith.constant {matrix_shape[0]} : i64\n" | |
| f" %{name}_dim1 = arith.constant {matrix_shape[1]} : i64\n" | |
| f" %{name}_element_type = hal.element_type<{element_type.value}> : i32\n" | |
| f" %{name}_seed = arith.constant {random_matrix_seed} : i32\n" | |
| f" %{name} = util.call @matmul_test.generate_random_matrix(%device, %{name}_dim0, %{name}_dim1, %{name}_element_type, %{name}_seed) : (!hal.device, i64, i64, i32, i32) -> !hal.buffer_view\n" | |
| ) |