328 lines
15 KiB
Python
328 lines
15 KiB
Python
from __future__ import annotations
|
|
import math
|
|
from typing import Self, cast
|
|
from tinygrad.dtype import DType, DTypeLike, dtypes, least_upper_dtype, to_dtype, bitcast
|
|
from tinygrad.helpers import all_int, argfix, ceildiv, prod, TRAINING
|
|
from tinygrad.mixin.op import OpMixin
|
|
from tinygrad.device import canonicalize_device
|
|
|
|
|
|
class RandMixin(OpMixin):
|
|
@staticmethod
|
|
def _threefry_random_bits(key, counts0, counts1):
|
|
x = (counts1.cast(dtypes.uint64) << 32) | counts0.cast(dtypes.uint64)
|
|
x = x.threefry((key[1].cast(dtypes.uint64) << 32) | key[0].cast(dtypes.uint64))
|
|
return x.cast(dtypes.uint32).cat((x >> 32).cast(dtypes.uint32))
|
|
|
|
@classmethod
|
|
def random_bits(cls, key:Self, counter:Self, num:int) -> Self:
|
|
low, high = counter[0:1], counter[1:2]
|
|
bits = []
|
|
for i in range(0, num, dtypes.uint32.max):
|
|
chunk_num = min(num - i, dtypes.uint32.max)
|
|
c_low = low + (i & 0xffffffff)
|
|
c_high = high + (i >> 32) + (c_low < low)
|
|
new_key = cls._threefry_random_bits(key, c_low, c_high)
|
|
counts0 = cls.arange(ceildiv(chunk_num, 2), dtype=dtypes.uint32)
|
|
counts1 = counts0 + ceildiv(chunk_num, 2)
|
|
bits.append(cls._threefry_random_bits(new_key, counts0, counts1)[:chunk_num])
|
|
return bits[0].cat(*bits[1:]) if bits else counter[0:0]
|
|
|
|
@staticmethod
|
|
def _bits_to_rand(bits, shape:tuple[int, ...], dtype:DType):
|
|
_, nmant = dtypes.finfo(dtype)
|
|
uint_dtype = {1: dtypes.uint8, 2: dtypes.uint16, 4: dtypes.uint32, 8: dtypes.uint64}[dtype.itemsize]
|
|
uint_bits = bits.bitcast(uint_dtype)
|
|
float_one_bits = bitcast(1.0, dtype, uint_dtype)
|
|
return uint_bits.rshift(dtype.bitsize - nmant).bitwise_or(float_one_bits).bitcast(dtype)[:prod(shape)].sub(1).reshape(shape)
|
|
|
|
@classmethod
|
|
def _rand(cls, key:Self, counter:Self, shape:tuple[int, ...], dtype:DType, contiguous:bool=True) -> Self:
|
|
bits = cls.random_bits(key, counter, ceildiv(prod(shape) * dtype.itemsize, 4))
|
|
out = cls._bits_to_rand(bits, shape, dtype)
|
|
return out.contiguous() if contiguous else out
|
|
|
|
@staticmethod
|
|
def _next_counter(device:str, num:int):
|
|
raise NotImplementedError("_next_counter requires the stateful per-device RNG counter, only implemented on Tensor")
|
|
|
|
@classmethod
|
|
def rand(cls, *shape, device:str|None=None, dtype:DTypeLike|None=None, contiguous:bool=True) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[0, 1)`.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
t = Tensor.rand(2, 3)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
dt = to_dtype(dtype or dtypes.default_float)
|
|
if not dtypes.is_float(dt) or dt in dtypes.weaks: raise ValueError(f"rand only supports concrete float dtypes, got {dt}")
|
|
if not all_int(shape:=argfix(*shape)) or not all(s >= 0 for s in shape): raise ValueError(f"invalid input {shape=}")
|
|
if device is not None and not isinstance(device, str): raise ValueError(f"rand only supports single device, got {device=}")
|
|
device = cast(str, canonicalize_device(device))
|
|
key, counter = cls._next_counter(device, ceildiv(prod(shape) * dt.itemsize, 4))
|
|
return cls._rand(key, counter, shape, dt, contiguous=contiguous)
|
|
|
|
def rand_like(self, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the same shape and sharding as `self`, filled with random values from a uniform distribution over the interval `[0, 1)`.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = Tensor.ones(2, 3)
|
|
print(Tensor.rand_like(t).numpy())
|
|
```
|
|
"""
|
|
if isinstance(self.device, tuple):
|
|
if kwargs.pop("device", None) is not None: raise RuntimeError("cannot specify `device` on `*_like` of a multi device tensor")
|
|
dtype = kwargs.pop("dtype", self.dtype)
|
|
return self._multi_like(lambda shape, dev: type(self).rand(*shape, dtype=dtype, device=dev, **kwargs))
|
|
return type(self).rand(*self.shape, device=kwargs.pop("device", self.device), dtype=kwargs.pop("dtype", self.dtype), **kwargs)
|
|
|
|
def randn_like(self, dtype:DTypeLike|None=None, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the same shape and sharding as `self`, filled with random values from a normal distribution with mean 0 and variance 1.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = Tensor.ones(2, 3)
|
|
print(Tensor.randn_like(t).numpy())
|
|
```
|
|
"""
|
|
if (dt:=to_dtype(dtype or self.dtype)) in dtypes.weaks and dtype is None: raise ValueError(f"randn_like requires an explicit dtype for {dt}")
|
|
src = self.stack(self).rand_like(**{**kwargs, "dtype": dtypes.float32})
|
|
# https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
|
|
return src[0].mul(2*math.pi).cos().mul((1 - src[1]).log().mul(-2).sqrt()).cast(dt)
|
|
|
|
@classmethod
|
|
def randn(cls, *shape, dtype:DTypeLike|None=None, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random values from a normal distribution with mean `0` and standard deviation `1`.
|
|
If `dtype` is not specified, the default type is used.
|
|
|
|
You can pass in the `device` keyword argument to control device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.randn(2, 3).numpy())
|
|
```
|
|
"""
|
|
return cls.empty(*shape, **kwargs).randn_like(dtype=dtype) # type: ignore[attr-defined]
|
|
|
|
@classmethod
|
|
def randint(cls, *shape, low=0, high=10, dtype=dtypes.int32, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random integer values generated uniformly from the interval `[low, high)`.
|
|
Requires `low < high`. If `dtype` is not specified, the default type is used.
|
|
|
|
You can pass in the `device` keyword argument to control device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.randint(2, 3, low=5, high=10).numpy())
|
|
```
|
|
"""
|
|
if not all_int([low, high]): raise TypeError(f"{low=} and {high=} must be integers")
|
|
if not dtypes.is_int(dtype := to_dtype(dtype)): raise TypeError(f"{dtype=} must be int")
|
|
if low >= high: raise ValueError(f"Tensor.randint requires low < high, got {low=}, {high=}")
|
|
return cls.uniform(*shape, low=low, high=high, dtype=dtype, **kwargs)
|
|
|
|
@classmethod
|
|
def normal(cls, *shape, mean=0.0, std=1.0, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random values from a normal distribution with the given `mean` and standard deviation `std`.
|
|
Requires `std >= 0`.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.normal(2, 3, mean=10, std=2).numpy())
|
|
```
|
|
"""
|
|
if std < 0: raise ValueError(f"Tensor.normal requires std >= 0, got {std=}")
|
|
return std * cls.randn(*shape, **kwargs) + mean
|
|
|
|
@classmethod
|
|
def uniform(cls, *shape, low=0.0, high=1.0, dtype:DTypeLike|None=None, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[low, high)`.
|
|
Requires `low < high`.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.uniform(2, 3, low=2, high=10).numpy())
|
|
```
|
|
"""
|
|
if not all_int(shape:=argfix(*shape)) or not all(s >= 0 for s in shape): raise ValueError(f"invalid input {shape=}")
|
|
if low >= high: raise ValueError(f"Tensor.uniform requires low < high, got {low=}, {high=}")
|
|
return ((high-low) * cls.rand(*shape, **kwargs)).cast(dtype or dtypes.default_float) + low
|
|
|
|
@classmethod
|
|
def scaled_uniform(cls, *shape, **kwargs) -> Self:
|
|
"""
|
|
Creates a tensor with the given shape, filled with random values from a uniform distribution
|
|
over the interval `[-prod(shape)**-0.5, prod(shape)**-0.5)`.
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.scaled_uniform(2, 3).numpy())
|
|
```
|
|
"""
|
|
return cls.uniform(*shape, low=-1.0, high=1.0, **kwargs).mul(prod(argfix(*shape))**-0.5)
|
|
|
|
@classmethod
|
|
def glorot_uniform(cls, *shape, **kwargs) -> Self:
|
|
"""
|
|
<https://www.tensorflow.org/api_docs/python/tf/keras/initializers/GlorotUniform>
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.glorot_uniform(2, 3).numpy())
|
|
```
|
|
"""
|
|
bound = (6 / (argfix(*shape)[0]+prod(argfix(*shape)[1:]))) ** 0.5
|
|
return cls.uniform(*shape, low=-bound, high=bound, **kwargs)
|
|
|
|
@classmethod
|
|
def kaiming_uniform(cls, *shape, a:float = 0.01, **kwargs) -> Self:
|
|
"""
|
|
<https://pytorch.org/docs/stable/_modules/torch/nn/init.html#kaiming_uniform_>
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.kaiming_uniform(2, 3).numpy())
|
|
```
|
|
"""
|
|
bound = (6 / (1 + a ** 2) / prod(argfix(*shape)[1:])) ** 0.5
|
|
return cls.uniform(*shape, low=-bound, high=bound, **kwargs)
|
|
|
|
@classmethod
|
|
def kaiming_normal(cls, *shape, a:float = 0.01, **kwargs) -> Self:
|
|
"""
|
|
<https://pytorch.org/docs/stable/_modules/torch/nn/init.html#kaiming_normal_>
|
|
|
|
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
|
Additionally, all other keyword arguments are passed to the constructor of the tensor.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.kaiming_normal(2, 3).numpy())
|
|
```
|
|
"""
|
|
std = (2 / (1 + a ** 2) / prod(argfix(*shape)[1:])) ** 0.5
|
|
return cls.normal(*shape, mean=0.0, std=std, **kwargs)
|
|
|
|
@classmethod
|
|
def randperm(cls, n:int, device=None, dtype=dtypes.int32, **kwargs) -> Self:
|
|
"""
|
|
Returns a tensor with a random permutation of integers from `0` to `n-1`.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
print(Tensor.randperm(6).numpy())
|
|
```
|
|
"""
|
|
return cls.rand(n, device=device, **kwargs).argsort().cast(dtype)
|
|
|
|
def multinomial(self, num_samples:int = 1, replacement:bool = False) -> Self:
|
|
"""
|
|
Returns a tensor with `num_samples` indices sampled from a multinomial distribution weighted by `self`.
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
t = Tensor([1, 2, 3, 4])
|
|
print(t.multinomial(20, replacement=True).numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
t = Tensor([1, 2, 3, 4])
|
|
print(t.multinomial(3, replacement=False).numpy())
|
|
```
|
|
"""
|
|
assert 1 <= self.ndim <= 2 and num_samples > 0, f"{self.ndim=} must be 1 or 2 dim, {num_samples=} must be positive"
|
|
weight = self.unsqueeze(0) if self.ndim == 1 else self
|
|
assert replacement or num_samples <= weight.shape[1], "no replacement samples must not exceed population size"
|
|
if replacement or num_samples == 1:
|
|
cdf = (cw := weight.cumsum(1).float()) / cw[:, -1].unsqueeze(1)
|
|
unif_samples = type(self).rand(num_samples, cdf.shape[0], 1).to(self.device) # type: ignore[attr-defined]
|
|
indices = (unif_samples >= cdf).sum(2).permute((1, 0))
|
|
else:
|
|
# Efraimidis-Spirakis
|
|
indices = (weight.rand_like(dtype=dtypes.float32).log2() / weight).topk(num_samples, dim=1)[1]
|
|
return (indices.squeeze(0) if self.ndim == 1 else indices).cast(dtypes.int32)
|
|
|
|
def dropout(self, p=0.5) -> Self:
|
|
"""
|
|
Applies dropout to `self`.
|
|
|
|
NOTE: dropout is only applied when `TRAINING` is set (e.g. inside `Context(TRAINING=1)`).
|
|
|
|
- Paper: https://jmlr.org/papers/v15/srivastava14a.html
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
Tensor.manual_seed(42)
|
|
t = Tensor.randn(2, 2)
|
|
with Context(TRAINING=1):
|
|
print(t.dropout().numpy())
|
|
```
|
|
"""
|
|
if not 0 <= p <= 1: raise ValueError(f"{p=} is out of range [0, 1]")
|
|
if not TRAINING or p == 0: return self
|
|
if p == 1: return self.const_like(0)
|
|
return (self.rand_like(dtype=dtypes.default_float, contiguous=False) >= p).contiguous().where(self, 0) / (1.0 - p)
|
|
|
|
def scaled_dot_product_attention(self, key:Self, value:Self, attn_mask:Self|None=None, dropout_p:float=0.0,
|
|
is_causal:bool=False, enable_gqa:bool=False) -> Self:
|
|
"""
|
|
Computes scaled dot-product attention.
|
|
`self` is the query tensor, `key` is the key tensor, and `value` is the value tensor.
|
|
|
|
- Paper: https://arxiv.org/abs/1706.03762v7
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
q = Tensor.randn(2, 4, 8)
|
|
k = Tensor.randn(2, 4, 8)
|
|
v = Tensor.randn(2, 4, 8)
|
|
print(q.scaled_dot_product_attention(k, v).numpy())
|
|
```
|
|
"""
|
|
# GQA: https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
|
|
if enable_gqa:
|
|
key = key.repeat_interleave(int(self.shape[-3] // key.shape[-3]), dim=-3)
|
|
value = value.repeat_interleave(int(self.shape[-3] // value.shape[-3]), dim=-3)
|
|
|
|
q = self
|
|
qk = q.matmul(key.transpose(-2,-1), dtype=least_upper_dtype(q.dtype, key.dtype, dtypes.float32)) / math.sqrt(q.shape[-1])
|
|
# handle attention mask
|
|
if is_causal:
|
|
if attn_mask is not None: raise RuntimeError("cannot set attn_mask when is_causal=True")
|
|
attn_mask = qk.const_like(True, dtypes.bool).tril()
|
|
if attn_mask is not None:
|
|
if attn_mask.dtype == dtypes.bool: attn_mask = attn_mask.where(0, -float("inf"))
|
|
qk = qk + attn_mask
|
|
return qk.cast(self.dtype).softmax(-1).dropout(dropout_p) @ value
|