Source code for hidet.graph.tensor

# Licensed 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.
from __future__ import annotations

from typing import List, Optional, Tuple, Sequence, Union
import warnings

import numpy as np

import hidet.runtime.storage
import hidet.cuda
from hidet.ir import dtypes
from hidet.ir.type import DataType, data_type
from hidet.ir.expr import Expr, Constant, SymbolVar, symbol_var, is_constant
from hidet.ir.layout import DataLayout, RowMajorLayout
from hidet.runtime.storage import Storage
from hidet.utils import prod
from hidet.utils.overrides import set_module
from hidet.runtime.device import Device, instantiate_device


def _simplify_dim(dim: Union[int, Expr]) -> Union[int, SymbolVar]:
    from hidet.ir.tools import simplify

    if isinstance(dim, (int, SymbolVar)):
        return dim
    elif isinstance(dim, Constant):
        return int(dim)
    else:
        return simplify(dim)


@set_module('hidet')
class Tensor:
    """An n-dimension array, could be symbolic or concrete.

    This class defines an n-dimension array.

    Parameters
    ----------
    shape: Sequence[int]
        The shape of the tensor.

    dtype: DataType or str
        The data type of the tensor.

    device: Device or str
        The device of the tensor.

    storage: Storage, optional
        The storage of the tensor. None indicates it is a symbolic tensor.

    layout: DataLayout, optional
        The data layout of the tensor.

    trace: Tuple[Operator, int], optional
        Where this tensor is derived from. A trace = (op, i) indicates that this tensor is the i-th output of the op
        operator.
    """

    def __init__(self, shape, dtype, device, storage, layout=None, trace=None):
        from hidet.graph.operator import Operator

        self._shape: Tuple[Union[Expr, int], ...] = tuple(_simplify_dim(dim) for dim in shape)
        self._dtype: DataType = data_type(dtype)
        self._device: Device = instantiate_device(device)
        self._storage: Optional[Storage] = storage
        self._layout: Optional[DataLayout] = layout
        self._trace: Optional[Tuple[Operator, int]] = trace

    @property
    def shape(self) -> Tuple[Union[int, Expr], ...]:
        """
        The shape of the tensor.

        The shape is a tuple of integers indicating the size of the tensor along each dimension.

        Returns
        -------
        shape: Tuple[int, ...]
            The shape of the tensor.
        """
        return self._shape

    @property
    def dtype(self) -> DataType:
        """
        The data type of the tensor.

        Returns
        -------
        dtype: DataType
            The data type of the tensor.
        """
        return self._dtype

    @property
    def device(self) -> Device:
        """
        The device of the tensor.

        Returns
        -------
        device: Device
            The device of the tensor.
        """
        return self._device

    @property
    def storage(self) -> Optional[Storage]:
        """
        The storage of the tensor.

        Returns
        -------
        storage: Storage
            The storage of the tensor.
        """
        return self._storage

    @property
    def trace(self):
        """
        The producer and the index of outputs of the producer of this tensor.

        This attribute is used to track how this tensor is computed. None indicates this is a leaf tensor where the
        value will be given by the user. Otherwise, it will be a tuple with (operator, index) where operator is the
        producer of this tensor and index is the index of the output of the operator.

        Returns
        -------
        trace: Tuple[hidet.graph.Operator, int]
            The trace of this tensor.
        """
        return self._trace

    @property
    def size(self) -> int:
        """
        The number of elements in the tensor.

        Returns
        -------
        size: int
            The number of elements in the tensor.
        """
        return prod(self._shape)

    @property
    def layout(self) -> DataLayout:
        """
        The data layout of the tensor.

        .. note::

          This attribute is experimental and might change in the future.

        Returns
        -------
        layout: Optional[DataLayout]
            The data layout of the tensor. None indicates the compact row major layout.
        """
        return self._layout

    @property
    def nbytes(self):
        """The number of bytes of the tensor.

        Returns
        -------
        ret: int
            The number of bytes.
        """
        return prod(self.shape) * self.dtype.nbytes

    @property
    def op(self):
        """The operator that produces this tensor.

        Returns
        -------
        ret: hidet.graph.operator.Operator, optional
            The operator that produces this tensor. None indicates it is not traced.
        """
        return self.trace[0] if self.trace else None

    def __pos__(self):
        return self

    def __neg__(self) -> Tensor:
        from .ops import negative

        return negative(self)

    def __add__(self, other) -> Tensor:
        from .ops import add

        return add(self, other)

    def __sub__(self, other) -> Tensor:
        from .ops import subtract

        return subtract(self, other)

    def __mul__(self, other) -> Tensor:
        from .ops import multiply, utils

        return multiply(self, utils.convert_to_tensor(other, self))

    def __truediv__(self, other) -> Tensor:
        from .ops import divide, utils

        return divide(self, utils.convert_to_tensor(other, self))

    def __mod__(self, other) -> Tensor:
        from .ops import mod, utils

        return mod(self, utils.convert_to_tensor(other, self))

    def __pow__(self, power, modulo=None) -> Tensor:
        from .ops import pow, utils

        return pow(self, utils.convert_to_tensor(power, self))

    def __matmul__(self, other) -> Tensor:
        from .ops import utils, matmul

        return matmul(self, utils.convert_to_tensor(other, self))

    def __invert__(self) -> Tensor:
        from .ops import bitwise_invert

        return bitwise_invert(self)

    def __and__(self, other) -> Tensor:
        from .ops import bitwise_and, utils

        return bitwise_and(self, utils.convert_to_tensor(other, self))

    def __or__(self, other):
        from .ops import bitwise_or, utils

        return bitwise_or(self, utils.convert_to_tensor(other, self))

    def __xor__(self, other):
        from .ops import bitwise_xor, utils

        return bitwise_xor(self, utils.convert_to_tensor(other, self))

    def __lshift__(self, other):
        from .ops import bitwise_left_shift, utils

        return bitwise_left_shift(self, utils.convert_to_tensor(other, self))

    def __rshift__(self, other):
        from .ops import bitwise_right_shift, utils

        return bitwise_right_shift(self, utils.convert_to_tensor(other, self))

    def __lt__(self, other):
        from .ops import less, utils

        return less(self, utils.convert_to_tensor(other, self))

    def __le__(self, other):
        from .ops import less_equal, utils

        return less_equal(self, utils.convert_to_tensor(other, self))

    def __gt__(self, other):
        from .ops import greater, utils

        return greater(self, utils.convert_to_tensor(other, self))

    # we do not define __eq__ method for Tensor

    def __ne__(self, other):
        from .ops import not_equal, utils

        return not_equal(self, utils.convert_to_tensor(other, self))

    def __radd__(self, other):
        from .ops import add

        return add(other, self)

    def __rsub__(self, other):
        from .ops import subtract

        return subtract(other, self)

    def __rmul__(self, other):
        from .ops import multiply

        return multiply(other, self)

    def __abs__(self):
        from .ops import abs

        return abs(self)

    def __bool__(self) -> bool:
        if self.size > 1:
            raise RuntimeError('Boolean value of Tensor with more than one value is ambiguous')
        return bool(self.item())

    def __float__(self) -> float:
        if self.size > 1:
            raise RuntimeError('only one element tensors can be converted to Python scalars')
        return float(self.item())

    def __index__(self) -> int:
        if self.size > 1:
            raise RuntimeError('only one element tensors can be converted to Python scalars')
        return int(self.item())

    def __int__(self) -> int:
        if self.size > 1:
            raise RuntimeError('only one element tensors can be converted to Python scalars')
        return int(self.item())

    def __str__(self):
        head = self.signature()
        if self.storage:
            array_str = str(self.cpu().numpy())
            return '{}\n{}'.format(head, array_str)
        else:
            if self.trace is None:
                return head
            else:
                return '{}\nfrom {}'.format(head, self.trace)

    def __getitem__(self, item):
        from hidet.graph.ops import strided_slice

        if isinstance(item, Tensor):
            if len(item.shape) > 1:
                raise NotImplementedError("Tensor indexing via Tensor currently only supports 1D index tensor")
            if not item.dtype.is_integer():
                raise TypeError("Tensor indexing via Tensor requires integer index tensor")
            from .ops import take

            return take(self, item, axis=0)

        if isinstance(item, list):
            item = tuple(item)

        if not isinstance(item, tuple):
            item = tuple([item])

        # now, the item could have
        # 1. integer index
        # 2. slice
        # 3. Ellipsis
        # 4. None
        # e.g., [1, 3:5, ..., None]

        # process Ellipsis
        # e.g., x[1, ..., 2] -> x[1, :, :, 2]
        if Ellipsis in item:
            if item.count(Ellipsis) > 1:
                raise ValueError('Only one ellipsis allowed in index.')
            ellipsis_index = item.index(Ellipsis)
            ellipsis_ndim = len(self.shape) - sum([1 if axis not in [None, Ellipsis] else 0 for axis in item])
            ellipsis_ndim = max(ellipsis_ndim, 0)
            item = item[:ellipsis_index] + (slice(None),) * ellipsis_ndim + item[ellipsis_index + 1 :]

        # process None
        # e.g., x[2, None, 3] -> x[2, 1, 3]
        if None in item:
            dims = []
            for i, v in enumerate(item):
                if v is None:
                    dims.append(i)
            item = [v if v is not None else slice(None) for v in item]
            return self.unsqueeze(dims)[item]

        assert None not in item

        # normalize index
        normalized_item = []
        for i, v in enumerate(item):
            if isinstance(v, int):
                if v < 0:
                    v = v + self.shape[i]
                if is_constant(v, self.shape[i]) and (v < 0 or v >= self.shape[i]):
                    raise IndexError(
                        'index {} is out of bound for dimension {} with size {}'.format(v, i, self.shape[i])
                    )
                normalized_item.append(v)
            else:
                normalized_item.append(v)
        item = tuple(normalized_item)

        # process slice and integer index
        rank = len(self.shape)
        while len(item) < rank:
            item = item + (slice(None),)
        starts, ends, steps = [], [], []
        squeeze_dims = []
        for dim, v in enumerate(item):
            if isinstance(v, (int, Expr)):
                squeeze_dims.append(dim)
                starts.append(v)
                ends.append(v + 1)
                steps.append(1)
            else:
                assert isinstance(v, slice)
                starts.append(v.start)
                ends.append(v.stop)
                steps.append(v.step)
        sliced = strided_slice(self, starts, ends, strides=steps).squeeze(squeeze_dims)
        return sliced

    def __iter__(self):
        raise TypeError('hidet.Tensor does not support iteration.')

    def __hash__(self):
        """
        Notes
        -----
        This is a hack to make hidet.Tensor hashable. There are some places where we need to use tensor as the key
        of a dictionary, (e.g., in a graph optimization pass or graph execution). However, to implement a correct
        protocol for hashable objects, we need to implement __eq__ as well to compare two objects when their hash
        values are equal. But as a tensor, the __eq__ method is used to do element-wise comparison, and it returns
        a tensor instead of a boolean value. Thus, there will be a problem when the dict that takes Tensor as key type
        has other kinds of objects (e.g., int). We deliberately ignore this problem here in exchange for the convenience
        of using tensor as the key of a dict.
        """
        return id(self)

    def __getstate__(self):
        if self.storage:
            data = self.detach().cpu().numpy()
        else:
            data = None

        return {
            'shape': self.shape,
            'dtype': self.dtype,
            'device': self.device,
            'data': data,
            'layout': self.layout,
            'trace': self.trace,
        }

    def __setstate__(self, state):
        data = state['data']
        if data is not None:
            assert isinstance(data, np.ndarray)
            tensor = from_numpy(data).to(device=state['device'])
            storage = tensor.storage
        else:
            storage = None

        self._device = state['device']
        self._shape = state['shape']
        self._dtype = state['dtype']
        self._storage = storage
        self._layout = state['layout']
        self._trace = state['trace']

    def __dlpack__(self, stream: Optional[int] = None):
        """
        This function is used to support interoperability with other frameworks that support __dlpack__ protocol.
        """
        from .impl.dlpack import to_dlpack

        if stream is not None:
            consumer_stream = hidet.cuda.ExternalStream(stream)
            provider_stream = hidet.cuda.current_stream()
            if consumer_stream != provider_stream:
                event = hidet.cuda.Event()
                event.record(provider_stream)
                consumer_stream.wait_event(event)
        return to_dlpack(self)

    def __dlpack_device__(self) -> Tuple[int, int]:
        """
        This function is used to support interoperability with other frameworks that support __dlpack__ protocol.
        """
        from .impl.dlpack import to_dlpack_device

        return to_dlpack_device(self)

[docs] def tolist(self): """ Convert the tensor to a nested list of numbers. Returns ------- ret: the nested list of numbers The nested list of numbers. The number of nested levels is equal to the rank of the tensor. """ return self.cpu().numpy().tolist()
[docs] def to_device(self, device, /, *, stream=None): """ Move the tensor to the specified device. Parameters ---------- device: Device or str The device to move the tensor to. stream: Stream or None The stream to use for the copy. If None, the current stream is used. Returns ------- ret: Tensor The tensor on the specified device. """ device = instantiate_device(device) if device.is_cpu(): tensor = self.cpu() elif device.is_cuda(): tensor = self.cuda_async(device, stream=stream) else: raise ValueError('Cannot recognize device {}'.format(device)) return tensor
[docs] def item(self) -> Union[int, float, bool]: """ Convert the tensor to a scalar value. Returns ------- """ if prod(self._shape) == 1: ret = self.squeeze(dims=list(range(len(self.shape)))).tolist() if not isinstance(ret, (int, float, bool, complex)): raise TypeError('Cannot convert tensor to scalar.') return ret else: raise RuntimeError('Only support .item() method for tensor with only one element')
[docs] def signature(self) -> str: """Get the signature of the tensor. Returns ------- ret: str The signature of the tensor. """ return "Tensor(shape={}, dtype='{}', device='{}')".format(self.shape, self.dtype.name, self.device)
[docs] def is_symbolic(self) -> bool: """ Check if the tensor is symbolic. A tensor is symbolic if it is not backed by any storage (i.e., ``self.storage is None``). Returns ------- ret: bool True if the tensor is symbolic, False otherwise. """ return self.storage is None
[docs] def contiguous(self): """Create a tensor with contiguous row-major layout. If the tensor already has the continuous row-major layout, this tensor is returned directly. Returns ------- ret: Tensor The tensor with contiguous row-major layout. """ if self.layout is None or isinstance(self.layout, RowMajorLayout): return self return self.reshape(self.shape)
[docs] def reshape(self, shape: Sequence[int]): """Create a reshaped tensor. See Also :func:`hidet.graph.ops.reshape`. Parameters ---------- shape: Sequence[int] The new shape. Returns ------- ret: Tensor The reshaped tensor. """ from .ops import reshape return reshape(self, shape)
[docs] def squeeze(self, dims: Union[int, Sequence[int]]): """Create a squeezed tensor. See Also :func:`hidet.graph.ops.squeeze`. Parameters ---------- dims: Union[int, Sequence[int]] The dimension(s) to squeeze. Returns ------- ret: Tensor The squeezed tensor. """ from .ops import squeeze return squeeze(self, dims)
[docs] def unsqueeze(self, dims: Union[int, Sequence[int]]): """Create a unsqueezed tensor. See Also :func:`hidet.graph.ops.unsqueeze`. Parameters ---------- dims: Union[int, Sequence[int]] The dimensions to unsqueeze. Returns ------- ret: Tensor The unsqueezed tensor. """ from .ops import unsqueeze return unsqueeze(self, dims)
[docs] def rearrange(self, plan: List[List[int]]): """Create a rearranged tensor. See Also :func:`hidet.graph.ops.rearrange`. Parameters ---------- plan: List[List[int]] The rearrange plan. Returns ------- ret: Tensor The rearranged tensor. """ from .ops import rearrange return rearrange(self, plan)
[docs] def sum(self, dims: Union[int, List[int]], keep_dim: bool = False): """Create a sum reduced tensor. See Also :func:`hidet.graph.ops.reduce_sum`. Parameters ---------- dims: Union[int, List[int]] The dimensions to sum up. keep_dim: bool Whether to keep the reduced dimensions. Returns ------- ret: Tensor The reduced tensor. """ from .ops import sum return sum(self, dims=dims, keep_dim=keep_dim)
[docs] def mean(self, dims: Union[int, List[int]], keep_dim: bool = False): """Create a mean reduced tensor. See Also :func:`hidet.graph.ops.reduce_mean`. Parameters ---------- dims: Union[int, List[int]] The dimensions to average up. keep_dim: bool Whether to keep the reduced dimensions. Returns ------- ret: Tensor The reduced tensor. """ from .ops import mean return mean(self, dims=dims, keep_dim=keep_dim)
[docs] def astype(self, dtype): """Cast the data type of current tensor. Parameters ---------- dtype: DataType or str The target data type to convert to. Returns ------- ret: Tensor The tensor with the new data type. """ from .ops import cast return cast(self, dtype)
[docs] def to(self, dtype=None, device=None): """Cast the data type of current tensor or/and move it to another device. Parameters ---------- dtype: DataType or str, optional The target data type to convert to. None indicates unchanged. device: Device or str, optional The target device to copy the tensor. None indicates unchanged. Returns ------- ret: Tensor The tensor with the new data type on target device. """ from .ops import cast tensor = self if dtype is not None: tensor = cast(tensor, dtype) if device is not None: device = instantiate_device(device) if device.is_cpu(): tensor = tensor.cpu() elif device.is_cuda(): tensor = tensor.cuda(device) else: raise ValueError('Cannot recognize device {}'.format(device)) return tensor
[docs] def cpu(self): """Create a copy of self tensor on cpu device. If the current tensor is already on cpu device, self is returned. Returns ------- ret: Tensor The new tensor or self. """ from hidet.graph.ops import transfer if self.device.kind == 'cpu': return self else: if self.storage is not None: return Tensor(self.shape, self.dtype, 'cpu', self.storage.cpu(), self.layout) else: return transfer(self, 'cpu')
[docs] def cuda(self, device=None): """Create a copy of self tensor on cuda device. If the current tensor is already on cuda device, self is returned. Parameters ---------- device: Device, optional The target cuda device. None indicates the current cuda device. Returns ------- ret: Tensor The new tensor or self. """ from hidet.graph.ops import transfer if device is None: device = 'cuda' device = instantiate_device(device) if self.device == device: return self else: if self.storage is not None: return Tensor(self.shape, self.dtype, device, self.storage.cuda(device.id), self.layout) else: return transfer(self, device)
def vcuda_(self): """Cast the tensor to vcuda device in place. If the current tensor is already on vcuda device, nothing is performed Returns ------- ret: None This operation is in-place """ if self.device.is_vcuda(): return if not self.device.is_cuda(): raise ValueError("Tensor must be on cuda device, got {}".format(self.device)) # if the tensor has no storage, there is no need to cast if self.storage is not None: self._storage = self.storage.vcuda(self.device.id) self._device = Device('vcuda', self.device.id) def cuda_(self): """Cast the tensor from vcuda device in place. If the current tensor is already on cuda device, nothing is performed Returns ------- ret: None This operation is in-place """ if self.device.is_cuda(): return if not self.device.is_vcuda(): raise ValueError("Tensor must be on vcuda device, got {}".format(self.device)) if self.storage is not None: self._storage = self.storage.cuda(self.device.id) self._device = Device('cuda', self.device.id)
[docs] def copy(self) -> Tensor: """Create a copy of current tensor. Returns ------- ret: Tensor A new tensor with the same contents as the current one. """ if self.trace is not None: raise ValueError('The symbolic tensor is not modifiable, so feel free to use them without copying.') return Tensor( shape=list(self.shape), dtype=self.dtype, device=self.device, storage=self.storage.copy(), layout=self.layout, trace=None, )
def copy_(self, src: Tensor): src_converted = src.to(dtype=self.dtype, device=self.device) if len(src.shape) != len(self.shape) or any(a != b for a, b in zip(self.shape, src_converted.shape)): raise ValueError( 'The shape of source tensor {} does not match the shape of target tensor {}'.format( src_converted.shape, self.shape ) ) if src_converted is src: self._storage = src.storage.copy() else: self._storage = src.storage return self
[docs] def copy_async(self, stream=None) -> Tensor: """Create a copy of current tensor asynchronously. Parameters ---------- stream: hidet.cuda.Stream, optional The stream to copy the tensor. None indicates the current stream of the device where self tensor is on. Returns ------- ret: Tensor A new tensor with the same contents as the current one. """ if self.trace is not None: raise ValueError('The symbolic tensor is not modifiable, so feel free to use them without copying.') return Tensor( shape=list(self.shape), dtype=self.dtype, device=self.device, storage=self.storage.copy_async(stream), layout=self.layout, trace=None, )
[docs] def detach(self): """Detach the current tensor from tracing. Returns ------- ret: Tensor The detached tensor. """ if self.trace is None: return self else: return Tensor( shape=self.shape, dtype=self.dtype, device=self.device, storage=self.storage, layout=self.layout, trace=None, )
[docs] def cpu_async(self, stream=None): """ Copy the tensor to CPU asynchronously. Parameters ---------- stream: hidet.cuda.Stream, optional The stream to copy the tensor to CPU on. Returns ------- ret: Tensor The tensor on CPU. """ if self.device.kind == 'cpu': return self else: if self.trace is None: ret = Tensor( self.shape, self.dtype, 'cpu', self.storage.cpu_async(stream) if self.storage else None, self.layout ) return ret else: raise ValueError('Please use .cpu() for symbolic tensor transfer.')
[docs] def cuda_async(self, device=None, stream=None): """ Copy the tensor to GPU asynchronously. Parameters ---------- device: Device, optional The target cuda device. None indicates the current cuda device. stream: hidet.cuda.Stream, optional The stream to copy the tensor to GPU on. None indicates the current stream. Returns ------- ret: Tensor The tensor on GPU. """ if device is None: device = 'cuda' device = instantiate_device(device) if self.device.is_cuda(): return self else: if self.trace is None: ret = Tensor( self.shape, self.dtype, device, self.storage.cuda_async(device.id, stream) if self.storage else None, self.layout, ) return ret else: raise ValueError('Please use .cuda(...) for symbolic tensor transfer.')
[docs] def numpy(self) -> np.ndarray: """ Convert the tensor to a numpy array. The tensor must be on CPU device. Otherwise, a RuntimeError will be raised. The returned numpy array will share the same memory with the tensor. Returns ------- ret: np.ndarray The numpy array. """ if self.device.kind != 'cpu': raise RuntimeError('Cannot convert a tensor on {} to numpy array.'.format(self.device)) if self.dtype in [dtypes.bfloat16, dtypes.tfloat32]: warnings.warn('numpy does not support {}, converting to float32'.format(self.dtype.name)) return self.astype(dtypes.float32).numpy() if self.dtype == dtypes.boolean: # workaround for numpy not supporting exporting boolean to dlpack return np.from_dlpack(self.to(dtype='uint8')).astype(np.bool_) else: return np.from_dlpack(self)
[docs] def torch(self): """ Convert to a torch tensor. Returns ------- ret: torch.Tensor The torch tensor that shares the memory with the hidet tensor. """ import torch if self.dtype == dtypes.boolean: # workaround for torch not supporting exporting boolean to dlpack return torch.from_dlpack(self.to(dtype='uint8')).bool() return torch.from_dlpack(self)
def masked_fill(self, mask, value): """ Fills the tensor with value where mask is True Parameters ---------- mask: Tensor The target cuda device. None indicates the current cuda device. value: Union[float, int] The stream to copy the tensor to GPU on. None indicates the current stream. Returns ------- ret: Tensor """ from .ops import where return where(mask, full([], value, dtype=self.dtype, device=self.device), self) def expand(self, *sizes: int) -> Tensor: from .ops import broadcast sizes: List[int] = list(sizes) assert len(sizes) >= len(self.shape) for i in range(len(sizes)): if sizes[i] == -1: ri = len(sizes) - 1 - i assert ri < len(self.shape) sizes[i] = int(self.shape[len(self.shape) - 1 - ri]) return broadcast(self, sizes) def float(self) -> Tensor: return self.to(dtype=hidet.float32) def transpose(self, dim0: int, dim1: int): from .ops import transpose if dim0 < dim1: dim0, dim1 = dim1, dim0 return transpose(self, [dim0, dim1])
[docs]def empty(shape, dtype='float32', device='cpu', layout=None): """Create an uninitialized tensor. Parameters ---------- shape: Sequence[int] The shape of new tensor. dtype: str or DataType The data type of element of the tensor. device: Device or str, default 'cpu' The device of the new tensor is created on. layout: DataLayout, optional The layout of the new tensor. None indicates the default layout (row-major layout). Returns ------- ret: Tensor The created tensor. """ dtype = data_type(dtype) num_bytes = int(prod(shape) * dtype.nbytes) storage = Storage.new(device, num_bytes) return Tensor(shape=shape, dtype=dtype, device=device, storage=storage, layout=layout)
[docs]def symbol(shape: Sequence[Union[int, str, Expr]], dtype='float32', device='cpu', layout=None) -> Tensor: """Create a symbolic tensor. Parameters ---------- shape: Sequence[Union[int, str, Expr]] The shape of new tensor. The shape can contain symbolic variables. str indicates the corresponding dimension is a symbolic variable with the given name. dtype: str or DataType The data type of element of the tensor. device: Device or str, default 'cpu' The device of the new tensor is created on. layout: DataLayout, optional The layout of the new tensor. None indicates the default layout (row-major layout). Returns ------- ret: Tensor The created tensor. """ updated_shape = [] for d in shape: if isinstance(d, str): updated_shape.append(symbol_var(d)) else: updated_shape.append(d) return Tensor(shape=updated_shape, dtype=dtype, device=device, storage=None, layout=layout)
[docs]def zeros(shape: Sequence[int], dtype: Union[DataType, str] = 'float32', device='cpu') -> Tensor: """Create a tensor initialized with zero. Parameters ---------- shape: Sequence[int] The shape of new tensor. dtype: str or DataType The data type of element of the tensor. device: Device or str, default 'cpu' The device of the new tensor is created on. Returns ------- ret: Tensor The created tensor. """ dtype = data_type(dtype) return full(shape, dtype.zero, dtype, device)
[docs]def ones(shape, dtype='float32', device='cpu') -> Tensor: """Create a tensor initialized with one. Parameters ---------- shape: Sequence[int] The shape of new tensor. dtype: DataType or str, default 'float32' The data type of element of the tensor. device: Device or str, default 'cpu' The device of the new tensor is created on. Returns ------- ret: Tensor The created tensor. """ dtype = data_type(dtype) return full(shape, dtype.one, dtype, device)
[docs]def full(shape, fill_value: Union[float, int], dtype='float32', device='cpu') -> Tensor: """Create a tensor initialized with given constant. Parameters ---------- shape: Sequence[int] The shape of new tensor. fill_value: float or int or hidet.ir.Constant The constant to initialize the new tensor. dtype: DataType or str, default 'float32' The data type of element of the tensor. device: Device or str, default 'cpu' The device of the new tensor is created on. Returns ------- ret: Tensor The created tensor. """ from hidet import ops dtype = data_type(dtype) return ops.full(shape=shape, value=fill_value, dtype=dtype, device=device)
[docs]def randn(shape, dtype='float32', mean=0.0, stddev=1.0, device='cpu') -> Tensor: """Create a tensor with uniformly distributed values. Parameters ---------- shape: Sequence[int] The shape of new tensor. dtype: DataType or str, default 'float32' The data type of element of the tensor. mean: float, default 0.0 The mean of the uniform distribution. stddev: float, default 1.0 The standard deviation of the uniform distribution. device: Device or str, default 'cpu' The device of the new tensor is created on. Returns ------- ret: Tensor The created tensor. Examples -------- >>> randn([2, 3]) Tensor(shape=[2, 3], dtype='float32', device='cuda') [[ 0.10720467 -1.6906018 0.06347568] [-0.37061226 0.562728 1.857547 ]] """ dtype: DataType = data_type(dtype) if dtype.is_complex(): assert isinstance(dtype, dtypes.complex.ComplexType) real = hidet.randn(shape, dtype=dtype.base_dtype, mean=mean, stddev=stddev, device=device) imag = hidet.randn(shape, dtype=dtype.base_dtype, mean=mean, stddev=stddev, device=device) return real + imag * 1j else: if any(not isinstance(d, int) for d in shape): raise RuntimeError('shape must be a sequence of integers, got {}'.format(repr(shape))) np_tensor = np.array(np.random.randn(*shape) * stddev + mean).astype( np.float32 ) # wrap np.array(...) for shape=[] dtype = data_type(dtype) if isinstance(np_tensor, float): # shape = [] np_tensor = np.array(np_tensor) hidet_tensor = from_numpy(np_tensor) return hidet_tensor.to(device=device, dtype=dtype)
def randint(low: int, high=None, shape: Sequence[int] = (), dtype: str = 'int32') -> Tensor: dtype_map = {'int32': np.int32, 'int64': np.int64} if dtype not in dtype_map: return randint(low=low, high=high, shape=shape, dtype='int32').astype(dtype) return asarray(np.random.randint(low=low, high=high, size=shape, dtype=dtype_map[dtype]))
[docs]def empty_like(data, shape=None, dtype=None, device=None, layout=None) -> Tensor: """ Create an uninitialized tensor with the same shape, dtype, and device as the given tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. layout: DataLayout, optional The layout of the new tensor. If None, the layout of data is used. Returns ------- ret: Tensor The created tensor. """ return empty( shape=shape if shape is not None else data.shape, dtype=dtype if dtype is not None else data.dtype, device=device if device is not None else data.device, layout=layout if layout is not None else data.layout, )
[docs]def symbol_like(data, shape=None, dtype=None, device=None, layout=None): """Create a symbol tensor like an existing tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. layout: DataLayout, optional The layout of the new tensor. If None, the layout of data is used. Returns ------- ret: Tensor The created symbol tensor. """ return symbol( shape=shape if shape is not None else data.shape, dtype=dtype if dtype is not None else data.dtype, device=device if device is not None else data.device, layout=layout if layout is not None else data.layout, )
[docs]def zeros_like(data, shape=None, dtype=None, device=None) -> Tensor: """ Create a tensor initialized with zero with the same shape, dtype, and device as the given tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. Returns ------- ret: Tensor The created tensor with all elements as zero. """ return zeros( shape=data.shape if shape is None else shape, dtype=data.dtype if dtype is None else dtype, device=data.device if device is None else device, )
[docs]def ones_like(data, shape=None, dtype=None, device=None) -> Tensor: """ Create a tensor initialized with one with the same shape, dtype, and device as the given tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. Returns ------- ret: Tensor The created tensor with all elements as one. """ return ones( shape=data.shape if shape is None else shape, dtype=data.dtype if dtype is None else dtype, device=data.device if device is None else device, )
[docs]def full_like( data: Tensor, fill_value, shape: Optional[Sequence[int]] = None, dtype: Optional[Union[str, DataType]] = None, device: Optional[str] = None, ) -> Tensor: """ Create a tensor initialized with fill_value with the same shape, dtype, and device as the given tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. fill_value: int, float, bool, complex The value to fill the tensor with. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. Returns ------- ret: Tensor The created tensor with all elements as fill_value. """ return full( shape=data.shape if shape is None else shape, fill_value=fill_value, dtype=data.dtype if dtype is None else dtype, device=data.device if device is None else device, )
[docs]def randn_like( data: Tensor, mean: float = 0.0, stddev: float = 1.0, shape: Optional[Sequence[int]] = None, dtype: Optional[str] = None, device: Optional[str] = None, ) -> Tensor: """ Create a randomly initialized tensor with the same shape, dtype, and device as the given tensor. Parameters ---------- data: Tensor The tensor to copy shape, dtype, and device from. mean: float, optional The mean of the normal distribution. stddev: float, optional The standard deviation of the normal distribution. shape: Sequence[int], optional The shape of new tensor. If None, the shape of data is used. dtype: DataType or str, optional The data type of element of the tensor. If None, the dtype of data is used. device: Device or str, optional The device of the new tensor is created on. If None, the device of data is used. Returns ------- ret: Tensor The created tensor with random values sampled from a normal distribution. """ return randn( shape=data.shape if shape is None else shape, dtype=data.dtype if dtype is None else dtype, device=data.device if device is None else device, mean=mean, stddev=stddev, )
[docs]def from_numpy(nparray: np.ndarray) -> Tensor: """ Create a tensor from a numpy array, sharing the memory with the numpy array when possible. Parameters ---------- nparray: numpy.ndarray The numpy array to create the tensor from. Returns ------- ret: Tensor The created tensor. """ if not isinstance(nparray, np.ndarray): raise TypeError('nparray must be a numpy array') if not nparray.flags['WRITEABLE']: # make a copy if the array is read-only nparray = nparray.copy() if nparray.dtype == np.bool_: return from_dlpack(nparray.astype(np.uint8)).to(dtype='bool') else: return from_dlpack(nparray)
[docs]def from_dlpack(dltensor) -> Tensor: """ Create a hidet tensor from an object that implements the __dlpack__ protocol. Parameters ---------- dltensor: an object that implements the DLPack protocol. The object must have the method `__dlpack__` that returns a PyCapsule object with name `dltensor`. Returns ------- ret: Tensor The hidet tensor that shares the same storage with the DLPack tensor. """ from .impl.dlpack import from_dlpack_capsule if not hasattr(dltensor, '__dlpack__'): raise RuntimeError('Expect a dltensor that implements __dlpack__ method.') return from_dlpack_capsule(dltensor.__dlpack__())
[docs]def from_torch(torch_tensor): """Create a hidet tensor from pytorch tensor. The created tensor shared the same memory as given pytorch tensor. Thus, any content modification on one tensor would be reflected on the other one. Parameters ---------- torch_tensor: torch.Tensor The pytorch tensor. Returns ------- ret: Tensor The created hidet tensor. """ import torch if not isinstance(torch_tensor, torch.Tensor): raise ValueError('Expect a torch.Tensor, got {}'.format(type(torch_tensor))) if torch_tensor.requires_grad: torch_tensor = torch_tensor.detach() # raise ValueError('Please first call .detach() on the pytorch tensor before converting it to hidet.') assert isinstance(torch_tensor, torch.Tensor) if torch_tensor.dtype == torch.bool: # exporting torch.bool to dlpack is not supported by pytorch yet return from_dlpack(torch_tensor.to(dtype=torch.uint8)).to(dtype='bool') else: return from_dlpack(torch_tensor)
[docs]def asarray(obj, /, *, dtype=None, device=None) -> Tensor: """ Convert a list, tuple, or numpy ndarray to a hidet tensor. Parameters ---------- obj: bool, int, float, List, Tuple, Tensor, np.ndarray The object to be converted. dtype: DataType or str, optional The data type of the output tensor. device: Device or str The device of the output tensor. Returns ------- ret: Tensor The hidet tensor converted from given object. """ from hidet.ir.dtypes import dtype_to_numpy if isinstance(obj, Tensor): ret = obj elif isinstance(obj, np.ndarray): ret = from_numpy(obj) else: array = np.array(obj, dtype=dtype_to_numpy(data_type(dtype)) if dtype else None) if array.dtype == np.float64: # numpy uses float64 as the default float data type, convert it to float32 as hidet takes float32 as default array = array.astype(np.float32) ret = from_numpy(array) return ret.to(dtype=dtype, device=device)