578 lines
20 KiB
Python
578 lines
20 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.
|
|
"""Abstraction for array data structures."""
|
|
|
|
import functools
|
|
from numbers import Integral
|
|
|
|
import tvm_ffi
|
|
|
|
import tvm
|
|
from tvm.ir import PointerType, PrimType, Range
|
|
from tvm.runtime import Object, Scriptable, convert
|
|
|
|
from . import _ffi_api
|
|
|
|
|
|
@tvm_ffi.register_object("tirx.Buffer")
|
|
class Buffer(Object, Scriptable):
|
|
"""Symbolic data buffer in TVM.
|
|
|
|
Buffer provide a way to represent data layout
|
|
specialization of data structure in TVM.
|
|
|
|
Do not construct directly, use :py:func:`~decl_buffer` instead.
|
|
See the documentation of :py:func:`decl_buffer` for more details.
|
|
|
|
See Also
|
|
--------
|
|
decl_buffer : Declare a buffer
|
|
"""
|
|
|
|
READ = 1
|
|
WRITE = 2
|
|
|
|
def access_ptr(self, access_mask, ptr_type="handle", content_lanes=1, offset=0, extent=None):
|
|
"""Get an access pointer to the head of buffer.
|
|
|
|
This is the recommended method to get buffer data
|
|
ptress when interacting with external functions.
|
|
|
|
Parameters
|
|
----------
|
|
access_mask : int
|
|
The access pattern MASK. Indicate whether the
|
|
access will read or write to the data content.
|
|
|
|
ptr_type : str or tvm.ir.Type, optional
|
|
The data type of the result pointer. Do not specify
|
|
unless we want to cast pointer to specific type.
|
|
|
|
content_lanes: int, optional
|
|
The number of lanes for the data type. This value
|
|
is greater than one for vector types.
|
|
|
|
offset: Expr, optional
|
|
The offset of pointer. We can use it to offset by
|
|
the number of elements from the address of ptr.
|
|
|
|
extent: Expr, optional
|
|
The extent of pointer.
|
|
|
|
Examples
|
|
--------
|
|
.. code-block:: python
|
|
|
|
# Get access ptr for read
|
|
buffer.access_ptr("r")
|
|
# Get access ptr for read/write with bitmask
|
|
buffer.access_ptr(Buffer.READ | Buffer.WRITE)
|
|
# Get access ptr for read/write with str flag
|
|
buffer.access_ptr("rw")
|
|
# Get access ptr for read with offset
|
|
buffer.access_ptr("r", offset = 100)
|
|
# Get access ptr for read with extent
|
|
buffer.access_ptr("r", extent = 100)
|
|
"""
|
|
if isinstance(access_mask, str):
|
|
mask = 0
|
|
for value in access_mask:
|
|
if value == "r":
|
|
mask = mask | Buffer.READ
|
|
elif value == "w":
|
|
mask = mask | Buffer.WRITE
|
|
else:
|
|
raise ValueError(f"Unknown access_mask {access_mask}")
|
|
access_mask = mask
|
|
if isinstance(ptr_type, str):
|
|
ptr_type = (
|
|
PointerType(PrimType("void"))
|
|
if ptr_type == "handle"
|
|
else PointerType(PrimType(ptr_type))
|
|
)
|
|
elif isinstance(ptr_type, PrimType):
|
|
ptr_type = PointerType(ptr_type)
|
|
offset = convert(offset)
|
|
extent = convert(extent)
|
|
return _ffi_api.BufferAccessPtr(
|
|
self,
|
|
access_mask,
|
|
ptr_type,
|
|
content_lanes,
|
|
offset,
|
|
extent, # type: ignore
|
|
)
|
|
|
|
def vload(self, begin, dtype=None, predicate=None):
|
|
"""Generate an Expr that loads dtype from begin index.
|
|
|
|
Parameters
|
|
----------
|
|
begin : Array of Expr
|
|
The beginning index in unit of Buffer.dtype
|
|
|
|
dtype : str
|
|
The data type to be loaded,
|
|
can be vector type which have lanes that is multiple of Buffer.dtype
|
|
|
|
predicate : Optional[Expr]
|
|
A vector mask of boolean values indicating which lanes of a vector are to be
|
|
loaded. The number lanes of the mask must be equal to the number of lanes being loaded.
|
|
|
|
Returns
|
|
-------
|
|
load : Expr
|
|
The corresponding load expression.
|
|
"""
|
|
begin = (begin,) if isinstance(begin, int) or tvm.ir.is_prim_expr(begin) else begin
|
|
dtype = dtype if dtype else self.dtype
|
|
return _ffi_api.BufferVLoad(self, begin, dtype, predicate) # type: ignore
|
|
|
|
def vstore(self, begin, value, predicate=None):
|
|
"""Generate a Stmt that store value into begin index.
|
|
|
|
Parameters
|
|
----------
|
|
begin : Array of Expr
|
|
The beginning index in unit of Buffer.dtype
|
|
|
|
value : Expr
|
|
The value to be stored.
|
|
|
|
predicate : Optional[Expr]
|
|
A vector mask of boolean values indicating which lanes of a vector are to be
|
|
stored. The number lanes of the mask must be equal to the number of lanes in
|
|
value.
|
|
|
|
Returns
|
|
-------
|
|
store : Stmt
|
|
The corresponding store stmt.
|
|
"""
|
|
begin = (begin,) if isinstance(begin, int) or tvm.ir.is_prim_expr(begin) else begin
|
|
return _ffi_api.BufferVStore(self, begin, value, predicate) # type: ignore
|
|
|
|
def scope(self):
|
|
"""Return the storage scope associated with this buffer.
|
|
Returns
|
|
-------
|
|
scope : str
|
|
The storage scope associated with this buffer.
|
|
"""
|
|
return _ffi_api.BufferStorageScope(self) # type: ignore
|
|
|
|
def get_flattened_buffer(self):
|
|
"""Generate a Buffer that is a flattened version of this buffer.
|
|
|
|
Returns
|
|
-------
|
|
flattened : Buffer
|
|
The corresponding flat buffer.
|
|
"""
|
|
return _ffi_api.BufferGetFlattenedBuffer(self) # type: ignore
|
|
|
|
def with_allocated_addr(self, allocated_addr):
|
|
"""Return a new buffer with the allocated address."""
|
|
return _ffi_api.BufferWithAllocatedAddr(self, allocated_addr) # type: ignore
|
|
|
|
def with_dtype(self, dtype):
|
|
"""Return a new buffer with the dtype."""
|
|
return _ffi_api.BufferWithDtype(self, dtype) # type: ignore
|
|
|
|
def with_data(self, data):
|
|
"""Return a new buffer with the data."""
|
|
return _ffi_api.BufferWithData(self, data) # type: ignore
|
|
|
|
def offset_of(self, indices):
|
|
"""Determine the offset of the provided indices in the flattened buffer.
|
|
|
|
Parameters
|
|
----------
|
|
indices : Union[Expr, List[Expr]]
|
|
|
|
The indices of the element in the original buffer.
|
|
|
|
Returns
|
|
-------
|
|
flattened_indices: List[Expr]
|
|
|
|
The offset indices of the element in the flattened buffer.
|
|
"""
|
|
return _ffi_api.BufferOffsetOf(self, indices) # type: ignore
|
|
|
|
@property
|
|
def byte_offset(self):
|
|
"""Get the byte offset of the buffer."""
|
|
return self.elem_offset * tvm.DataType(self.dtype).bits // 8
|
|
|
|
def elem_offset_of(self, indices, inner=True):
|
|
"""Get the element offset of the buffer at the given indices.
|
|
Note that indices subject to buffer's layout mapping.
|
|
|
|
Parameters
|
|
----------
|
|
indices : Union[Expr, List[Expr]]
|
|
The indices of the element in the original buffer.
|
|
|
|
inner : bool, optional
|
|
If False, the offset is relative to the original buffer.
|
|
Default is True.
|
|
|
|
Returns
|
|
-------
|
|
offset: Expr
|
|
The element offset of the buffer at the given indices.
|
|
"""
|
|
if inner:
|
|
return _ffi_api.BufferOffsetOfp(self, indices)
|
|
return self.elem_offset + _ffi_api.BufferOffsetOfp(self, indices)
|
|
|
|
def byte_offset_of(self, indices, inner=True):
|
|
"""Get the byte offset of the buffer at the given indices.
|
|
Note that indices subject to buffer's layout mapping.
|
|
|
|
Parameters
|
|
----------
|
|
indices : Union[Expr, List[Expr]]
|
|
The indices of the element in the original buffer.
|
|
|
|
inner : bool, optional
|
|
If False, the offset is relative to the original buffer.
|
|
Default is True.
|
|
|
|
Returns
|
|
-------
|
|
offset: Expr
|
|
The byte offset of the buffer at the given indices.
|
|
"""
|
|
return self.elem_offset_of(indices, inner) * tvm.DataType(self.dtype).bits // 8
|
|
|
|
def is_scalar(self, alloc_or_decl=True):
|
|
"""Check if the buffer is a scalar.
|
|
|
|
Parameters
|
|
----------
|
|
alloc_or_decl : bool, optional
|
|
Whether to consider alloc_scalar and decl_scalar as scalar. True for alloc_scalar,
|
|
False for decl_scalar.
|
|
|
|
Returns
|
|
-------
|
|
bool: True if the buffer is a scalar, False otherwise.
|
|
"""
|
|
return _ffi_api.BufferIsScalar(self, alloc_or_decl)
|
|
|
|
def ptr_to(self, indices):
|
|
"""Get the pointer to the buffer at the given indices (logical indices).
|
|
|
|
Note that the bufferload inside requires LowerTIPp pass to apply the layout to get the physical indices.
|
|
""" # noqa: E501
|
|
assert len(indices) == len(self.shape), (
|
|
f"The number of indices {indices} does not match the shape of the buffer {self.shape}"
|
|
)
|
|
return tvm.tirx.address_of(self[tuple(indices)])
|
|
|
|
def view(self, *args, **kwargs) -> "Buffer":
|
|
"""Creates a new view of the buffer. (used by parser)
|
|
|
|
Supported signatures are ``view(*shape, layout=None)``, where shape can contain
|
|
``-1`` to indicate that the dimension size is auto-inferred, and
|
|
``view(dtype: Union[str, tvm.DataType])``.
|
|
|
|
Returns
|
|
-------
|
|
view : DeclBufferFrame
|
|
The corresponding view buffer.
|
|
"""
|
|
|
|
def _infer_shape(shape):
|
|
shape = list(shape)
|
|
if -1 in shape and shape.count(-1) == 1:
|
|
size = functools.reduce(lambda x, y: x * y, self.shape)
|
|
n_size = functools.reduce(lambda x, y: x * y, [s for s in shape if s != -1], 1)
|
|
shape[shape.index(-1)] = size // n_size
|
|
else:
|
|
# Only validate the shape product when both old and new shapes
|
|
# are fully concrete: a Expr `==` returns an `EQ` node, not
|
|
# a Python bool, and `assert <Expr>` raises (no __bool__).
|
|
if all(isinstance(s, int) for s in shape) and all(
|
|
isinstance(s, int) for s in self.shape
|
|
):
|
|
assert functools.reduce(lambda x, y: x * y, shape) == functools.reduce(
|
|
lambda x, y: x * y, self.shape
|
|
), (
|
|
"The shape of the buffer "
|
|
+ str(self.shape)
|
|
+ " and the new shape "
|
|
+ str(shape)
|
|
+ " are not compatible"
|
|
)
|
|
return shape
|
|
|
|
if len(args) == 1 and isinstance(args[0], str | tvm.DataType) and not kwargs:
|
|
cast_dtype = tvm.DataType(args[0])
|
|
cur_dtype = tvm.DataType(self.dtype)
|
|
if cast_dtype.bits > cur_dtype.bits:
|
|
# cast up
|
|
assert cast_dtype.bits % cur_dtype.bits == 0
|
|
ratio = cast_dtype.bits // cur_dtype.bits
|
|
layout = self.layout.pack(ratio)
|
|
shape = [s for s in self.shape[:-1]] + [self.shape[-1] // ratio]
|
|
new_elem_offset = self.elem_offset // ratio
|
|
else:
|
|
# cast down
|
|
assert cur_dtype.bits % cast_dtype.bits == 0
|
|
ratio = cur_dtype.bits // cast_dtype.bits
|
|
layout = self.layout.unpack(ratio)
|
|
shape = [s for s in self.shape[:-1]] + [self.shape[-1] * ratio]
|
|
new_elem_offset = self.elem_offset * ratio
|
|
return tvm.tirx.script.builder.decl_buffer(
|
|
shape,
|
|
cast_dtype,
|
|
self.data,
|
|
self.strides,
|
|
new_elem_offset,
|
|
None,
|
|
self.scope(),
|
|
self.data_alignment,
|
|
self.offset_factor,
|
|
"",
|
|
self.axis_separators,
|
|
layout,
|
|
)
|
|
else:
|
|
# --- Signature 1: view(*shape, **opts) ---
|
|
# Check if all positional args are integers/PrimExprs with dtype int32 or int64 (the shape) # noqa: E501
|
|
shape = args
|
|
assert all(
|
|
isinstance(arg, int)
|
|
or (tvm.ir.is_prim_expr(arg) and arg.ty.dtype in ["int32", "int64"])
|
|
for arg in shape
|
|
), "shape must be a list of integers or PrimExprs with dtype int32 or int64"
|
|
# Safely get optional keyword arguments
|
|
layout = kwargs.get("layout", None)
|
|
# Assert there are no other kwargs
|
|
assert set(kwargs.keys()).issubset({"layout"}), (
|
|
f"Unsupported kwargs for view: {set(kwargs.keys()) - {'layout'}}"
|
|
)
|
|
|
|
if layout is None:
|
|
shape = _infer_shape(shape)
|
|
|
|
return tvm.tirx.script.builder.decl_buffer(
|
|
shape,
|
|
self.dtype,
|
|
self.data,
|
|
self.strides,
|
|
self.elem_offset,
|
|
None,
|
|
self.scope(),
|
|
self.data_alignment,
|
|
self.offset_factor,
|
|
"",
|
|
self.axis_separators,
|
|
self.layout if layout is None else layout,
|
|
)
|
|
|
|
def local(self, *shape, layout=None) -> "Buffer":
|
|
"""Create a thread-local view of this buffer.
|
|
|
|
When called with no shape arguments, auto-infers a 1D shape from
|
|
the layout's non-thread component (i.e. ``layout.storage().shard``).
|
|
|
|
Parameters
|
|
----------
|
|
shape : tuple of Expr
|
|
The shape of the local view for indexing. If omitted, a 1D
|
|
shape is computed automatically.
|
|
|
|
layout : optional
|
|
Override layout. If None, uses the storage layout
|
|
(parent layout with thread axes removed).
|
|
|
|
Returns
|
|
-------
|
|
local : DeclBufferFrame
|
|
The corresponding local buffer.
|
|
"""
|
|
if not shape:
|
|
local_layout = self.layout.storage()
|
|
total = functools.reduce(
|
|
lambda x, y: x * y, [it.extent for it in local_layout.shard], 1
|
|
)
|
|
shape = (total,)
|
|
return tvm.tirx.script.builder.decl_buffer(
|
|
shape,
|
|
self.dtype,
|
|
self.data,
|
|
self.strides,
|
|
self.elem_offset,
|
|
None,
|
|
self.scope(),
|
|
self.data_alignment,
|
|
self.offset_factor,
|
|
"",
|
|
self.axis_separators,
|
|
self.layout.storage() if layout is None else layout,
|
|
)
|
|
|
|
def permute(self, *dims) -> "Buffer":
|
|
"""Permute the dimensions of the buffer.
|
|
|
|
Parameters
|
|
----------
|
|
dims : tuple of int
|
|
The permutation of dimensions.
|
|
|
|
Returns
|
|
-------
|
|
permuted : DeclBufferFrame
|
|
The buffer with permuted dimensions.
|
|
"""
|
|
new_shape = [self.shape[d] for d in dims]
|
|
# Permute *logical* dims, not the layout's fine-grained shard iters: a
|
|
# tcgen05/atom layout maps several shard iters to each logical axis, so
|
|
# group by the current shape first and permute whole groups. ``group``
|
|
# returns a regrouped layout (degenerate extent-1 iters folded away)
|
|
# plus seps over *that* layout — permute the regrouped one, not
|
|
# ``self.layout``. For a simple layout (one shard iter per axis) this
|
|
# reduces to ``permute_dims(dims)``.
|
|
grouped, seps = self.layout.group(list(self.shape))
|
|
new_layout = grouped.permute_by_groups(seps, list(dims))
|
|
return tvm.tirx.script.builder.decl_buffer(
|
|
new_shape,
|
|
self.dtype,
|
|
self.data,
|
|
self.strides,
|
|
self.elem_offset,
|
|
None,
|
|
self.scope(),
|
|
self.data_alignment,
|
|
self.offset_factor,
|
|
"",
|
|
self.axis_separators,
|
|
new_layout,
|
|
)
|
|
|
|
def __getitem__(self, indices):
|
|
from ..arith import Analyzer # pylint: disable=import-outside-toplevel
|
|
from .expr import BufferLoad, Ramp # pylint: disable=import-outside-toplevel
|
|
from .stmt import BufferRegion # pylint: disable=import-outside-toplevel
|
|
|
|
if not isinstance(indices, tuple | list):
|
|
indices = [indices]
|
|
has_slice = any(isinstance(i, slice) for i in indices)
|
|
has_step = any(
|
|
isinstance(i, slice) and (i.step is not None and i.step != 1) for i in indices
|
|
)
|
|
has_implicit_slice = len(indices) < len(self.shape)
|
|
analyzer = Analyzer()
|
|
if (has_slice and not has_step) or has_implicit_slice:
|
|
region = []
|
|
for i, index in enumerate(indices):
|
|
if isinstance(index, slice):
|
|
start = 0 if index.start is None else index.start
|
|
stop = self.shape[i] if index.stop is None else index.stop
|
|
region.append(Range.from_min_extent(start, analyzer.simplify(stop - start)))
|
|
else:
|
|
region.append(
|
|
Range.from_min_extent(
|
|
index,
|
|
tvm.tirx.expr.IntImm(index.ty, 1) if tvm.ir.is_prim_expr(index) else 1,
|
|
)
|
|
)
|
|
if has_implicit_slice:
|
|
for i in range(len(indices), len(self.shape)):
|
|
region.append(Range.from_min_extent(0, self.shape[i]))
|
|
return BufferRegion(self, region)
|
|
else:
|
|
expr_indices = []
|
|
for i, index in enumerate(indices):
|
|
if isinstance(index, slice):
|
|
start = 0 if index.start is None else index.start
|
|
stop = self.shape[i] if index.stop is None else index.stop
|
|
step = 1 if index.step is None else index.step
|
|
# We should ensure the dtype of start is the same with that of step.
|
|
if tvm.ir.is_prim_expr(start) and isinstance(step, int):
|
|
step = tvm.tirx.expr.IntImm(start.ty, step)
|
|
lanes = analyzer.simplify((stop - start + step - 1) // step)
|
|
if lanes == 1:
|
|
expr_indices.append(start)
|
|
else:
|
|
expr_indices.append(Ramp(start, step, int(lanes)))
|
|
else:
|
|
expr_indices.append(index)
|
|
return BufferLoad(self, expr_indices)
|
|
|
|
|
|
def decl_buffer(
|
|
shape,
|
|
dtype=None,
|
|
name="buffer",
|
|
data=None,
|
|
strides=None,
|
|
elem_offset=None,
|
|
scope="",
|
|
data_alignment=-1,
|
|
offset_factor=0,
|
|
buffer_type="",
|
|
axis_separators=None,
|
|
span=None,
|
|
layout="default",
|
|
):
|
|
# pylint: disable=import-outside-toplevel
|
|
from .expr import Var
|
|
from .layout import S, TileLayout
|
|
|
|
shape = (shape,) if tvm.ir.is_prim_expr(shape) or isinstance(shape, Integral) else shape
|
|
dtype = "float32" if dtype is None else dtype
|
|
strides = () if strides is None else strides
|
|
|
|
if axis_separators is None:
|
|
axis_separators = []
|
|
|
|
if layout == "default":
|
|
layout = TileLayout(S[tuple(shape)]) if shape else None
|
|
|
|
if offset_factor != 0 and elem_offset is None:
|
|
shape_ty = shape[0].ty if shape and tvm.ir.is_prim_expr(shape[0]) else "int32"
|
|
elem_offset = Var(f"{name}_elem_offset", shape_ty)
|
|
if data is None:
|
|
# Bool is represented as uint1 in the IR, but stored as int8
|
|
storage_type = dtype if isinstance(dtype, PrimType) else PrimType(dtype)
|
|
storage_type = PrimType("int8") if storage_type.dtype == "bool" else storage_type
|
|
data = Var(name, PointerType(storage_type, scope), span)
|
|
return _ffi_api.Buffer( # type: ignore
|
|
data,
|
|
dtype,
|
|
shape,
|
|
strides,
|
|
elem_offset,
|
|
name,
|
|
data_alignment,
|
|
offset_factor,
|
|
buffer_type,
|
|
axis_separators,
|
|
span,
|
|
layout,
|
|
)
|
|
|
|
|
|
@tvm_ffi.register_object("tirx.DataProducer")
|
|
class DataProducer(Object):
|
|
pass
|