IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
163
artifacts/package_runtime/tinygrad/mixin/creation.py
Normal file
163
artifacts/package_runtime/tinygrad/mixin/creation.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from typing import TYPE_CHECKING, Callable, Self
|
||||
from tinygrad.dtype import ConstType, DType, DTypeLike, Invalid, dtypes, to_dtype
|
||||
from tinygrad.helpers import argfix, prod
|
||||
from tinygrad.mixin.dtype import DTypeMixin
|
||||
from tinygrad.mixin.movement import MovementMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
|
||||
class CreationMixin(DTypeMixin, MovementMixin):
|
||||
@staticmethod
|
||||
def const(b, dtype=None): raise NotImplementedError
|
||||
|
||||
def const_like(self, b: ConstType, dtype:DType|None=None) -> Self: return self._wrap_uop(self._uop.const_like(b, dtype))
|
||||
|
||||
def _multi_like(self, fxn:'Callable[[tuple[sint, ...], str|None], Self]') -> Self:
|
||||
from tinygrad.uop.ops import UOp
|
||||
assert isinstance(self.device, tuple), f"_multi_like needs a multi device tensor, got {self.device}"
|
||||
if self._uop.axis is None: return self._wrap_uop(fxn(self.shape, None)._uop.shard(self.device, None))
|
||||
return self._wrap_uop(UOp.mstack(*[fxn(self._uop.shard_shape, d)._uop for d in self.device]).unshard(self._uop.axis))
|
||||
|
||||
@classmethod
|
||||
def empty(cls, *shape, device:str|tuple[str, ...]|None=None, dtype:DTypeLike|None=None) -> Self:
|
||||
"""
|
||||
Creates an empty tensor with the given shape.
|
||||
|
||||
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"
|
||||
t = Tensor.empty(2, 3)
|
||||
print(t.shape)
|
||||
```
|
||||
"""
|
||||
from tinygrad.uop.ops import UOp, to_max_shape
|
||||
from tinygrad.device import canonicalize_device
|
||||
dt = to_dtype(dtype) if dtype is not None else dtypes.default_float
|
||||
new_shape = argfix(*shape)
|
||||
max_shape = to_max_shape(new_shape)
|
||||
u = UOp.new_buffer(canonicalize_device(device), prod(max_shape), dt).reshape(max_shape).shrink_to(new_shape)
|
||||
return cls._wrap_uop(u)
|
||||
|
||||
def empty_like(self, dtype: DTypeLike|None=None, device: str|tuple[str, ...]|None=None) -> Self:
|
||||
"""
|
||||
Creates an empty tensor with the same shape as `self`.
|
||||
If `dtype` is not specified, the dtype of `self` is used.
|
||||
"""
|
||||
return self._wrap_uop(self._uop.empty_like(dtype, device))
|
||||
|
||||
@classmethod
|
||||
def invalids(cls, *shape, device:str|tuple[str, ...]|None=None, dtype:DTypeLike|None=None) -> Self:
|
||||
"""
|
||||
Creates a tensor with the given shape, filled with Invalid.
|
||||
|
||||
This is an alternative to Tensor.empty when you want an "anonymous" buffer.
|
||||
|
||||
Eventually Tensor.empty will be replaced by this.
|
||||
"""
|
||||
return cls.full(argfix(*shape), Invalid, dtype=dtype, device=device)
|
||||
|
||||
@classmethod
|
||||
def full(cls, shape:'tuple[sint, ...]', fill_value:'ConstType|UOp', dtype:DTypeLike|None=None,
|
||||
device:str|tuple[str, ...]|None=None, buffer=True) -> Self:
|
||||
"""
|
||||
Creates a tensor with the given shape, filled with the given value.
|
||||
|
||||
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
||||
Pass `buffer=False` to get a broadcast const value instead of a materialized buffer.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.full((2, 3), 42).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.full((2, 3), False).numpy())
|
||||
```
|
||||
"""
|
||||
# TODO: enable this check
|
||||
# if not buffer: assert device is None, "buffer=False does not support device specification"
|
||||
from tinygrad.uop.ops import UOp
|
||||
new_shape = argfix(shape)
|
||||
dt = to_dtype(dtype) if dtype is not None else fill_value.dtype if isinstance(fill_value, UOp) else dtypes.from_py(fill_value)
|
||||
val = cls.const(fill_value, dt)
|
||||
val = val.reshape((1,)*len(new_shape)).expand(new_shape)
|
||||
if not buffer: return val
|
||||
ret = val.empty_like(dt if dtype is not None else None, device)
|
||||
return cls._wrap_uop(ret._uop.after(ret._uop.store(val._uop)))
|
||||
|
||||
def full_like(self, fill_value:ConstType, dtype:DTypeLike|None=None, device:str|tuple[str, ...]|None=None, buffer=True) -> Self:
|
||||
"""
|
||||
Creates a tensor with the same shape as `self`, filled with the given value.
|
||||
If `dtype` is not specified, the dtype of `self` is used.
|
||||
|
||||
You can pass in the `device` keyword argument to control device of the tensor.
|
||||
Pass `buffer=False` to get a broadcast const value instead of a materialized buffer.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.ones(2, 3)
|
||||
print(Tensor.full_like(t, 42).numpy())
|
||||
```
|
||||
"""
|
||||
if isinstance(self.device, tuple):
|
||||
if device is not None: raise RuntimeError("cannot specify `device` on `*_like` of a multi device tensor")
|
||||
return self._multi_like(lambda shape, dev: type(self).full(shape, fill_value, dtype=dtype or self.dtype, device=dev, buffer=buffer))
|
||||
return type(self).full(self.shape, fill_value, dtype=dtype or self.dtype, device=self.device if device is None else device, buffer=buffer)
|
||||
|
||||
@classmethod
|
||||
def zeros(cls, *shape, **kwargs) -> Self:
|
||||
"""
|
||||
Creates a tensor with the given shape, filled with zeros.
|
||||
|
||||
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"
|
||||
print(Tensor.zeros(2, 3).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.zeros(2, 3, dtype=dtypes.int32).numpy())
|
||||
```
|
||||
"""
|
||||
return cls.full(argfix(*shape), 0.0, **kwargs)
|
||||
|
||||
def zeros_like(self, **kwargs) -> Self:
|
||||
"""
|
||||
Creates a tensor with the same shape as `self`, filled with zeros.
|
||||
|
||||
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"
|
||||
t = Tensor.ones(2, 3)
|
||||
print(Tensor.zeros_like(t).numpy())
|
||||
```
|
||||
"""
|
||||
return self.full_like(0, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def ones(cls, *shape, **kwargs) -> Self:
|
||||
"""
|
||||
Creates a tensor with the given shape, filled with ones.
|
||||
|
||||
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"
|
||||
print(Tensor.ones(2, 3).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.ones(2, 3, dtype=dtypes.int32).numpy())
|
||||
```
|
||||
"""
|
||||
return cls.full(argfix(*shape), 1.0, **kwargs)
|
||||
|
||||
def ones_like(self, **kwargs) -> Self:
|
||||
"""
|
||||
Creates a tensor with the same shape as `self`, filled with ones.
|
||||
|
||||
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"
|
||||
t = Tensor.zeros(2, 3)
|
||||
print(Tensor.ones_like(t).numpy())
|
||||
```
|
||||
"""
|
||||
return self.full_like(1, **kwargs)
|
||||
139
artifacts/package_runtime/tinygrad/mixin/dtype.py
Normal file
139
artifacts/package_runtime/tinygrad/mixin/dtype.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from typing import TYPE_CHECKING, Self
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, to_dtype
|
||||
from tinygrad.uop import Ops
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import UOp
|
||||
|
||||
class DTypeMixin:
|
||||
@property
|
||||
def dtype(self) -> DType: raise NotImplementedError
|
||||
@property
|
||||
def _uop(self) -> 'UOp': raise NotImplementedError
|
||||
@classmethod
|
||||
def _wrap_uop(cls, u:'UOp') -> Self: raise NotImplementedError
|
||||
|
||||
def cast(self, dtype:DTypeLike) -> Self:
|
||||
"""
|
||||
Casts `self` to the given `dtype`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, 2.5, 3], dtype=dtypes.float)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.cast(dtypes.int32)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.cast(dtypes.uint8)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
return self if self.dtype == (dt:=to_dtype(dtype)) else self._wrap_uop(self._uop.alu(Ops.CAST, arg=dt))
|
||||
|
||||
def bitcast(self, dtype:DTypeLike) -> Self:
|
||||
"""
|
||||
Bitcasts `self` to the given `dtype`. If the itemsize differs, the last axis is rescaled.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, 2, 3], dtype=dtypes.int32)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.bitcast(dtypes.uint32)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
dt = to_dtype(dtype)
|
||||
if self.dtype in dtypes.weaks or dt in dtypes.weaks: raise RuntimeError(f"bitcast requires concrete dtypes, got {self.dtype} -> {dt}")
|
||||
return self if self.dtype == dt else self._wrap_uop(self._uop.alu(Ops.BITCAST, arg=dt))
|
||||
|
||||
def element_size(self) -> int:
|
||||
"""
|
||||
Returns the size in bytes of an individual element in the tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([5], dtype=dtypes.int16)
|
||||
print(t.element_size())
|
||||
```
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: raise RuntimeError(f"element_size requires a concrete dtype, got {self.dtype}")
|
||||
return self.dtype.itemsize
|
||||
|
||||
def is_floating_point(self) -> bool:
|
||||
"""
|
||||
Returns `True` if the tensor contains floating point types, i.e. is one of `dtypes.float64`, `dtypes.float32`,
|
||||
`dtypes.float16`, `dtypes.bfloat16`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([8, 9], dtype=dtypes.float32)
|
||||
print(t.is_floating_point())
|
||||
```
|
||||
"""
|
||||
return dtypes.is_float(self.dtype)
|
||||
|
||||
def float(self) -> Self:
|
||||
"""
|
||||
Convenience method to cast `self` to a `float32` Tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, 2, 3], dtype=dtypes.int32)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.float()
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(dtypes.float32)
|
||||
|
||||
def half(self) -> Self:
|
||||
"""
|
||||
Convenience method to cast `self` to a `float16` Tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, 2, 3], dtype=dtypes.int32)
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.half()
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(dtypes.float16)
|
||||
|
||||
def int(self) -> Self:
|
||||
"""
|
||||
Convenience method to cast `self` to a `int32` Tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1.5, -0.5, 0.0, 0.5, 1.5])
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.int()
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(dtypes.int32)
|
||||
|
||||
def bool(self) -> Self:
|
||||
"""
|
||||
Convenience method to cast `self` to a `bool` Tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, 0, 1])
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = t.bool()
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(dtypes.bool)
|
||||
|
||||
def bfloat16(self) -> Self: return self.cast(dtypes.bfloat16)
|
||||
def double(self) -> Self: return self.cast(dtypes.double)
|
||||
def long(self) -> Self: return self.cast(dtypes.long)
|
||||
def short(self) -> Self: return self.cast(dtypes.short)
|
||||
1087
artifacts/package_runtime/tinygrad/mixin/elementwise.py
Normal file
1087
artifacts/package_runtime/tinygrad/mixin/elementwise.py
Normal file
File diff suppressed because it is too large
Load Diff
129
artifacts/package_runtime/tinygrad/mixin/gradient.py
Normal file
129
artifacts/package_runtime/tinygrad/mixin/gradient.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from typing import cast
|
||||
import math, dataclasses
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
|
||||
from tinygrad.helpers import argsort
|
||||
from tinygrad.dtype import sum_acc_dtype
|
||||
|
||||
def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
|
||||
if op == Ops.ADD: return (ctx._broadcast_to(ret.src[0].shape),)
|
||||
if op == Ops.MAX: return (((mask:=ret.src[0].eq(ret).cast(ctx.dtype))/mask._rop(Ops.ADD, tuple(range(ret.arg[1])))) * ctx,)
|
||||
if op == Ops.MUL: return (ctx * ret / ret.src[0],)
|
||||
|
||||
def _compact_params(body:UOp, all_args:tuple[UOp, ...]) -> tuple[UOp, tuple[UOp, ...]]:
|
||||
"""Remove unused PARAMs from body and return compacted (body, args)."""
|
||||
used = sorted({p.arg.slot: p for p in body.toposort() if p.op is Ops.PARAM}.items())
|
||||
body = body.substitute({p: p.replace(arg=dataclasses.replace(p.arg, slot=j)) for j,(_, p) in enumerate(used)}, walk=True)
|
||||
return body, tuple(all_args[i] for i,_ in used)
|
||||
|
||||
def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
fxn, args = k.src[0], k.src[1:]
|
||||
if k.arg.grad_fxn is not None:
|
||||
# put const on a device, also TODO why do we still have NOOP...
|
||||
def on_dev(g, i): return g.clone(device=args[i].device if k.op is Ops.CALL else k.device) if g.device is None else g
|
||||
if ctx.op is Ops.TUPLE:
|
||||
real = [on_dev(g, i) for i,g in enumerate(ctx.src) if g.op is not Ops.NOOP]
|
||||
return (None,) + (k.arg.grad_fxn(*real, call=k) if len(real) > 1 else k.arg.grad_fxn(real[0], k))
|
||||
return (None,) + k.arg.grad_fxn(on_dev(ctx, 0), k)
|
||||
assert fxn.op is Ops.TUPLE, f"expected TUPLE body for gradient, got {fxn.op}"
|
||||
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
grad_args = ctx.src
|
||||
root_grad = UOp(Ops.TUPLE, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
|
||||
g if g.base.op is Ops.CONST else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
|
||||
grads = compute_gradient(fxn, root_grad, set(params.values()))
|
||||
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
|
||||
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(fxn.src)} if k.arg.precompile else {}
|
||||
fwd_outs = tuple(k.gettuple(i) for i in range(len(fxn.src))) if k.arg.precompile else ()
|
||||
# collect needed gradient bodies, compact unused params, create a single backward CALL
|
||||
grad_bodies = [(i, grads[p]) for i in needed if (p:=params.get(i)) is not None and p in grads]
|
||||
bwd_body = UOp.maketuple(*(gb for _, gb in grad_bodies)).substitute(fwd_subs, walk=True)
|
||||
bwd_body, compact_args = _compact_params(bwd_body, (*args, *grad_args, *fwd_outs))
|
||||
bwd_call = bwd_body.call(*compact_args, name=(k.arg.name or "")+"_backward", precompile=k.arg.precompile_backward)
|
||||
gb_map = {i: idx for idx, (i, _) in enumerate(grad_bodies)}
|
||||
return (None,) + tuple(bwd_call.gettuple(gb_map[i]) if i in gb_map else None for i in range(len(args)))
|
||||
|
||||
# ctx is grad_output
|
||||
pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="ret"), lambda ctx, ret: (ctx.cast(ret.src[0].dtype),)),
|
||||
(UPat(Ops.RECIPROCAL, name="ret"), lambda ctx, ret: (-ctx * ret * ret,)),
|
||||
(UPat(Ops.SIN, name="ret"), lambda ctx, ret: ((math.pi/2 - ret.src[0]).sin() * ctx,)),
|
||||
(UPat(Ops.LOG2, name="ret"), lambda ctx, ret: (ctx / (ret.src[0] * math.log(2)),)),
|
||||
(UPat(Ops.EXP2, name="ret"), lambda ctx, ret: (ret * ctx * math.log(2),)),
|
||||
(UPat(Ops.SQRT, name="ret"), lambda ctx, ret: (ctx / (ret*2),)),
|
||||
(UPat(Ops.TRUNC), lambda ctx: (ctx.const_like(0),)),
|
||||
(UPat((Ops.CMPLT, Ops.CMPNE)), lambda: (None, None)),
|
||||
(UPat(Ops.ADD), lambda ctx: (ctx, ctx)),
|
||||
(UPat(Ops.POW, name="ret", src=(UPat.var("b"), UPat.var("e"))), lambda ctx, ret, b, e:
|
||||
(ctx * e.eq(0).where(e, e*b.pow(e-1)), ctx * b.eq(0).where((e<0).where(ret.const_like(-math.inf), 0), ret*b.log2()*math.log(2.0)))),
|
||||
(UPat(Ops.MAX, src=(UPat.var("x"), UPat.var("y"))), lambda ctx, x, y:
|
||||
((x>y).where(ctx, (x.eq(y)).where(ctx * 0.5, 0)), (x<y).where(ctx, (x.eq(y)).where(ctx * 0.5, 0)))),
|
||||
(UPat(Ops.MUL, name="ret"), lambda ctx, ret: (ret.src[1]*ctx, ret.src[0]*ctx)),
|
||||
(UPat(Ops.WHERE, name="ret"), lambda ctx, ret: (None, ret.src[0].where(ctx, ctx.const_like(0)), ret.src[0].where(ctx.const_like(0), ctx))),
|
||||
(UPat(Ops.REDUCE, name="ret"), lambda ctx, ret: reduce_gradient(ctx, ret, ret.arg[0])),
|
||||
(UPat(Ops.CONTIGUOUS), lambda ctx: (ctx,)),
|
||||
(UPat(Ops.CONTIGUOUS_BACKWARD), lambda ctx: (ctx.contiguous(),)),
|
||||
(UPat(Ops.RESHAPE, name="ret"), lambda ctx, ret: (ctx.reshape(ret.src[0].shape), None)),
|
||||
(UPat(Ops.EXPAND), lambda ctx: (ctx, None)),
|
||||
(UPat(Ops.PAD, name="ret"), lambda ctx, ret: (ctx.shrink(tuple([(p[0], s+p[0]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
|
||||
(UPat(Ops.SHRINK, name="ret"), lambda ctx, ret: (ctx.pad(tuple([(p[0], s-p[0]-p[1]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
|
||||
(UPat(Ops.PERMUTE, name="ret"), lambda ctx, ret: (ctx.permute(argsort(ret.marg)),)),
|
||||
(UPat(Ops.FLIP, name="ret"), lambda ctx, ret: (ctx.flip([i for i,x in enumerate(ret.marg) if x]),)),
|
||||
(UPat(Ops.STACK, name="ret"), lambda ctx, ret: tuple(ctx[i] for i in range(len(ret.src)))),
|
||||
(UPat(Ops.COPY, name="ret"), lambda ctx, ret: (ctx.copy_to_device(ret.src[0].device),)),
|
||||
(UPat(Ops.UNSHARD, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
|
||||
(UPat(Ops.TUPLE), lambda ctx: ctx.src),
|
||||
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
|
||||
(ctx, UOp.maketuple(*(ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1))))),
|
||||
# clone/assign gradient passes through to val
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
|
||||
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
|
||||
# there's no gradient for bitcast
|
||||
(UPat(Ops.BITCAST), lambda: (None,)),
|
||||
])
|
||||
|
||||
def _deepwalk(root:UOp, targets:set[UOp]) -> tuple[list[UOp], dict[UOp, bool]]:
|
||||
# compute the target path (top down)
|
||||
in_target_path: dict[UOp, bool] = {}
|
||||
root.topovisit(lambda u: any(in_target_path[x] or x in targets for x in u.src), in_target_path)
|
||||
# don't flow through DETACH or anything not in target path
|
||||
return [node for node in in_target_path if node.op is not Ops.DETACH and in_target_path[node]], in_target_path
|
||||
|
||||
def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp]:
|
||||
walk, in_target_path = _deepwalk(root, targets)
|
||||
grads: dict[UOp, UOp] = {root: root_grad}
|
||||
for t0 in reversed(walk):
|
||||
if t0 not in grads or grads[t0].op is Ops.NOOP: continue
|
||||
# GETTUPLE: accumulate gradient into a TUPLE UOp on the FUNCTION, process when we hit the FUNCTION
|
||||
if t0.op is Ops.GETTUPLE:
|
||||
k = t0.src[0] # the FUNCTION
|
||||
assert k.op is Ops.FUNCTION and k.src[0].op is Ops.TUPLE
|
||||
n_outputs = len(k.src[0].src)
|
||||
prev = grads[k].src if k in grads else tuple(UOp(Ops.NOOP) for _ in range(n_outputs))
|
||||
grads[k] = UOp.maketuple(*(prev[i] + grads[t0] if i == t0.arg and prev[i].op is not Ops.NOOP else
|
||||
grads[t0] if i == t0.arg else prev[i] for i in range(n_outputs)))
|
||||
continue
|
||||
# FUNCTION/CALL: pass needed param set so backward only computes required gradients
|
||||
# (FUNCTION uses implicit TUPLE gradient or grad_fxn; CALL requires an explicit grad_fxn)
|
||||
if t0.op in {Ops.FUNCTION, Ops.CALL}:
|
||||
needed = {i for i, arg in enumerate(t0.src[1:]) if arg in targets or in_target_path.get(arg, False)}
|
||||
lgrads:tuple[UOp|None, ...]|None = call_gradient(grads[t0], t0, needed)
|
||||
else:
|
||||
lgrads = cast(tuple[UOp|None, ...]|None, pm_gradient.rewrite(t0, ctx=grads[t0]))
|
||||
if lgrads is None: raise RuntimeError(f"failed to compute gradient for {t0.op}\n\nin {str(t0)[0:1000]}...")
|
||||
assert len(lgrads) == len(t0.src), f"got {len(lgrads)} gradient, expected {len(t0.src)}"
|
||||
for k,v in zip(t0.src, lgrads):
|
||||
if v is None: continue
|
||||
# a shaped edge's gradient is summed to its source's shape
|
||||
if k._shape is not None and v._shape is not None and k._shape != v._shape:
|
||||
v = v.cast(sum_acc_dtype(v.dtype))._rop(Ops.ADD, broadcast_axes(k.shape, v.shape)).reshape(k.shape).cast(v.dtype)
|
||||
if k in grads and grads[k].op is not Ops.NOOP:
|
||||
if v.op is Ops.TUPLE and grads[k].op is Ops.TUPLE:
|
||||
grads[k] = UOp.maketuple(*(p + n if (p.op is not Ops.NOOP and n.op is not Ops.NOOP) else
|
||||
n if p.op is Ops.NOOP else p for p, n in zip(grads[k].src, v.src)))
|
||||
else: grads[k] = grads[k] + v
|
||||
else: grads[k] = v
|
||||
if len(forward_metadata:=all_metadata.get(t0, ())):
|
||||
backward_metadata = tuple(dataclasses.replace(x, backward=True) for x in forward_metadata)
|
||||
# we add the backward metadata to everything new in the graph
|
||||
for bw_uop in v.toposort(lambda x: x not in (t0, *t0.src, grads[t0])):
|
||||
all_metadata[bw_uop] = all_metadata.get(bw_uop, ())+backward_metadata
|
||||
return grads
|
||||
628
artifacts/package_runtime/tinygrad/mixin/movement.py
Normal file
628
artifacts/package_runtime/tinygrad/mixin/movement.py
Normal file
@@ -0,0 +1,628 @@
|
||||
# mixins add syntactic sugar to Tensor and UOp
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Self, Sequence
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.helpers import prod, argfix, argsort, flatten, dedup, make_tuple, ceildiv, round_up, all_int
|
||||
from tinygrad.uop.ops import resolve, smax, _align_left, _broadcast_shape, broadcast_axes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import sint
|
||||
|
||||
|
||||
class MovementMixin:
|
||||
# required to implement
|
||||
def _mop(self, op: Ops, arg) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def shape(self) -> tuple[sint, ...]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def device(self) -> str|tuple[str, ...]|None:
|
||||
raise NotImplementedError
|
||||
|
||||
# great functions you get!
|
||||
@property
|
||||
def ndim(self) -> int:
|
||||
"""
|
||||
Returns the number of dimensions in the tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[1, 2], [3, 4]])
|
||||
print(t.ndim)
|
||||
```
|
||||
"""
|
||||
return len(self.shape)
|
||||
|
||||
def numel(self) -> sint:
|
||||
"""
|
||||
Returns the total number of elements in the tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
|
||||
print(t.numel())
|
||||
```
|
||||
"""
|
||||
return prod(self.shape)
|
||||
|
||||
def size(self, dim:int|None=None) -> sint|tuple[sint, ...]:
|
||||
"""
|
||||
Returns the size of the tensor. If `dim` is specified, return the length along dimension `dim`. Otherwise return the shape of the tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[4, 5, 6], [7, 8, 9]])
|
||||
print(t.size())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.size(dim=1))
|
||||
```
|
||||
"""
|
||||
return self.shape if dim is None else self.shape[dim]
|
||||
|
||||
def _normalize_indices(self, indices:list) -> list:
|
||||
if len(ell := [i for i,x in enumerate(indices) if x is Ellipsis]) > 1: raise IndexError("indices can only have a single ellipsis")
|
||||
num_real = len(indices) - len(ell) - sum(1 for i in indices if i is None)
|
||||
if num_real > self.ndim: raise IndexError(f"too many indices ({num_real}) for {self.ndim}D")
|
||||
fill_idx = ell[0] if ell else len(indices)
|
||||
indices[fill_idx:fill_idx+1] = [slice(None)] * (self.ndim - num_real)
|
||||
return indices
|
||||
|
||||
def _resolve_dim(self, dim: int, *, extra: bool = False) -> int:
|
||||
total = self.ndim + int(extra)
|
||||
if not -max(1, total) <= dim <= max(1, total) - 1:
|
||||
raise IndexError(f"{dim=} out of range {[-max(1, total), max(1, total) - 1]}")
|
||||
return dim + total if dim < 0 else dim
|
||||
|
||||
def _parse_view_index(self, index, size: sint) -> dict:
|
||||
# parses a single slice/int/None/sint index into {boundary, stride, size, collapse_dim}
|
||||
from tinygrad.uop.ops import UOp, sint
|
||||
match index:
|
||||
case None: return {"size":1, "boundary":(0,1), "stride":1, "collapse_dim":False}
|
||||
case int() | UOp(): # sint
|
||||
if resolve(index >= size, False) or resolve(index < -size, False): raise IndexError(f"{index=} is out of bounds with {size=}")
|
||||
# TODO: is this right for (negative) symbolic?
|
||||
b = index if resolve(index >= 0, False) else index + size
|
||||
return {"size":size, "boundary":(b, b+1), "stride":1, "collapse_dim":True}
|
||||
case slice():
|
||||
if not all(s is None or isinstance(s, sint) for s in (index.start, index.stop, index.step)):
|
||||
raise TypeError(f"slice {index=} is not supported")
|
||||
if resolve(index.step == 0, False): raise ValueError(f"{index=} cannot have 0 as step")
|
||||
start, stop = 0 if index.start is None else index.start, size if index.stop is None else index.stop
|
||||
step = 1 if index.step is None else index.step
|
||||
# resolve negative int bounds against the (possibly symbolic) size, like slice.indices
|
||||
if isinstance(start, int) and start < 0: start = start + size
|
||||
if isinstance(stop, int) and stop < 0: stop = stop + size
|
||||
if all_int((start, stop, step)):
|
||||
# handle int slicing (clamp, stride)
|
||||
*bound, stride = index.indices(int(size.vmax) if isinstance(size, UOp) else size)
|
||||
bound = [0, 0] if stride * (bound[1] - bound[0]) < 0 else ([bound[1]+1, bound[0]+1] if stride < 0 else bound)
|
||||
return {"size":ceildiv(bound[1]-bound[0], abs(stride)), "boundary":tuple(bound), "stride":stride, "collapse_dim":False}
|
||||
if resolve(step == 1, False) and resolve((stop-start) >= 0, False):
|
||||
return {"size":stop-start, "boundary":(start, stop), "stride":step, "collapse_dim":False}
|
||||
raise TypeError(f"slice {index=} is not supported")
|
||||
case _: raise IndexError(f"{type(index).__name__} indexing is not supported")
|
||||
|
||||
def _apply_view_ops(self, mops:list) -> Self:
|
||||
# applies shrink + flip + stride from a list of parsed view indices
|
||||
# flip negative strides
|
||||
x = self.shrink(tuple(m["boundary"] for m in mops)).flip(tuple(i for i, m in enumerate(mops) if m["stride"] < 0))
|
||||
strides = tuple(abs(m["stride"]) for m in mops)
|
||||
# apply stride
|
||||
if any(st != 1 for st in strides):
|
||||
if not all_int(x.shape): raise RuntimeError("symbolic shape not supported")
|
||||
x = x.pad_to(tuple(round_up(s, st) for s, st in zip(x.shape, strides)))
|
||||
x = x.reshape(tuple(flatten((s // st, st) for s, st in zip(x.shape, strides))))
|
||||
x = x.shrink_to(tuple(flatten((s, 1) for s in x.shape[::2]))).reshape(x.shape[::2])
|
||||
return x
|
||||
|
||||
def _broadcast_to(self, new_shape: tuple[sint, ...]) -> Self:
|
||||
if self.shape == new_shape:
|
||||
return self
|
||||
if self.ndim > len(new_shape):
|
||||
raise ValueError(f"cannot broadcast tensor to fewer dimensions. shape={self.shape} to {new_shape=}")
|
||||
# first unsqueeze left with 1s https://data-apis.org/array-api/latest/API_specification/broadcasting.html
|
||||
shape, _ = _align_left(self.shape, new_shape)
|
||||
# for each dimension, check either dim is 1, or it does not change
|
||||
if not all(s == ns or s == 1 for s, ns in zip(shape, new_shape)):
|
||||
raise ValueError(f"cannot broadcast {self.shape} to {new_shape=}")
|
||||
# EXPAND only adds dims on the left. squeeze 1s that need expanding, EXPAND on left, permute back.
|
||||
n_left = len(new_shape) - len(self.shape)
|
||||
expand_at = tuple(i-n_left for i in broadcast_axes(self.shape, new_shape) if i >= n_left)
|
||||
kept = tuple(i for i in range(len(self.shape)) if i not in expand_at)
|
||||
squeezed = self.reshape(tuple(self.shape[i] for i in kept))
|
||||
expanded = squeezed._mop(Ops.EXPAND, arg=new_shape[:n_left] + tuple(new_shape[n_left+i] for i in expand_at))
|
||||
# expanded shape = [left] + [expand_at dims] + [kept dims], permute to new_shape
|
||||
perm = tuple(range(n_left)) + tuple(
|
||||
n_left + (expand_at.index(i) if i in expand_at else len(expand_at) + kept.index(i)) for i in range(len(self.shape)))
|
||||
return expanded.permute(perm)
|
||||
|
||||
def expand(self, shape, *args) -> Self:
|
||||
"""
|
||||
Returns a tensor that is expanded to the shape that is specified.
|
||||
Expand can also increase the number of dimensions that a tensor has.
|
||||
|
||||
Passing a `-1` or `None` to a dimension means that its size will not be changed.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([1, 2, 3])
|
||||
print(t.expand(4, -1).numpy())
|
||||
```
|
||||
"""
|
||||
new_shape = tuple(from_ if to == -1 or to is None else to for from_, to in zip(*(_align_left(self.shape, argfix(shape, *args)))))
|
||||
return self._broadcast_to(new_shape)
|
||||
|
||||
def reshape(self, shape, *args) -> Self:
|
||||
"""
|
||||
Returns a tensor with the same data as the original tensor but with a different shape.
|
||||
`shape` can be passed as a tuple or as separate arguments.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(6)
|
||||
print(t.reshape(2, 3).numpy())
|
||||
```
|
||||
"""
|
||||
# resolve None and args
|
||||
new_shape = tuple([s if s is not None else self.shape[i] for i, s in enumerate(argfix(shape, *args))])
|
||||
# resolve -1
|
||||
if (c := new_shape.count(-1)) > 1:
|
||||
raise RuntimeError(f"only one dimension can be inferred using -1, getting {new_shape}")
|
||||
if c:
|
||||
new_shape = tuple([-prod(self.shape) // prod(new_shape) if s == -1 else s for s in new_shape])
|
||||
if prod(self.shape) != prod(new_shape):
|
||||
raise ValueError(f"size mismatch, can't reshape ({self.shape}) -> ({new_shape})")
|
||||
ret = self._mop(Ops.RESHAPE, arg=new_shape)
|
||||
return self if ret.shape == self.shape else ret
|
||||
|
||||
def pad(self, arg:tuple[tuple[sint, sint] | None, ...]) -> Self:
|
||||
if self.ndim != len(arg):
|
||||
raise ValueError(f"{self.ndim=} != {len(arg)=}")
|
||||
ret = self._mop(Ops.PAD, tuple((x[0], s+x[0]+x[1]) if x is not None else (0, s) for x, s in zip(arg, self.shape)))
|
||||
return self if ret.shape == self.shape else ret
|
||||
|
||||
def shrink(self, arg: tuple[tuple[sint, sint] | None, ...]) -> Self:
|
||||
"""
|
||||
Returns a tensor that shrinks the each axis based on input arg.
|
||||
`arg` must have the same length as `self.ndim`.
|
||||
For each axis, it can be `None`, which means no shrink, or a tuple `(start, end)` that works the same as Python slice.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(9).reshape(3, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.shrink(((None, (1, 3)))).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.shrink((((0, 2), (0, 2)))).numpy())
|
||||
```
|
||||
"""
|
||||
if self.ndim != len(arg):
|
||||
raise ValueError(f"{self.ndim=} != {len(arg)=}")
|
||||
ret = self._mop(Ops.SHRINK, arg=[(x[0], x[1]-x[0]) if x is not None else (0, s) for x, s in zip(arg, self.shape)])
|
||||
return self if ret.shape == self.shape else ret
|
||||
|
||||
def permute(self, order, *args) -> Self:
|
||||
"""
|
||||
Returns a tensor that is a permutation of the original tensor.
|
||||
The new tensor has the same data as the original tensor but with the dimensions permuted according to the order specified.
|
||||
`order` can be passed as a tuple or as separate arguments.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.empty(2, 3, 5)
|
||||
print(t.shape)
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.permute(2, 0, 1).shape)
|
||||
```
|
||||
"""
|
||||
order_arg = tuple(self._resolve_dim(x) for x in argfix(order, *args))
|
||||
if sorted(order_arg) != list(range(self.ndim)):
|
||||
raise RuntimeError(f"order is not a valid permutation, getting {order_arg}")
|
||||
return self._mop(Ops.PERMUTE, arg=order_arg) if order_arg != tuple(range(self.ndim)) else self
|
||||
|
||||
def flip(self, axis, *args) -> Self:
|
||||
"""
|
||||
Returns a tensor that reverses the order of the original tensor along given `axis`.
|
||||
`axis` can be passed as a tuple or as separate arguments.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(6).reshape(2, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.flip(0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.flip((0, 1)).numpy())
|
||||
```
|
||||
"""
|
||||
axis_arg = tuple(self._resolve_dim(x) for x in argfix(axis, *args))
|
||||
assert all(not isinstance(x, bool) and x >= 0 and x < self.ndim for x in axis_arg), f"flip args must be axis ints {axis_arg}"
|
||||
if len(axis_arg) != len(dedup(axis_arg)):
|
||||
raise RuntimeError(f"dim can appear at most once, getting {axis_arg}")
|
||||
flip_arg = tuple([i in axis_arg for i in range(len(self.shape))])
|
||||
return self._mop(Ops.FLIP, arg=flip_arg) if any(flip_arg) else self
|
||||
|
||||
def stack(self, *args: Self, dim: int = 0) -> Self:
|
||||
"""
|
||||
Concatenates self with other tensors in `args` along a new dimension specified by `dim`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t0, t1, t2 = Tensor([1, 2]), Tensor([3, 4]), Tensor([5, 6])
|
||||
print(t0.stack(t1, t2, dim=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t0.stack(t1, t2, dim=1).numpy())
|
||||
```
|
||||
"""
|
||||
tensors = argfix(self, *args)
|
||||
dim = tensors[0]._resolve_dim(dim, extra=True)
|
||||
assert all(t.shape == tensors[0].shape for t in tensors), f"all shapes must match for stack, got {[t.shape for t in tensors]}"
|
||||
ret = tensors[0]._mop(Ops.STACK, arg=tuple(t._uop for t in tensors[1:]))
|
||||
return ret if dim == 0 else ret.permute(tuple(range(1, dim+1)) + (0,) + tuple(range(dim+1, ret.ndim)))
|
||||
|
||||
# **** high level ****
|
||||
|
||||
def shrink_to(self, shape, *args) -> Self:
|
||||
return self.shrink(tuple([None if ns is None else (0, ns) for ns in argfix(shape, *args)]))
|
||||
|
||||
def pad_to(self, shape, *args) -> Self:
|
||||
return self._mop(Ops.PAD, tuple((0, s if ns is None else ns) for s,ns in zip(self.shape, argfix(shape, *args), strict=True)))
|
||||
|
||||
def view(self, shape, *args) -> Self:
|
||||
"""`.view` is an alias for `.reshape`."""
|
||||
return self.reshape(shape, *args)
|
||||
|
||||
def squeeze(self, dim: int | None = None) -> Self:
|
||||
"""
|
||||
Returns a tensor with specified dimensions of input of size 1 removed.
|
||||
If `dim` is not specified, all dimensions with size 1 are removed.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.zeros(2, 1, 2, 1, 2)
|
||||
print(t.squeeze().shape)
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.squeeze(0).shape)
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.squeeze(1).shape)
|
||||
```
|
||||
"""
|
||||
if dim is None:
|
||||
return self.reshape(tuple(dim for dim in self.shape if dim != 1))
|
||||
dim = self._resolve_dim(dim)
|
||||
return self if not self.ndim or self.shape[dim] != 1 else self.reshape(self.shape[:dim] + self.shape[dim + 1 :])
|
||||
|
||||
def unsqueeze(self, dim: int) -> Self:
|
||||
"""
|
||||
Returns a tensor with a new dimension of size 1 inserted at the specified `dim`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([1, 2, 3, 4])
|
||||
print(t.unsqueeze(0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.unsqueeze(1).numpy())
|
||||
```
|
||||
"""
|
||||
dim = self._resolve_dim(dim, extra=True)
|
||||
return self.reshape(self.shape[:dim] + (1,) + self.shape[dim:])
|
||||
|
||||
@property
|
||||
def T(self) -> Self:
|
||||
"""`.T` is an alias for `.transpose()`."""
|
||||
return self.transpose()
|
||||
|
||||
def transpose(self, dim0=1, dim1=0) -> Self:
|
||||
"""
|
||||
Returns a tensor that is a transposed version of the original tensor.
|
||||
The given dimensions `dim0` and `dim1` are swapped.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(6).reshape(2, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.transpose(0, 1).numpy())
|
||||
```
|
||||
"""
|
||||
order = list(range(self.ndim))
|
||||
order[dim0], order[dim1] = order[dim1], order[dim0]
|
||||
return self.permute(order)
|
||||
|
||||
def flatten(self, start_dim=0, end_dim=-1) -> Self:
|
||||
"""
|
||||
Flattens the tensor by reshaping it into a one-dimensional tensor.
|
||||
If `start_dim` or `end_dim` are passed, only dimensions starting with `start_dim` and ending with `end_dim` are flattened.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(8).reshape(2, 2, 2)
|
||||
print(t.flatten().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.flatten(start_dim=1).numpy())
|
||||
```
|
||||
"""
|
||||
start_dim, end_dim = self._resolve_dim(start_dim), self._resolve_dim(end_dim)
|
||||
return self.reshape(self.shape[:start_dim] + (prod(self.shape[start_dim : end_dim + 1]),) + self.shape[end_dim + 1 :])
|
||||
|
||||
def unflatten(self, dim: int, sizes: tuple[int, ...]) -> Self:
|
||||
"""
|
||||
Unflattens dimension `dim` of the tensor into multiple dimensions specified by `sizes`. `Tensor.flatten()` is the inverse of this function.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.ones(3, 4, 1).unflatten(1, (2, 2)).shape)
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.ones(3, 4, 1).unflatten(1, (-1, 2)).shape)
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor.ones(5, 12, 3).unflatten(-2, (2, 2, 3, 1, 1)).shape)
|
||||
```
|
||||
"""
|
||||
dim = self._resolve_dim(dim)
|
||||
return self.reshape(self.shape[:dim] + sizes + self.shape[dim + 1 :])
|
||||
|
||||
def rearrange(self, formula: str, **sizes) -> Self:
|
||||
"""
|
||||
Rearranges input according to formula
|
||||
|
||||
See: https://einops.rocks/api/rearrange/
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
x = Tensor([[1, 2], [3, 4]])
|
||||
print(Tensor.rearrange(x, "batch channel -> (batch channel)").numpy())
|
||||
```
|
||||
"""
|
||||
|
||||
def parse_side(s: str) -> tuple[list[str], list[tuple[int, int]]]:
|
||||
"""Parse one side of formula into (axis_names, dims) where dims are (start, end) index pairs for parens."""
|
||||
tokens = f" {s} ".replace("…", "...").replace("(", " ( ").replace(")", " ) ").replace(" ", " ").replace(" 1 ", " ( ) ").split()
|
||||
lparens, rparens = [i for i, tok in enumerate(tokens) if tok == "("], [i for i, tok in enumerate(tokens) if tok == ")"]
|
||||
pairs = list(zip(lparens, rparens))
|
||||
assert len(lparens) == len(rparens) and sorted(flatten(pairs)) == flatten(pairs), "bracket mismatch"
|
||||
return [tok for tok in tokens if tok not in ("(", ")")], [(lp - 2*i, rp - 1 - 2*i) for i, (lp, rp) in enumerate(pairs)]
|
||||
|
||||
assert formula.count("->") == 1, 'need exactly one "->" in formula'
|
||||
(lhs, unflatten_dims), (rhs, flatten_dims) = map(parse_side, formula.split("->"))
|
||||
|
||||
for name in sizes: assert name in lhs, f"axis {name} is not used in transform"
|
||||
assert sorted(lhs) == sorted(rhs) and len(lhs) == len(set(lhs)), f"name mismatch in {formula}"
|
||||
for name in lhs+rhs: assert name == "..." or (name.isidentifier() and "_" not in (name[0], name[-1])), f"invalid axis name {name}"
|
||||
assert "..." not in flatten([lhs[s:e] for s, e in unflatten_dims]), f"cannot have collapsed ellipsis (...) in lhs of {formula}"
|
||||
assert lhs.count("...") <= 1, f"too many ellipses in {formula}"
|
||||
|
||||
# resolve ellipsis
|
||||
if "..." in lhs:
|
||||
ell_len = len(self.shape) - len(lhs) + 1 + sum(e - s - 1 for s, e in unflatten_dims)
|
||||
lhs, rhs = map(lambda l: l[:(i := l.index("..."))] + [f"...{j}" for j in range(ell_len)] + l[i + 1:] if "..." in l else l, (lhs, rhs))
|
||||
def newdims(side, s, e): return (s + (ell_len - 1 if "...0" in side[:s] else 0), e + (ell_len - 1 if "...0" in side[:e] else 0))
|
||||
unflatten_dims, flatten_dims = [newdims(lhs, s, e) for s, e in unflatten_dims], [newdims(rhs, s, e) for s, e in flatten_dims]
|
||||
|
||||
# unflatten -> permute -> flatten
|
||||
t = self
|
||||
for start, end in unflatten_dims: t = t.unflatten(start, tuple(sizes.get(lhs[i], -1) for i in range(start, end)))
|
||||
for i, name in enumerate(lhs):
|
||||
if name in sizes: assert sizes[name] == t.shape[i], f"size provided for dimension {name} incorrect"
|
||||
t = t.permute([lhs.index(name) for name in rhs])
|
||||
for start, end in reversed(flatten_dims): t = t.flatten(start, end - 1) if start < end else t.unsqueeze(start)
|
||||
return t
|
||||
|
||||
def split(self, sizes:int|Sequence[int], dim:int=0) -> tuple[Self, ...]:
|
||||
"""
|
||||
Splits the tensor into chunks along the dimension specified by `dim`.
|
||||
If `sizes` is an integer, it splits into equally sized chunks if possible, otherwise the last chunk will be smaller.
|
||||
If `sizes` is a list, it splits into `len(sizes)` chunks with size in `dim` according to `size`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(10).reshape(5, 2)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
split = t.split(2)
|
||||
print("\\n".join([repr(x.numpy()) for x in split]))
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
split = t.split([1, 4])
|
||||
print("\\n".join([repr(x.numpy()) for x in split]))
|
||||
```
|
||||
"""
|
||||
dim = self._resolve_dim(dim)
|
||||
dim_sz = self.shape[dim]
|
||||
assert isinstance(dim_sz, int), f"does not support symbolic shape in split dimension {dim}: {self.shape}"
|
||||
if isinstance(sizes, int): sizes = [min(sizes, dim_sz-i) for i in range(0, max(1, dim_sz), max(1, sizes))]
|
||||
assert sum(sizes) == dim_sz, f"expect sizes to sum exactly to {dim_sz}, but got {sum(sizes)}"
|
||||
return tuple(self.shrink(tuple((sum(sizes[:i]), sum(sizes[:i+1])) if j == dim else None for j in range(self.ndim))) for i in range(len(sizes)))
|
||||
|
||||
def chunk(self, chunks:int, dim:int=0) -> list[Self]:
|
||||
"""
|
||||
Splits the tensor into `chunks` number of chunks along the dimension `dim`.
|
||||
If the tensor size along `dim` is not divisible by `chunks`, all returned chunks will be the same size except the last one.
|
||||
The function may return fewer than the specified number of chunks.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
chunked = Tensor.arange(11).chunk(6)
|
||||
print("\\n".join([repr(x.numpy()) for x in chunked]))
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
chunked = Tensor.arange(12).chunk(6)
|
||||
print("\\n".join([repr(x.numpy()) for x in chunked]))
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
chunked = Tensor.arange(13).chunk(6)
|
||||
print("\\n".join([repr(x.numpy()) for x in chunked]))
|
||||
```
|
||||
"""
|
||||
dim = self._resolve_dim(dim)
|
||||
dim_sz = self.shape[dim]
|
||||
assert isinstance(dim_sz, int), f"does not support symbolic shape in split dimension {dim}: {self.shape}"
|
||||
assert chunks > 0, f"expect chunks to be greater than 0, got: {chunks}"
|
||||
return list(self.split(ceildiv(dim_sz, chunks) if dim_sz else [0]*chunks, dim=dim))
|
||||
|
||||
def meshgrid(self, *args, indexing:str="ij") -> tuple[Self, ...]:
|
||||
"""
|
||||
Generates coordinate matrices from coordinate vectors.
|
||||
Input tensors can be scalars or 1D tensors.
|
||||
|
||||
`indexing` determines how the output grids are aligned.
|
||||
`ij` indexing follows matrix-style indexing and `xy` indexing follows Cartesian-style indexing.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
x, y = Tensor([1, 2, 3]), Tensor([4, 5, 6])
|
||||
grid_x, grid_y = x.meshgrid(y)
|
||||
print(grid_x.numpy())
|
||||
print(grid_y.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
grid_x, grid_y = x.meshgrid(y, indexing="xy")
|
||||
print(grid_x.numpy())
|
||||
print(grid_y.numpy())
|
||||
```
|
||||
"""
|
||||
if indexing not in ("ij", "xy"): raise RuntimeError(f'indexing must be in ("ij", "xy"), got {indexing}')
|
||||
if len(tensors:=(self, *args)) == 1: return tensors
|
||||
basis = tuple(range(len(tensors))) if indexing == "ij" else (1, 0) + tuple(range(2, len(tensors)))
|
||||
tensors = tuple(t.reshape((-1,) + (1,)*(len(args) - i)) for i,t in zip(basis, tensors))
|
||||
output_shape = _broadcast_shape(*(t.shape for t in tensors))
|
||||
return tuple(t._broadcast_to(output_shape) for t in tensors)
|
||||
|
||||
def diag(self) -> Self:
|
||||
"""
|
||||
Returns a 2-D square tensor with the elements of input as the main diagonal.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1, 2, 3]).diag().numpy())
|
||||
```
|
||||
"""
|
||||
if self.ndim != 1: raise ValueError(f"expect input to be 1-D, getting {self.ndim}-D")
|
||||
return self.unsqueeze(-1).pad_to((None, 1+(n:=self.shape[0]))).flatten().shrink_to((n*n,)).reshape(n,n)
|
||||
|
||||
def diagonal(self, offset:int=0, dim1:int=0, dim2:int=1) -> Self:
|
||||
"""
|
||||
Returns a view of the diagonal elements with respect to `dim1` and `dim2`.
|
||||
`offset` controls which diagonal: 0 is main, positive is above, negative is below.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(9).reshape(3, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.diagonal().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.diagonal(offset=1).numpy())
|
||||
```
|
||||
"""
|
||||
if (dim1:=self._resolve_dim(dim1)) == (dim2:=self._resolve_dim(dim2)): raise RuntimeError("dim1 and dim2 cannot be the same dimension")
|
||||
x = self.permute(*[i for i in range(self.ndim) if i != dim1 and i != dim2], dim1, dim2)
|
||||
if offset >= 0: x = x.shrink(tuple(None for _ in x.shape[:-1]) + ((offset, x.shape[-1]),))
|
||||
else: x = x.shrink(tuple(None for _ in x.shape[:-2]) + ((-offset, x.shape[-2]), None))
|
||||
if (d := min(int(x.shape[-2]), int(x.shape[-1]))) <= 0: return x.reshape(*x.shape[:-2], 0)
|
||||
nones, x = tuple(None for _ in x.shape[:-2]), x.shrink_to(tuple(None for _ in x.shape[:-2]) + (d, d))
|
||||
return x.flatten(-2).pad_to(nones+(d*(d+1),)).unflatten(-1, (d, d+1)).shrink_to(nones+(None, 1)).squeeze(-1)
|
||||
|
||||
def roll(self, shifts:int|tuple[int, ...], dims:int|tuple[int, ...]|None=None) -> Self:
|
||||
"""
|
||||
Rolls the tensor along specified dimension(s).
|
||||
The rolling operation is circular, meaning that elements that go beyond the edge are wrapped around to the beginning of the dimension.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(4)
|
||||
print(t.roll(shifts=1, dims=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.roll(shifts=-1, dims=0).numpy())
|
||||
```
|
||||
"""
|
||||
if dims is None: return self.flatten().roll(shifts, 0).reshape(self.shape)
|
||||
dims, shifts = tuple(self._resolve_dim(d) for d in make_tuple(dims, 1)), make_tuple(shifts, 1)
|
||||
if len(dims) != len(shifts): raise RuntimeError(f"{len(dims)=} != {len(shifts)=}")
|
||||
shrink_arg: list[tuple[sint, sint]|None] = [None] * self.ndim
|
||||
for d, s in zip(dims, shifts): shrink_arg[d] = (delta:=self.shape[d]-s%self.shape[d], delta+self.shape[d])
|
||||
return self.repeat(*tuple(2 if i in dims else 1 for i in range(self.ndim))).shrink(tuple(shrink_arg))
|
||||
|
||||
# *** movement ops with expand ***
|
||||
|
||||
def repeat_interleave(self, repeats: int, dim: int | None = None) -> Self:
|
||||
"""
|
||||
Repeats elements of a tensor.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([1, 2, 3])
|
||||
print(t.repeat_interleave(2).numpy())
|
||||
```
|
||||
"""
|
||||
x, dim = (self.flatten(), 0) if dim is None else (self, self._resolve_dim(dim))
|
||||
shp = x.shape
|
||||
x = x.reshape(*shp[: dim + 1], 1, *shp[dim + 1 :])
|
||||
x = x.expand(*shp[: dim + 1], repeats, *shp[dim + 1 :])
|
||||
x = x.reshape(*shp[:dim], shp[dim] * repeats, *shp[dim + 1 :])
|
||||
return x
|
||||
|
||||
def repeat(self, repeats, *args) -> Self:
|
||||
"""
|
||||
Repeats tensor number of times along each dimension specified by `repeats`.
|
||||
`repeats` can be passed as a tuple or as separate arguments.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([1, 2, 3])
|
||||
print(t.repeat(4, 2).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.repeat(4, 2, 1).shape)
|
||||
```
|
||||
"""
|
||||
repeats = argfix(repeats, *args)
|
||||
base_shape = _align_left(self.shape, repeats)[0]
|
||||
unsqueezed_shape = flatten([[s] if r == 1 else [1, s] for r, s in zip(repeats, base_shape)])
|
||||
expanded_shape = flatten([[s] if r == 1 else [r, s] for r, s in zip(repeats, base_shape)])
|
||||
final_shape = [r * s for r, s in zip(repeats, base_shape)]
|
||||
return self.reshape(unsqueezed_shape).expand(expanded_shape).reshape(final_shape)
|
||||
|
||||
# **** pool level ****
|
||||
|
||||
def _pool(self, k_: tuple[sint, ...], stride: int | tuple[int, ...] = 1, dilation: int | tuple[int, ...] = 1) -> Self:
|
||||
assert len(self.shape) >= len(k_), f"can't pool {self.shape} with {k_}"
|
||||
s_, d_ = make_tuple(stride, len(k_)), make_tuple(dilation, len(k_))
|
||||
assert len(k_) == len(s_) == len(d_), f"stride/dilation mismatch kernel:{k_} stride:{s_} dilation:{d_}"
|
||||
noop, i_ = [None] * (self.ndim - len(k_)), self.shape[-len(k_) :]
|
||||
assert all(resolve(d * (k - 1) + 1 <= i) for k, d, i in zip(k_, d_, i_)), "kernel size cannot be greater than actual input size"
|
||||
o_ = [ceildiv(i - d * (k - 1), s) for i, d, k, s in zip(i_, d_, k_, s_)]
|
||||
# input size scaling factor to make sure shrink for stride is possible
|
||||
f_ = [smax(1, ceildiv(o * s - d, i)) for o, s, i, d in zip(o_, s_, i_, d_)]
|
||||
# repeats such that we don't need padding
|
||||
x = self.repeat([1] * len(noop) + [ceildiv(k * (i * f + d), i) for k, i, d, f in zip(k_, i_, d_, f_)])
|
||||
# handle dilation
|
||||
x = x.shrink_to(noop + [k * (i * f + d) for k, i, d, f in zip(k_, i_, d_, f_)])
|
||||
x = x.reshape(noop + flatten((k, (i * f + d)) for k, i, d, f in zip(k_, i_, d_, f_)))
|
||||
# handle stride
|
||||
x = x.shrink_to(noop + flatten((k, o * s) for k, o, s in zip(k_, o_, s_))).reshape(noop + flatten((k, o, s) for k, o, s in zip(k_, o_, s_)))
|
||||
x = x.shrink_to(noop + flatten((k, o, 1) for k, o in zip(k_, o_))).reshape(noop + flatten((k, o) for k, o in zip(k_, o_)))
|
||||
# permute to move reduce to the end
|
||||
return x.permute(*range(len(noop)), *[len(noop) + i * 2 + 1 for i in range(len(i_))], *[len(noop) + i * 2 for i in range(len(i_))])
|
||||
|
||||
def unfold(self, dim:int, size, step:int) -> Self:
|
||||
"""
|
||||
Unfolds the tensor along dimension `dim` into overlapping windows.
|
||||
|
||||
Each window has length `size` and begins every `step` elements of `self`.
|
||||
Returns the input tensor with dimension `dim` replaced by dims `(n_windows, size)`
|
||||
where `n_windows = (self.shape[dim] - size) // step + 1`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
unfolded = Tensor.arange(8).unfold(0,2,2)
|
||||
print("\\n".join([repr(x.numpy()) for x in unfolded]))
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
unfolded = Tensor.arange(27).reshape(3,3,3).unfold(-1,2,3)
|
||||
print("\\n".join([repr(x.numpy()) for x in unfolded]))
|
||||
```
|
||||
"""
|
||||
if size < 0: raise RuntimeError(f'size must be >= 0 but got {size=}')
|
||||
if step <= 0: raise RuntimeError(f'step must be > 0 but got {step=}')
|
||||
if size > self.shape[dim]: raise RuntimeError(f'maximum size for tensor at dimension {dim} is {self.shape[dim]} but size is {size}')
|
||||
dim = self._resolve_dim(dim)
|
||||
perm_to_last = tuple(i for i in range(self.ndim) if i != dim) + (dim,)
|
||||
return self.permute(perm_to_last)._pool((size,), step).permute(argsort(perm_to_last) + (self.ndim,))
|
||||
1978
artifacts/package_runtime/tinygrad/mixin/op.py
Normal file
1978
artifacts/package_runtime/tinygrad/mixin/op.py
Normal file
File diff suppressed because it is too large
Load Diff
327
artifacts/package_runtime/tinygrad/mixin/rand.py
Normal file
327
artifacts/package_runtime/tinygrad/mixin/rand.py
Normal file
@@ -0,0 +1,327 @@
|
||||
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
|
||||
138
artifacts/package_runtime/tinygrad/mixin/reduce.py
Normal file
138
artifacts/package_runtime/tinygrad/mixin/reduce.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from typing import Self, Sequence
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.dtype import DTypeLike, dtypes, strong_dtype, sum_acc_dtype, to_dtype
|
||||
from tinygrad.helpers import make_tuple
|
||||
from tinygrad.mixin.dtype import DTypeMixin
|
||||
from tinygrad.mixin.movement import MovementMixin
|
||||
|
||||
|
||||
class ReduceMixin(DTypeMixin, MovementMixin):
|
||||
def _rop(self, op: Ops, axis: tuple[int, ...]) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def _reduce(self, op:Ops, axis:int|Sequence[int]|None=None, keepdim=False) -> Self:
|
||||
self = self.cast(strong_dtype(self.dtype))
|
||||
axis = tuple(self._resolve_dim(x) for x in (range(self.ndim) if axis is None else make_tuple(axis, 1)))
|
||||
if self.ndim == 0: axis = ()
|
||||
ret = self._rop(op, axis)
|
||||
return ret.reshape(tuple(1 if i in axis else s for i,s in enumerate(self.shape))) if keepdim else ret
|
||||
|
||||
def sum(self, axis:int|Sequence[int]|None=None, keepdim=False, dtype:DTypeLike|None=None) -> Self:
|
||||
"""
|
||||
Returns the sum of the elements of the tensor along the specified axis or axes.
|
||||
|
||||
You can pass in `axis` and `keepdim` keyword arguments to control the axis along
|
||||
which the maximum is computed and whether the reduced dimensions are retained.
|
||||
|
||||
You can pass in `dtype` keyword argument to control the data type of the accumulation.
|
||||
If not specified, the accumulation data type is chosen based on the input tensor's data type.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor.arange(6).reshape(2, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.sum().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.sum(axis=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.sum(axis=1).numpy())
|
||||
```
|
||||
"""
|
||||
ret = self.cast(sum_acc_dtype(self.dtype) if dtype is None else to_dtype(dtype))._reduce(Ops.ADD, axis, keepdim)
|
||||
return ret.cast(self.dtype) if dtype is None and self.dtype in (dtypes.float16, dtypes.bfloat16, *dtypes.fp8s) else ret
|
||||
|
||||
def prod(self, axis:int|Sequence[int]|None=None, keepdim=False, dtype:DTypeLike|None=None) -> Self:
|
||||
"""
|
||||
Returns the product of the elements of the tensor along the specified axis or axes.
|
||||
|
||||
You can pass in `axis` and `keepdim` keyword arguments to control the axis along
|
||||
which the maximum is computed and whether the reduced dimensions are retained.
|
||||
|
||||
You can pass in `dtype` keyword argument to control the data type of the accumulation.
|
||||
If not specified, the accumulation data type is chosen based on the input tensor's data type.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([-1, -2, -3, 1, 2, 3]).reshape(2, 3)
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.prod().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.prod(axis=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.prod(axis=1).numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(to_dtype(dtype) if dtype is not None else self.dtype)._reduce(Ops.MUL, axis, keepdim)
|
||||
|
||||
def max(self, axis:int|Sequence[int]|None=None, keepdim=False) -> Self:
|
||||
"""
|
||||
Returns the maximum value of the tensor along the specified axis or axes.
|
||||
|
||||
You can pass in `axis` and `keepdim` keyword arguments to control the axis along
|
||||
which the maximum is computed and whether the reduced dimensions are retained.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[1, 0, 2], [5, 4, 3]])
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.max().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.max(axis=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.max(axis=1, keepdim=True).numpy())
|
||||
```
|
||||
"""
|
||||
return self._reduce(Ops.MAX, axis, keepdim)
|
||||
|
||||
def any(self, axis:int|Sequence[int]|None=None, keepdim=False) -> Self:
|
||||
"""
|
||||
Tests if any element evaluates to `True` along the specified axis or axes.
|
||||
|
||||
You can pass in `axis` and `keepdim` keyword arguments to control the reduce axis and whether the reduced dimensions are retained.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[True, True], [True, False], [False, False]])
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.any().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.any(axis=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.any(axis=1, keepdim=True).numpy())
|
||||
```
|
||||
"""
|
||||
return self.bool().max(axis, keepdim)
|
||||
|
||||
def all(self, axis:int|Sequence[int]|None=None, keepdim=False) -> Self:
|
||||
"""
|
||||
Tests if all element evaluates to `True` along the specified axis or axes.
|
||||
|
||||
You can pass in `axis` and `keepdim` keyword arguments to control the reduce axis and whether the reduced dimensions are retained.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[True, True], [True, False], [False, False]])
|
||||
print(t.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.all().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.all(axis=0).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.all(axis=1, keepdim=True).numpy())
|
||||
```
|
||||
"""
|
||||
return self.bool().prod(axis, keepdim)
|
||||
Reference in New Issue
Block a user