Files
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

579 lines
19 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=unrecognized-inline-option
"""Function data types."""
import collections
import inspect
from collections.abc import Callable, Mapping
from typing import Optional
import tvm_ffi
import tvm
import tvm.runtime
from tvm.ir import BaseFunc, Range
from tvm.runtime import Object, Scriptable
from ..runtime._tensor import Tensor
from . import _ffi_api
from .buffer import Buffer
from .expr import Expr, Var
@tvm_ffi.register_object("tirx.PrimFunc")
class PrimFunc(BaseFunc, Scriptable):
"""A function declaration expression.
Parameters
----------
params: List[Union[tvm.tirx.Var, tvm.tirx.Buffer]]
List of input parameters to the function.
body: tvm.tirx.Stmt
The body of the function.
ret_type: tvm.ir.Type
The return type annotation of the function.
buffer_map : Map[tvm.tirx.Var, tvm.tirx.Buffer]
The buffer binding map.
attrs: Optional[tvm.Attrs]
Attributes of the function, can be None
span : Optional[Span]
The location of this itervar in the source code.
"""
def __init__(self, params, body, ret_type=None, buffer_map=None, attrs=None, span=None):
# Legacy compatibility: expand body-carrying leaf stmt wrappers
# (e.g. DeclBuffer/AllocBuffer forms) into SeqStmt form.
from .stmt import _normalize_legacy_stmt
body = _normalize_legacy_stmt(body)
if ret_type is None:
ret_type = tvm.ir.Type.missing()
param_list = []
buffer_map = {} if buffer_map is None else buffer_map
for x in params:
x = tvm.runtime.convert(x) if not isinstance(x, Object) else x
if isinstance(x, Buffer):
var = Var(x.name, dtype="handle")
param_list.append(var)
buffer_map[var] = x
elif isinstance(x, Var):
param_list.append(x)
else:
raise TypeError("params can only contain Var or Buffer")
if attrs is None:
attrs = tvm.ir.make_node("ir.DictAttrs")
self.__init_handle_by_constructor__(
_ffi_api.PrimFunc,
param_list,
body,
ret_type,
buffer_map,
attrs,
span,
) # type: ignore
def with_body(self, new_body, span=None):
"""Create a new PrimFunc with the same set signatures but a new body.
Parameters
----------
new_body : Stmt
The new body.
span : Optional[Span]
The location of this itervar in the source code.
Returns
-------
new_func : PrimFunc
The created new function.
"""
return PrimFunc(
self.params,
new_body,
self.ret_type,
self.buffer_map,
self.attrs,
span,
)
def specialize(self, param_map: Mapping[Var, Expr | Buffer]):
"""Specialize parameters of PrimFunc
Parameters
----------
param_map : Mapping[Var, Union[Expr, Buffer]]
The mapping from function params to the instance
Examples
--------
We can define a Meta TIR function with symbolic shape:
.. code-block:: python
@T.prim_func(s_tir=True)
def mem_copy(a: T.handle, b: T.handle, m: T.int32, n: T.int32) -> None:
A = T.match_buffer(a, (m, n), "float32")
B = T.match_buffer(b, (m, n), "float32")
for i, j in T.grid(m, n):
with T.sblock():
vi, vj = T.axis.remap("SS", [i, j])
B[vi, vj] = A[vi, vj]
Then we can make it specialized with given shapes or buffers.
.. code-block:: python
a, _, m, n = mem_copy.params
func = mem_copy.specialize({a: tirx.decl_buffer((16, 16))})
# or
func = mem_copy.specialize({n: 16, m: 16})
The specialized function:
.. code-block:: python
@T.prim_func(s_tir=True)
def mem_copy_16_16(a: T.handle, b: T.handle) -> None:
A = T.match_buffer(a, (16, 16), "float32")
B = T.match_buffer(b, (16, 16), "float32")
for i, j in T.grid(16, 16):
with T.sblock():
vi, vj = T.axis.remap("SS", [i, j])
B[vi, vj] = A[vi, vj]
Returns
-------
func : PrimFunc
The new function with parameter specialized
"""
return _ffi_api.Specialize(self, param_map) # type: ignore
@tvm_ffi.register_object("tirx.TensorIntrin")
class TensorIntrin(Object):
"""A tensor intrinsic.
Parameters
----------
desc : PrimFunc
The function to describe the computation.
impl : PrimFunc
The function of the implementation for the execution.
"""
def __init__(self, desc, impl):
self.__init_handle_by_constructor__(_ffi_api.TensorIntrin, desc, impl)
@staticmethod
def register(name: str, desc: PrimFunc, impl: PrimFunc, override: bool = False):
"""Register a tensor intrinsic with its name.
Parameters
----------
name : str
The name of the TensorIntrin to register.
desc : PrimFunc
The function to describe the computation.
impl : PrimFunc
The function of the implementation for the execution.
override: bool
Whether override existing intrinsic.
"""
return _ffi_api.TensorIntrinRegister(name, TensorIntrin(desc, impl), override) # type: ignore
@staticmethod
def get(name: str, allow_missing: bool = False) -> Optional["TensorIntrin"]:
"""Look up a tensor intrinsic by its name.
Parameters
----------
name : str
The name of the TensorIntrin to look up.
allow_missing : bool
Whether to allow missing tensor intrin. If False, raise an error if the tensor intrin
doesn't exist.
Returns
-------
result : Optional[TensorIntrin]
The TensorIntrin with the specified name, or None if not found.
"""
return _ffi_api.TensorIntrinGet(name, allow_missing) # pylint: type: ignore
@tvm_ffi.register_object("tirx.IndexMap")
class IndexMap(Object):
"""A mapping from multi-dimensional indices to another set of multi-dimensional indices
Parameters
----------
initial_indices : List[Var]
Variables representing the indices prior to remapping.
final_indices : List[Expr]
Expressions defining the indices after remapping.
inverse_index_map : Union[Callable, Optional[IndexMap]]
The optional pre-defined inverse index map.
When this is defined, IndexMap::Inverse will return the pre-defined inverse index map.
Otherwise, the inverse index map will be computed on the fly.
It is the user's responsibility to ensure the correctness of the pre-defined inverse
index map.
"""
initial_indices: list[Var]
final_indices: list[Expr]
# Sentinel value used to indicate which groups of pre-flattening axes
# should be used to post-flattening axes axes. See
# Stage.transform_layout for more details.
AXIS_SEPARATOR = "axis_separator"
def __init__(self, initial_indices, final_indices, inverse_index_map):
if isinstance(inverse_index_map, Callable):
inverse_index_map = IndexMap.from_func(inverse_index_map)
self.__init_handle_by_constructor__(
_ffi_api.IndexMap, initial_indices, final_indices, inverse_index_map
)
@staticmethod
def from_func(
mapping_function: Callable,
ndim: int | None = None,
inverse_index_map: Callable | Optional["IndexMap"] = None,
*,
index_dtype: str = "int64",
):
"""Create an index map from a function
Parameters
----------
mapping_function : Callable
The function to map from source indices to target indices.
The function should accept `tirx.Var` parameters and return
a either a `tirx.Expr`, or a list of `tirx.Expr`.
Returning a `tirx.Expr` is equivalent to returning a
list of length 1 containing that `tirx.Expr`.
ndim: Optional[int]
The dimensionality of the buffer to which this
transformation should be applied. If mapping_function uses
variadic argument `*args`, `ndim` must be specified. If
mapping_function does not use variadic arguments, ndim is
optional.
inverse_index_map : Union[Callable, Optional[IndexMap]]
The optional pre-defined inverse index map.
When this is defined, IndexMap::Inverse will return the pre-defined inverse index map.
Otherwise, the inverse index map will be computed on the fly.
It is the user's responsibility to ensure the correctness of the pre-defined inverse
index map.
Returns
-------
index_map: IndexMap
Returns an IndexMap representing the `mapping_function`.
"""
index_map, axis_separators = IndexMap.from_func_with_separators(
mapping_function,
ndim,
inverse_index_map,
index_dtype=index_dtype,
)
assert not axis_separators, (
"The mapping_function provided to IndexMap.from_func "
"may not return IndexMap.AXIS_SEPARATOR. "
"If required, please use IndexMap.from_func_with_separators instead."
)
return index_map
@staticmethod
def from_func_with_separators(
mapping_function: Callable,
ndim: int | None = None,
inverse_index_map: Callable | Optional["IndexMap"] = None,
*,
index_dtype: str = "int64",
):
"""Create an index map from a function
Parameters
----------
mapping_function : Callable
The function to map from source indices to target indices.
The function should accept tirx.Var parameters and return
either a `tirx.Expr` or a list. Each element of the
returned list should be either a `tirx.Expr` or the
object `IndexMap.AXIS_SEPARATOR`. Returning a
`tirx.Expr` is equivalent to returning a list of length
1 containing that `tirx.Expr`.
ndim: Optional[int]
The dimensionality of the buffer to which this
transformation should be applied. If mapping_function uses
variadic argument `*args`, ndim must be specified. If
mapping_function does not use variadic arguments, ndim is
optional.
inverse_index_map : Union[Callable, Optional[IndexMap]]
The optional pre-defined inverse index map.
When this is defined, IndexMap::Inverse will return the pre-defined inverse index map.
Otherwise, the inverse index map will be computed on the fly.
It is the user's responsibility to ensure the correctness of the pre-defined inverse
index map.
index_dtype : str
The default index dtype to use for input iters in the mapping function.
Returns
-------
ret: Tuple[IndexMap, List[int]]
Returns a tuple whose first element is an IndexMap
representing the `mapping_function`, and whose second index
is a list of indices at which `IndexMap.AXIS_SEPARATOR`
occurred.
"""
params = inspect.signature(mapping_function).parameters
args = []
var_arg_name = None
kwargs = collections.OrderedDict()
for name, param in params.items():
if param.kind in [
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
]:
args.append(tvm.tirx.Var(name, index_dtype))
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
var_arg_name = name
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[name] = tvm.tirx.Var(name, index_dtype)
else:
raise ValueError("transform_layout mapping may not have *args")
# Now that all the named arguments have been collected,
# everything that remains should go to the *args, if
# specified.
if var_arg_name is not None:
assert ndim is not None, "ndim must be specified when *args is used"
num_var_args = ndim - len(args) - len(kwargs)
for i in range(num_var_args):
args.append(tvm.tirx.Var(f"{var_arg_name}_{i}", index_dtype))
mapping = mapping_function(*args, **kwargs)
initial_indices = args + list(kwargs.values())
final_indices = []
axis_separators = []
try:
iter(mapping)
is_iterable = True
except TypeError:
is_iterable = False
if is_iterable:
for val in mapping:
if tvm.ir.is_prim_expr(val):
final_indices.append(val)
elif val is IndexMap.AXIS_SEPARATOR:
axis_separators.append(len(final_indices))
else:
raise TypeError(
"Expected mapping function to return list of "
"either tvm.ir.Expr or IndexMap.AXIS_SEPARATOR. "
f"Instead received {val} of type {type(val)}."
)
else:
final_indices.append(mapping)
return IndexMap(initial_indices, final_indices, inverse_index_map), axis_separators
def is_equivalent_to(self, other_map: "IndexMap", analyzer=None) -> bool:
"""Return if the index maps are equivalent.
Parameters
----------
other_map: IndexMap
The IndexMap to which the comparison should be made.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use while comparing the mapped indices. When
provided, its accumulated bindings and constraints are reused so
that maps that are only equivalent under those bindings can be
proven equal.
Returns
-------
is_equivalent: bool
True if the two mappings represent the same
transformation, otherwise False
"""
if len(self.initial_indices) != len(other_map.initial_indices):
return False
if len(self.final_indices) != len(other_map.final_indices):
return False
if analyzer is None:
analyzer = tvm.arith.Analyzer()
mapped_other_final_indices = other_map.map_indices(self.initial_indices, analyzer=analyzer)
for self_index, other_index in zip(self.final_indices, mapped_other_final_indices):
if not analyzer.can_prove_equal(self_index, other_index):
return False
return True
def map_indices(self, indices: list[Expr], analyzer=None) -> list[Expr]:
"""Apply the index map to a set of indices
Parameters
----------
indices : List[Expr]
The indices to be mapped
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use while simplifying mapped indices.
Returns
-------
result : List[Expr]
The mapped indices
"""
return _ffi_api.IndexMapMapIndices(self, indices, analyzer)
def map_shape(self, shape: list[Expr], analyzer=None) -> list[Expr]:
"""Apply the index map to a buffer shape
Parameters
----------
shape : List[Expr]
The buffer shape to be mapped
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use while simplifying mapped shape expressions.
Returns
-------
result : List[Expr]
The mapped shape
"""
return _ffi_api.IndexMapMapShape(self, shape, analyzer)
def map_tensor(self, arr_src: Tensor) -> Tensor:
"""Apply thie index map to transform the layout of the input Tensor
Parameters
----------
arr_src : runtime.Tensor
The Tensor to be transformed
Returns
-------
arr_dst : runtime.Tensor
The transformed Tensor
"""
return _ffi_api.IndexMapMapTensor(self, arr_src)
def inverse(self, shape: list[Range | Expr], analyzer=None) -> "IndexMap":
"""Return the inverse of the map
Throws an error if the function is not bijective.
Parameters
----------
shape: List[Union[Range,Expr]]
The region over which the inverse should be determined.
Used for validating that the mapping is bijective over
this range.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use while deriving and validating the inverse.
Returns
-------
inverse : IndexMap
The inverse
"""
shape = [dim if isinstance(dim, Range) else Range(0, dim) for dim in shape]
return _ffi_api.IndexMapInverse(self, shape, analyzer)
def non_surjective_inverse(
self, shape: list[Range | Expr], analyzer=None
) -> tuple["IndexMap", Expr]:
"""Return the inverse of the map
Can be applied to transformations that introduce padding.
Parameters
----------
shape: List[Union[Range,Expr]]
The region over which the inverse should be determined.
Used for determining the predicate.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use while deriving the inverse and padding predicate.
Returns
-------
result : Tuple[IndexMap, Expr]
The inverse, and a predicate for which the inverse maps to
a valid index in the input range.
Examples
--------
.. code-block:: python
index_map = IndexMap.from_func(lambda i: [i//4, i%4])
inverse_map, predicate = index_map.non_surjective_inverse([14])
assert inverse_map.is_equivalent_to(IndexMap.from_func(lambda j,k: [4*j + k])
print(predicate) # Prints "(axis0==3) && (axis2 >= 2)"
"""
shape = [dim if isinstance(dim, Range) else Range(0, dim) for dim in shape]
return _ffi_api.IndexMapNonSurjectiveInverse(self, shape, analyzer)