forked from tinygrad/tinygrad
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee5f9cd29a | ||
|
|
50afa149f7 | ||
|
|
9759fd6193 | ||
|
|
42b6bf0b7a | ||
|
|
8091661df3 | ||
|
|
0e215c433d |
@@ -296,18 +296,21 @@ class TestDTypeALU(unittest.TestCase):
|
||||
@given(ht.int32, strat.sampled_from(dtypes_float+dtypes_int+dtypes_bool))
|
||||
def test_int32_cast(self, a, dtype): universal_test_cast(a, dtypes.int32, dtype)
|
||||
|
||||
@unittest.skip("relied on hacks")
|
||||
@given(strat.floats(width=32, min_value=1.0, max_value=254.0, allow_subnormal=False),
|
||||
strat.sampled_from(dtypes_float), strat.sampled_from((dtypes.uint8, dtypes.uint16)))
|
||||
def test_float_cast_to_unsigned(self, a, float_dtype, unsigned_dtype):
|
||||
if not is_dtype_supported(float_dtype): float_dtype = dtypes.float32
|
||||
universal_test_cast(a, float_dtype, unsigned_dtype)
|
||||
|
||||
@unittest.skip("relied on hacks")
|
||||
@given(strat.floats(width=32, min_value=256.0, max_value=65000.0, allow_subnormal=False),
|
||||
strat.sampled_from(dtypes_float), strat.sampled_from((dtypes.uint8, dtypes.uint16)))
|
||||
def test_float_cast_to_unsigned_overflow(self, a, float_dtype, unsigned_dtype):
|
||||
if not is_dtype_supported(float_dtype): float_dtype = dtypes.float32
|
||||
universal_test_cast(a, float_dtype, unsigned_dtype)
|
||||
|
||||
@unittest.skip("relied on hacks")
|
||||
@given(strat.floats(width=32, min_value=-65000.0, max_value=-1.0, allow_subnormal=False),
|
||||
strat.sampled_from(dtypes_float), strat.sampled_from((dtypes.uint8, dtypes.uint16)))
|
||||
def test_float_cast_to_unsigned_underflow(self, a, float_dtype, unsigned_dtype):
|
||||
|
||||
@@ -465,6 +465,19 @@ class TestMultiTensor(unittest.TestCase):
|
||||
y_shard = norm_sharded(x_sharded).realize()
|
||||
np.testing.assert_allclose(y.numpy(), y_shard.numpy(), atol=1e-6, rtol=1e-6)
|
||||
|
||||
def test_sdpa_causal_shard_batch(self):
|
||||
B, H, T, D = 4, 2, 10, 16
|
||||
q = Tensor.rand(B, H, T, D)
|
||||
k = Tensor.rand(B, H, T, D)
|
||||
v = Tensor.rand(B, H, T, D)
|
||||
q_shard = q.shard(devices_2, axis=0)
|
||||
k_shard = k.shard(devices_2, axis=0)
|
||||
v_shard = v.shard(devices_2, axis=0)
|
||||
Tensor.realize(q, k, v, q_shard, k_shard, v_shard)
|
||||
y = Tensor.scaled_dot_product_attention(q, k, v, is_causal=True).realize()
|
||||
y_shard = Tensor.scaled_dot_product_attention(q_shard, k_shard, v_shard, is_causal=True).realize()
|
||||
np.testing.assert_allclose(y_shard.numpy(), y.numpy(), atol=1e-6, rtol=1e-6)
|
||||
|
||||
# NOTE: this is failing on LLVM CI, no idea why. Works locally.
|
||||
@slow
|
||||
def test_data_parallel_resnet(self):
|
||||
|
||||
@@ -3295,6 +3295,7 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(is_dtype_supported(dtypes.uchar), f"no uint8 on {Device.DEFAULT}")
|
||||
class TestOpsUint8(unittest.TestCase):
|
||||
@unittest.skip("relied on hacks")
|
||||
def test_cast(self):
|
||||
helper_test_op([(2,3,64,64)], lambda x: x.type(torch.uint8), lambda x: x.cast('uint8'), forward_only=True)
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class TestPatternMatcher(unittest.TestCase):
|
||||
|
||||
def _assert_eq_upat(self, a:UPat, b:UPat):
|
||||
assert (sorted(map(str,a.op)) if a.op else [] == (sorted(map(str,b.op)) if b.op else []))
|
||||
assert (sorted(a.dtype) if a.dtype else [] == (sorted(b.dtype) if b.dtype else []))
|
||||
assert (sorted(a.match_dtype) if a.match_dtype else [] == (sorted(b.match_dtype) if b.match_dtype else []))
|
||||
assert (a.name, type(a.src)) == (b.name, type(b.src))
|
||||
def simple_src(u:UPat):
|
||||
if u.src is None: return []
|
||||
|
||||
@@ -456,6 +456,7 @@ class TestDiskTensor(TempDirTestCase):
|
||||
np.testing.assert_equal(t1.numpy(), np.arange(128, dtype=np.uint8))
|
||||
np.testing.assert_equal(t2.numpy(), np.arange(64, dtype=np.uint8))
|
||||
|
||||
@unittest.skip("fails with setup_python_cap run")
|
||||
def test_disk_open_failure_state(self):
|
||||
from tinygrad.runtime.ops_disk import DiskDevice
|
||||
fn = pathlib.Path(self.tmp("dt_open_failure"))
|
||||
@@ -476,6 +477,7 @@ class TestDiskTensor(TempDirTestCase):
|
||||
t2.to("CPU").realize()
|
||||
assert disk_device.size == 200
|
||||
|
||||
@unittest.skip("fails with setup_python_cap run")
|
||||
def test_disk_permission_error(self):
|
||||
fn = pathlib.Path(self.tmp("dt_permission"))
|
||||
fn.write_bytes(bytes(range(256)))
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from typing import Self
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
|
||||
class DTypeMixin:
|
||||
@property
|
||||
def dtype(self) -> DType: raise NotImplementedError
|
||||
|
||||
def cast(self, dtype:DType) -> Self: raise NotImplementedError
|
||||
|
||||
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())
|
||||
```
|
||||
"""
|
||||
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)
|
||||
+89
-13
@@ -2,9 +2,10 @@ import math
|
||||
from typing import Self
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.dtype import dtypes, ConstType
|
||||
from tinygrad.mixin.dtype import DTypeMixin
|
||||
|
||||
|
||||
class MathMixin:
|
||||
class MathMixin(DTypeMixin):
|
||||
# required to implement
|
||||
def alu(self, op: Ops, *src: Self) -> Self:
|
||||
raise NotImplementedError
|
||||
@@ -23,16 +24,11 @@ class MathMixin:
|
||||
return self.ne(True)
|
||||
|
||||
def neg(self) -> Self:
|
||||
if (dtype := getattr(self, "dtype")) is None:
|
||||
raise TypeError(f"MathTraits __neg__ requires a dtype, {self=}")
|
||||
return self.logical_not() if dtype.scalar() == dtypes.bool else self * (-1)
|
||||
return self.logical_not() if self.dtype.scalar() == dtypes.bool else self * (-1)
|
||||
|
||||
def _check_dtype(self) -> None:
|
||||
if (dtype := getattr(self, "dtype")) is not None:
|
||||
if isinstance(dtype, tuple):
|
||||
dtype = dtype[0]
|
||||
if not (dtypes.is_bool(dtype) or dtypes.is_int(dtype)):
|
||||
raise RuntimeError(f"{dtype} is not supported")
|
||||
if not (dtypes.is_bool(self.dtype) or dtypes.is_int(self.dtype)):
|
||||
raise RuntimeError(f"{self.dtype} is not supported")
|
||||
|
||||
def add(self, x: Self | ConstType, reverse: bool = False) -> Self:
|
||||
"""
|
||||
@@ -199,10 +195,10 @@ class MathMixin:
|
||||
return self.mod(x, True)
|
||||
|
||||
def __lt__(self, x: Self | ConstType) -> Self:
|
||||
return self.alu(Ops.CMPLT, self.ufix(x))
|
||||
return self._binop(Ops.CMPLT, x, False)
|
||||
|
||||
def __gt__(self, x: Self | ConstType) -> Self:
|
||||
return self.ufix(x).alu(Ops.CMPLT, self)
|
||||
return self._binop(Ops.CMPLT, x, True)
|
||||
|
||||
def __ge__(self, x: Self | ConstType) -> Self:
|
||||
return (self < x).logical_not()
|
||||
@@ -211,7 +207,7 @@ class MathMixin:
|
||||
return (self > x).logical_not()
|
||||
|
||||
def ne(self, x: Self | ConstType) -> Self:
|
||||
return self.alu(Ops.CMPNE, self.ufix(x))
|
||||
return self._binop(Ops.CMPNE, x, False)
|
||||
|
||||
def eq(self, x: Self | ConstType) -> Self:
|
||||
return self.ne(x).logical_not()
|
||||
@@ -240,7 +236,17 @@ class MathMixin:
|
||||
return self.rshift(x, True)
|
||||
|
||||
def maximum(self, x: Self | ConstType) -> Self:
|
||||
return self.alu(Ops.MAX, self.ufix(x))
|
||||
"""
|
||||
Computes element-wise maximum of `self` and `x`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-1, 2, 3]).maximum(1).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-1, 2, 3]).maximum(Tensor([-4, -2, 9])).numpy())
|
||||
```
|
||||
"""
|
||||
return self._binop(Ops.MAX, x, False)
|
||||
|
||||
def minimum(self, x: Self | ConstType) -> Self:
|
||||
return -(-self).maximum(-self.ufix(x))
|
||||
@@ -514,3 +520,73 @@ class MathMixin:
|
||||
```
|
||||
"""
|
||||
return self.sqrt().reciprocal()
|
||||
|
||||
def log(self) -> Self:
|
||||
"""
|
||||
Computes the natural logarithm element-wise.
|
||||
|
||||
See: https://en.wikipedia.org/wiki/Logarithm
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 4., 8.]).log().numpy())
|
||||
```
|
||||
"""
|
||||
return self.log2()*math.log(2)
|
||||
|
||||
def log10(self) -> Self:
|
||||
"""
|
||||
Computes the base-10 logarithm element-wise.
|
||||
|
||||
See: https://en.wikipedia.org/wiki/Logarithm
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 4., 8.]).log10().numpy())
|
||||
```
|
||||
"""
|
||||
return self.log2()*math.log10(2)
|
||||
|
||||
def atanh(self) -> Self:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Tangent (atanh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#atanh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-0.9, -0.6, -0.3, 0., 0.3, 0.6, 0.9]).atanh().numpy())
|
||||
```
|
||||
"""
|
||||
return ((1 + self)/(1 - self)).log() / 2
|
||||
|
||||
def asinh(self) -> Self:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Sine (asinh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#asinh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3., -2., -1., 0., 1., 2., 3.]).asinh().numpy())
|
||||
```
|
||||
"""
|
||||
return (self + (self.square() + 1).sqrt()).log()
|
||||
|
||||
def acosh(self) -> Self:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Cosine (acosh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#acosh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3., -2., -1., 0., 1., 2., 3.]).acosh().numpy())
|
||||
```
|
||||
"""
|
||||
return (self + (self.square() - 1).sqrt()).log()
|
||||
|
||||
def round(self) -> Self:
|
||||
"""
|
||||
Rounds the tensor element-wise with rounding half to even.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5]).round().numpy())
|
||||
```
|
||||
"""
|
||||
return ((self > 0).eq((b := self.trunc() / 2.0).trunc().eq(b))).where((self - 0.5).ceil(), (self + 0.5).floor())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Self
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.helpers import prod, argfix, flatten, dedup, make_tuple, ceildiv
|
||||
from tinygrad.helpers import prod, argfix, argsort, flatten, dedup, make_tuple, ceildiv
|
||||
from tinygrad.uop.ops import resolve, smax
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -373,3 +373,27 @@ class MovementMixin:
|
||||
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,))
|
||||
|
||||
+19
-221
@@ -189,12 +189,10 @@ class Tensor(OpMixin):
|
||||
all_tensors[weakref.ref(ret)] = None
|
||||
return ret
|
||||
|
||||
def _apply_broadcasted_uop(self, fxn:Callable, x:Tensor|ConstType, reverse=False) -> Tensor:
|
||||
lhs,rhs = self._broadcasted(x, reverse)
|
||||
return lhs._apply_uop(fxn, rhs)
|
||||
|
||||
# _binop and alu are used by MathMixin
|
||||
def _binop(self, op, x, reverse): return self._apply_broadcasted_uop(lambda *u: UOp.alu(u[0], op, *u[1:]), x, reverse)
|
||||
def _binop(self, op, x, reverse):
|
||||
lhs,rhs = self._broadcasted(x, reverse)
|
||||
return lhs._apply_uop(lambda *u: u[0].alu(op, *u[1:]), rhs)
|
||||
def alu(self, op: Ops, *src: Tensor) -> Tensor: return self._apply_uop(lambda *u: u[0].alu(op, *u[1:]), *src)
|
||||
|
||||
def requires_grad_(self, requires_grad=True) -> Tensor:
|
||||
@@ -614,14 +612,15 @@ class Tensor(OpMixin):
|
||||
print(t.numpy())
|
||||
```
|
||||
"""
|
||||
if not dtypes.is_float(dtype := to_dtype(dtype or dtypes.default_float)): raise ValueError(f"rand only supports float dtypes, got {dtype}")
|
||||
dt = to_dtype(dtype or dtypes.default_float)
|
||||
if not dtypes.is_float(dt): raise ValueError(f"rand only supports 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))
|
||||
|
||||
# if shape has 0, return zero tensor
|
||||
if (numel := prod(shape)) == 0: return Tensor.zeros(shape, device=device, dtype=dtype, **kwargs)
|
||||
num = ceildiv(numel * dtype.itemsize, 4)
|
||||
if (numel := prod(shape)) == 0: return Tensor.zeros(shape, device=device, dtype=dt, **kwargs)
|
||||
num = ceildiv(numel * dt.itemsize, 4)
|
||||
|
||||
# generate per device seeds and rng counter if we haven't seen this device yet
|
||||
if device not in Tensor._device_seeds:
|
||||
@@ -639,14 +638,14 @@ class Tensor(OpMixin):
|
||||
bits = Tensor._threefry_random_bits(Tensor._device_seeds[device], counts0, counts1)[:num]
|
||||
|
||||
# bitcast to uint with same number of bits
|
||||
_, nmant = dtypes.finfo(dtype)
|
||||
uint_dtype = {1: dtypes.uint8, 2: dtypes.uint16, 4: dtypes.uint32, 8: dtypes.uint64}[dtype.itemsize]
|
||||
_, nmant = dtypes.finfo(dt)
|
||||
uint_dtype = {1: dtypes.uint8, 2: dtypes.uint16, 4: dtypes.uint32, 8: dtypes.uint64}[dt.itemsize]
|
||||
bits = bits.bitcast(uint_dtype)
|
||||
# only randomize the mantissa bits and set the exponent to 1
|
||||
one = Tensor.ones_like(bits, device=bits.device, dtype=dtype).bitcast(uint_dtype)
|
||||
bits = bits.rshift(dtype.bitsize - nmant).bitwise_or(one)
|
||||
one = Tensor.ones_like(bits, device=bits.device, dtype=dt).bitcast(uint_dtype)
|
||||
bits = bits.rshift(dt.bitsize - nmant).bitwise_or(one)
|
||||
# bitcast back to the original dtype and reshape
|
||||
out = bits.bitcast(dtype)[:numel].sub(1).reshape(shape).requires_grad_(kwargs.get("requires_grad"))
|
||||
out = bits.bitcast(dt)[:numel].sub(1).reshape(shape).requires_grad_(kwargs.get("requires_grad"))
|
||||
return out.contiguous() if contiguous else out
|
||||
|
||||
# ***** creation helper functions *****
|
||||
@@ -770,8 +769,9 @@ class Tensor(OpMixin):
|
||||
print(Tensor.eye(2, 4).numpy())
|
||||
```
|
||||
"""
|
||||
if n < 0 or ((m := n if m is None else m) < 0): raise ValueError(f"cannot have negative {n=}, {m=}")
|
||||
t = (Tensor.arange(n, device=device).unsqueeze(-1) == Tensor.arange(m, device=device))
|
||||
m_ = n if m is None else m
|
||||
if n < 0 or m_ < 0: raise ValueError(f"cannot have negative {n=}, {m_=}")
|
||||
t = (Tensor.arange(n, device=device).unsqueeze(-1) == Tensor.arange(m_, device=device))
|
||||
return t.cast(dtype or dtypes.default_float).requires_grad_(requires_grad)
|
||||
|
||||
def _multi_like(self, fxn, *args, **kwargs) -> Tensor:
|
||||
@@ -1445,30 +1445,6 @@ class Tensor(OpMixin):
|
||||
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 unfold(self, dim:int, size:sint, step:int) -> Tensor:
|
||||
"""
|
||||
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,))
|
||||
|
||||
def meshgrid(self:Tensor, *args:Tensor, indexing:str="ij") -> tuple[Tensor, ...]:
|
||||
"""
|
||||
Generates coordinate matrices from coordinate vectors.
|
||||
@@ -2844,7 +2820,7 @@ class Tensor(OpMixin):
|
||||
print(Tensor([False, True]).logical_not().numpy())
|
||||
```
|
||||
"""
|
||||
return self.cast(dtypes.bool)._apply_broadcasted_uop(UOp.ne, True)
|
||||
return self.cast(dtypes.bool).ne(True)
|
||||
|
||||
def neg(self) -> Tensor:
|
||||
"""
|
||||
@@ -2868,30 +2844,6 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
return self._apply_uop(UOp.contiguous_backward)
|
||||
|
||||
def log(self) -> Tensor:
|
||||
"""
|
||||
Computes the natural logarithm element-wise.
|
||||
|
||||
See: https://en.wikipedia.org/wiki/Logarithm
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 4., 8.]).log().numpy())
|
||||
```
|
||||
"""
|
||||
return self.log2()*math.log(2)
|
||||
|
||||
def log10(self) -> Tensor:
|
||||
"""
|
||||
Computes the base-10 logarithm element-wise.
|
||||
|
||||
See: https://en.wikipedia.org/wiki/Logarithm
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 4., 8.]).log10().numpy())
|
||||
```
|
||||
"""
|
||||
return self.log2()*math.log10(2)
|
||||
|
||||
def log2(self) -> Tensor:
|
||||
"""
|
||||
Computes the base-2 logarithm element-wise.
|
||||
@@ -3019,16 +2971,6 @@ class Tensor(OpMixin):
|
||||
|
||||
# ***** math functions *****
|
||||
|
||||
def round(self: Tensor) -> Tensor:
|
||||
"""
|
||||
Rounds the tensor element-wise with rounding half to even.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5]).round().numpy())
|
||||
```
|
||||
"""
|
||||
return ((self > 0) == ((b := self.trunc() / 2.0).trunc() == b)).where((self - 0.5).ceil(), (self + 0.5).floor())
|
||||
|
||||
def lerp(self, end:Tensor, weight:Tensor|float) -> Tensor:
|
||||
"""
|
||||
Linearly interpolates between `self` and `end` by `weight`.
|
||||
@@ -3134,42 +3076,6 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
return (self.exp() + self.neg().exp()) / 2
|
||||
|
||||
def atanh(self) -> Tensor:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Tangent (atanh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#atanh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-0.9, -0.6, -0.3, 0., 0.3, 0.6, 0.9]).atanh().numpy())
|
||||
```
|
||||
"""
|
||||
return ((1 + self)/(1 - self)).log() / 2
|
||||
|
||||
def asinh(self) -> Tensor:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Sine (asinh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#asinh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3., -2., -1., 0., 1., 2., 3.]).asinh().numpy())
|
||||
```
|
||||
"""
|
||||
return (self + (self.square() + 1).sqrt()).log()
|
||||
|
||||
def acosh(self) -> Tensor:
|
||||
"""
|
||||
Applies the Inverse Hyperbolic Cosine (acosh) function element-wise.
|
||||
|
||||
- Described: https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#acosh
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-3., -2., -1., 0., 1., 2., 3.]).acosh().numpy())
|
||||
```
|
||||
"""
|
||||
return (self + (self.square() - 1).sqrt()).log()
|
||||
|
||||
def erf(self) -> Tensor:
|
||||
"""
|
||||
Applies error function element-wise.
|
||||
@@ -3289,7 +3195,7 @@ class Tensor(OpMixin):
|
||||
numerator, denominator = numerator.cast(dt), denominator.cast(dt)
|
||||
if rounding_mode == "trunc": return numerator.idiv(denominator)
|
||||
if rounding_mode == "floor":
|
||||
truncate_div, truncate_mod = numerator.idiv(denominator), numerator._apply_broadcasted_uop(UOp.mod, denominator)
|
||||
truncate_div, truncate_mod = numerator.idiv(denominator), numerator._binop(Ops.MOD, denominator, False)
|
||||
opposite_sign = ((numerator>0)&(denominator<0)) | ((numerator<0)&(denominator>0))
|
||||
return (opposite_sign&(truncate_mod!=0)).where(truncate_div-1, truncate_div)
|
||||
if rounding_mode == "trunc": return d.trunc().cast(output_dtype)
|
||||
@@ -3371,19 +3277,6 @@ class Tensor(OpMixin):
|
||||
# NOTE: pow(int, float) -> int
|
||||
return ret.round().cast(self.dtype) if not reverse and not dtypes.is_float(self.dtype) and dtypes.is_float(exponent.dtype) else ret
|
||||
|
||||
def maximum(self, x:Tensor|ConstType) -> Tensor:
|
||||
"""
|
||||
Computes element-wise maximum of `self` and `x`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-1, 2, 3]).maximum(1).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([-1, 2, 3]).maximum(Tensor([-4, -2, 9])).numpy())
|
||||
```
|
||||
"""
|
||||
return self._apply_broadcasted_uop(UOp.maximum, x)
|
||||
|
||||
def minimum(self, x:Tensor|ConstType) -> Tensor:
|
||||
"""
|
||||
Computes element-wise minimum of `self` and `x`.
|
||||
@@ -3468,10 +3361,6 @@ class Tensor(OpMixin):
|
||||
def __ilshift__(self, x) -> Tensor: return self.assign(self.lshift(x)) # type: ignore[misc]
|
||||
def __irshift__(self, x) -> Tensor: return self.assign(self.rshift(x)) # type: ignore[misc]
|
||||
|
||||
def __lt__(self, x) -> Tensor: return self._apply_broadcasted_uop(UOp.__lt__, x, False)
|
||||
def __gt__(self, x) -> Tensor: return self._apply_broadcasted_uop(UOp.__lt__, x, True)
|
||||
def ne(self, x) -> Tensor: return self._apply_broadcasted_uop(UOp.ne, x, False)
|
||||
|
||||
def __eq__(self, x) -> Tensor: return self.eq(x) # type: ignore[override]
|
||||
|
||||
# ***** encoding/decoding ops *****
|
||||
@@ -3631,7 +3520,7 @@ class Tensor(OpMixin):
|
||||
# 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.ones_like(requires_grad=False, device=self.device, dtype=dtypes.bool).tril()
|
||||
attn_mask = qk.ones_like(requires_grad=False, dtype=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
|
||||
@@ -3835,17 +3724,6 @@ class Tensor(OpMixin):
|
||||
|
||||
# ***** Tensor Properties *****
|
||||
|
||||
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())
|
||||
```
|
||||
"""
|
||||
return self.dtype.itemsize
|
||||
|
||||
def nbytes(self) -> int:
|
||||
"""
|
||||
Returns the total number of bytes of all elements in the tensor.
|
||||
@@ -3857,18 +3735,6 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
return int(self.numel()) * self.element_size()
|
||||
|
||||
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 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.
|
||||
@@ -3902,10 +3768,7 @@ class Tensor(OpMixin):
|
||||
print(t.dtype, t.numpy())
|
||||
```
|
||||
"""
|
||||
if (dt:=to_dtype(dtype)) in {dtypes.uint8, dtypes.uint16} and dtypes.is_float(self.dtype):
|
||||
# NOTE: values within the int32 range and outside the unsigned dtype range will cause values to wrap around
|
||||
return self._apply_uop(UOp.cast, dtype=dtypes.int32)._apply_uop(UOp.cast, dtype=dt)
|
||||
return self if self.dtype == dt else self._apply_uop(UOp.cast, dtype=dt)
|
||||
return self if self.dtype == (dt:=to_dtype(dtype)) else self._apply_uop(UOp.cast, dtype=dt)
|
||||
|
||||
def bitcast(self, dtype:DTypeLike) -> Tensor:
|
||||
"""
|
||||
@@ -3938,71 +3801,6 @@ class Tensor(OpMixin):
|
||||
return Tensor.stack(*(tmp>>8*i*ns for i in range(os//ns)), dim=-1).flatten(-2).cast(new_uint).bitcast(dtype)
|
||||
return self._apply_uop(UOp.bitcast, dtype=dt) if self.dtype != dt else self
|
||||
|
||||
def float(self) -> Tensor:
|
||||
"""
|
||||
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) -> Tensor:
|
||||
"""
|
||||
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) -> Tensor:
|
||||
"""
|
||||
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) -> Tensor:
|
||||
"""
|
||||
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) -> Tensor: return self.cast(dtypes.bfloat16)
|
||||
def double(self) -> Tensor: return self.cast(dtypes.double)
|
||||
def long(self) -> Tensor: return self.cast(dtypes.long)
|
||||
def short(self) -> Tensor: return self.cast(dtypes.short)
|
||||
|
||||
# *** image Tensor function replacements ***
|
||||
|
||||
def image_dot(self, w:Tensor, dtype:DTypeLike|None=None) -> Tensor:
|
||||
|
||||
+20
-15
@@ -893,13 +893,13 @@ def get_location() -> tuple[str, int]:
|
||||
return frm.f_code.co_filename, frm.f_lineno
|
||||
|
||||
class UPat(OpMixin):
|
||||
__slots__ = ("op", "dtype", "arg", "name", "src", "is_any")
|
||||
__slots__ = ("op", "match_dtype", "arg", "name", "src", "is_any")
|
||||
def __init__(self, op:Ops|tuple[Ops, ...]|set[Ops]|None=None, dtype:DType|tuple[DType, ...]|set[DType]|None=None,
|
||||
src:tuple[UPat, ...]|list[UPat]|UPat|None=None, arg:Any=None,
|
||||
name:str|None=None, allow_any_len:bool=False, custom_early_reject:set[Ops]|None=None, location=None, is_any:bool=False):
|
||||
assert op is None or isinstance(op, (Ops, tuple, set)), "op must be Ops or tuple of Ops"
|
||||
self.op: tuple[Ops, ...]|None = (op,) if isinstance(op, Ops) else (tuple(op) if isinstance(op, set) else op)
|
||||
self.dtype: tuple[DType, ...]|None = (dtype,) if isinstance(dtype, DType) else (tuple(dtype) if isinstance(dtype, set) else dtype)
|
||||
self.match_dtype: tuple[DType, ...]|None = (dtype,) if isinstance(dtype, DType) else (tuple(dtype) if isinstance(dtype, set) else dtype)
|
||||
self.arg, self.name, self._in_src, self.custom_early_reject = arg, name, src, custom_early_reject
|
||||
self.src: Any = None
|
||||
self.is_any = is_any
|
||||
@@ -922,9 +922,14 @@ class UPat(OpMixin):
|
||||
upat_match = [src] if isinstance(src, UPat) else ([] if src is None else self.src[0])
|
||||
self.early_reject = {pp.op[0] for pp in upat_match if pp.op is not None and len(pp.op) == 1}
|
||||
|
||||
@property
|
||||
def dtype(self) -> DType: return self.match_dtype[0] if self.match_dtype is not None else dtypes.void
|
||||
|
||||
def _check_dtype(self) -> None: pass
|
||||
|
||||
def __reduce__(self):
|
||||
return UPat, (self.op, self.dtype, self._in_src, self.arg, self.name, not self.strict_length, self.custom_early_reject, self.location)
|
||||
def named(self, name:str): return UPat(self.op, self.dtype, self._in_src, self.arg, name, not self.strict_length, self.custom_early_reject)
|
||||
return UPat, (self.op, self.match_dtype, self._in_src, self.arg, self.name, not self.strict_length, self.custom_early_reject, self.location)
|
||||
def named(self, name:str): return UPat(self.op, self.match_dtype, self._in_src, self.arg, name, not self.strict_length, self.custom_early_reject)
|
||||
|
||||
@staticmethod
|
||||
def any(*src): return UPat(src=src, is_any=True)
|
||||
@@ -948,23 +953,23 @@ class UPat(OpMixin):
|
||||
# copied from UOp
|
||||
def sink(self, *srcs:UPat|None, **kwargs): return UPat(Ops.SINK, dtypes.void, (self,)+tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
def index(self, idx:UPat, valid:UPat|None=None, **kwargs):
|
||||
return UPat(Ops.INDEX, self.dtype, (self,idx,valid) if valid is not None else (self,idx), **kwargs)
|
||||
return UPat(Ops.INDEX, self.match_dtype, (self,idx,valid) if valid is not None else (self,idx), **kwargs)
|
||||
def cast(self, dtype=None, **kwargs): return UPat(Ops.CAST, dtype, (self,), **kwargs)
|
||||
def bitcast(self, dtype=None): return UPat(Ops.BITCAST, dtype, (self,))
|
||||
def gep(self, i:int|None=None, **kwargs): return UPat(Ops.GEP, None, (self,), (i,) if i is not None else None, **kwargs)
|
||||
def load(self, *src:UPat, **kwargs): return UPat(Ops.LOAD, src=(self,)+src, **kwargs)
|
||||
def store(self, *src:UPat, **kwargs): return UPat(Ops.STORE, self.dtype, (self,)+src, **kwargs)
|
||||
def assign(self, x:UPat, **kwargs): return UPat(Ops.ASSIGN, self.dtype, (self,x), **kwargs)
|
||||
def reduce(self, *src:UPat, **kwargs): return UPat(Ops.REDUCE, self.dtype, src=(self,)+src, **kwargs)
|
||||
def broadcast(self, **kwargs): return UPat(Ops.VECTORIZE, self.dtype, src=self, **kwargs)
|
||||
def contiguous(self, *args, **kwargs): return UPat(Ops.CONTIGUOUS, dtype=self.dtype, src=(self,)+args, **kwargs)
|
||||
def after(self, *src:UPat, **kwargs): return UPat(Ops.AFTER, self.dtype, (self,)+src, **kwargs)
|
||||
def end(self, *src:UPat, **kwargs): return UPat(Ops.END, self.dtype, (self,)+src, **kwargs)
|
||||
def store(self, *src:UPat, **kwargs): return UPat(Ops.STORE, self.match_dtype, (self,)+src, **kwargs)
|
||||
def assign(self, x:UPat, **kwargs): return UPat(Ops.ASSIGN, self.match_dtype, (self,x), **kwargs)
|
||||
def reduce(self, *src:UPat, **kwargs): return UPat(Ops.REDUCE, self.match_dtype, src=(self,)+src, **kwargs)
|
||||
def broadcast(self, **kwargs): return UPat(Ops.VECTORIZE, self.match_dtype, src=self, **kwargs)
|
||||
def contiguous(self, *args, **kwargs): return UPat(Ops.CONTIGUOUS, dtype=self.match_dtype, src=(self,)+args, **kwargs)
|
||||
def after(self, *src:UPat, **kwargs): return UPat(Ops.AFTER, self.match_dtype, (self,)+src, **kwargs)
|
||||
def end(self, *src:UPat, **kwargs): return UPat(Ops.END, self.match_dtype, (self,)+src, **kwargs)
|
||||
|
||||
def const_like(self, b:ConstLike): return UPat.const(self.dtype, cast(ConstType, b))
|
||||
def const_like(self, b:ConstLike): return UPat.const(self.match_dtype, cast(ConstType, b))
|
||||
def alu(self, op:Ops, *src:UPat):
|
||||
asrc = (self,)+src
|
||||
return UPat(op, dtypes.bool if op in {Ops.CMPLT, Ops.CMPNE} else asrc[-1].dtype, list(asrc) if op in GroupOp.Commutative else asrc)
|
||||
return UPat(op, dtypes.bool if op in {Ops.CMPLT, Ops.CMPNE} else asrc[-1].match_dtype, list(asrc) if op in GroupOp.Commutative else asrc)
|
||||
|
||||
def match(self:UPat, uop:UOp, store:dict[str, UOp]) -> list[dict[str, UOp]]:
|
||||
if self.is_any:
|
||||
@@ -972,7 +977,7 @@ class UPat(OpMixin):
|
||||
return flatten([x for x in matches if x is not None])
|
||||
if (self.op is not None and uop.op not in self.op) or \
|
||||
(self.name is not None and store.setdefault(self.name, uop) is not uop) or \
|
||||
(self.dtype is not None and uop.dtype not in self.dtype and uop.dtype.scalar() not in self.dtype) or \
|
||||
(self.match_dtype is not None and uop.dtype not in self.match_dtype and uop.dtype.scalar() not in self.match_dtype) or \
|
||||
(self.arg is not None and self.arg != uop.arg) or \
|
||||
(len(uop.src) < self.required_len) or \
|
||||
(self.strict_length and len(uop.src) != self.required_len): return []
|
||||
|
||||
@@ -22,10 +22,10 @@ def _get_clause(self:UPat, base:UOp, depth=0) -> UOp:
|
||||
if self.strict_length or self.required_len > 0:
|
||||
and_clause.append(UOp(Ops.CUSTOM, src=(base,), arg=("len({0}.src)"+(" == " if self.strict_length else " >= ")+str(self.required_len))))
|
||||
if self.name is not None: and_clause.append(UOp(Ops.STORE, src=(UOp(Ops.DEFINE_VAR, arg=self.name), base)))
|
||||
if self.dtype is not None:
|
||||
if len(self.dtype) > 1:
|
||||
and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.BIND, arg=tuple(self.dtype))), arg="({0}.dtype in {1} or {0}.dtype._scalar in {1})"))
|
||||
else: and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.BIND, arg=self.dtype[0])), arg="({0}.dtype == {1} or {0}.dtype._scalar == {1})"))
|
||||
if self.match_dtype is not None:
|
||||
if len(self.match_dtype) > 1:
|
||||
and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.BIND, arg=tuple(self.match_dtype))), arg="({0}.dtype in {1} or {0}.dtype._scalar in {1})"))
|
||||
else: and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.BIND, arg=self.match_dtype[0])), arg="({0}.dtype == {1} or {0}.dtype._scalar == {1})"))
|
||||
if self.src is not None:
|
||||
# single match
|
||||
if len(self.src) == 1 and isinstance(self.src[0], tuple):
|
||||
|
||||
Reference in New Issue
Block a user