forked from tinygrad/tinygrad
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
303c5d3259 | ||
|
|
048f510b51 | ||
|
|
cacba3f4d5 | ||
|
|
01c6f396b1 | ||
|
|
d51003bb61 | ||
|
|
9433790adb | ||
|
|
92f9c850b4 | ||
|
|
b1060ca708 | ||
|
|
b1a72299ab | ||
|
|
8fa5993923 | ||
|
|
f41e4a758f | ||
|
|
f19a2ad771 | ||
|
|
787b2f2db2 | ||
|
|
ef37830d13 | ||
|
|
5244d3cd2a | ||
|
|
b764599d87 | ||
|
|
46b82d4755 | ||
|
|
76dade5a11 |
+10
@@ -0,0 +1,10 @@
|
||||
import os, pytest, signal, threading
|
||||
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(item):
|
||||
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 300)), os.kill, args=(os.getpid(), signal.SIGABRT))
|
||||
t.start()
|
||||
try: yield
|
||||
finally:
|
||||
t.cancel()
|
||||
t.join()
|
||||
@@ -5,10 +5,10 @@ def bit_extract(x: Tensor, e: int, s: int) -> Tensor:
|
||||
return (x >> s) & mask
|
||||
|
||||
def u16_to_f16(x: Tensor) -> Tensor:
|
||||
sign = bit_extract(x, 15, 15).float()
|
||||
sign = bit_extract(x, 15, 15).bool()
|
||||
exponent = bit_extract(x, 14, 10).float()
|
||||
fraction = bit_extract(x, 9, 0).float()
|
||||
return sign.where(-1, 1) * exponent.where((exponent - 15.0).exp2() * (1 + fraction / 1024.0), 6.103515625e-5 * (fraction / 1024.0))
|
||||
return sign.where(-1, 1) * exponent.bool().where((exponent - 15.0).exp2() * (1 + fraction / 1024.0), 6.103515625e-5 * (fraction / 1024.0))
|
||||
|
||||
def u32_to_f16(oo: Tensor) -> Tensor:
|
||||
f1 = u16_to_f16(oo>>16)
|
||||
|
||||
+10
-9
@@ -49,7 +49,7 @@ def make_getaddr(u, device=None):
|
||||
return UOp(Ops.GETADDR, dtypes.uint64, src=(u,), arg=device or to_tuple(u.device)[0])
|
||||
|
||||
def make_ins(op, *srcs):
|
||||
return UOp(Ops.INS, dtypes.void, tuple(UOp.const(dtypes.uint32, s) if isinstance(s, int) else s.cast(dtypes.uint32) for s in srcs), op)
|
||||
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(dtypes.uint32, s) if isinstance(s, int) else s.cast(dtypes.uint32) for s in srcs))
|
||||
|
||||
def make_placeholder(devs, size:int, dtype, name=None, unique=True) -> UOp:
|
||||
return UOp.param(next(UOp.unique_num) if unique else 0, dtype, shape=(size,), device=devs).rtag(name or "temp")
|
||||
@@ -133,7 +133,7 @@ def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str,
|
||||
for (ddevs, dqueue, dtag), lanes in deps.items():
|
||||
sig = make_mstack([make_signal(d if dl is None else ddevs[dl], queue=dqueue, sentinel=dl is None) for dl, d in zip(lanes, devices)])
|
||||
val = make_mstack([make_signal_value(d if dl is None else ddevs[dl], queue=dqueue) for dl, d in zip(lanes, devices)])
|
||||
waits.append((sig.index(zero:=UOp.const(dtypes.int, 0)).load() >= val.index(zero) + dtag).wait())
|
||||
waits.append(UOp(Ops.INS, arg="wait", src=(sig, val.index(UOp.const(dtypes.int, 0)) + dtag)))
|
||||
return waits, {dtag for _, _, dtag in deps}
|
||||
|
||||
def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[tuple[tuple[str, ...], str]],
|
||||
@@ -154,7 +154,8 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
waited |= cur_waited
|
||||
|
||||
# wait the syncs, store the device epoch; value bumps are a separate call: no lane may bump until every lane has patched its waits
|
||||
submit = make_submit(*waits, make_signal(devs).store((tl:=make_signal_value(devs)).index(zero)), devs=devs, queue="COMPUTE:0")
|
||||
store = UOp(Ops.INS, arg="store", src=(make_signal(devs), (tl:=make_signal_value(devs)).index(zero)))
|
||||
submit = make_submit(*waits, store, devs=devs, queue="COMPUTE:0")
|
||||
upd = [(tl, 1)] + [(make_signal_value(devs, queue=qn), n) for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])]
|
||||
bump = UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])
|
||||
finalizers += [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for b in (submit, bump)]
|
||||
@@ -181,15 +182,15 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
|
||||
for tag, ((call, _), (devices, queue), cmds) in enumerate(zip(batch, batch_info, call_waits)):
|
||||
# first queue use, sync prior device work with main signal
|
||||
if batch_info.index((devices, queue)) == tag:
|
||||
epoch = (make_signal(devices).index(0).load() >= make_signal_value(devices).index(0) - 1).wait()
|
||||
cmds = [UOp(Ops.BARRIER), epoch] + cmds
|
||||
|
||||
# signal queue timeline if someone waits for us
|
||||
store = make_signal(devices, queue=queue).store(make_signal_value(devices, queue=queue).index(0) + tag) if tag in waited else None
|
||||
epoch = UOp(Ops.INS, arg="wait", src=(make_signal(devices), make_signal_value(devices).index(0) - 1))
|
||||
cmds = [UOp(Ops.INS, arg="barrier", src=()), epoch] + cmds
|
||||
|
||||
# and make hcq call
|
||||
info = HCQInfo(get_call_name(call, get_call_arg_uops(call)), estimate_uop(call), devices, queue)
|
||||
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))] + ([store] if store is not None else [])
|
||||
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))]
|
||||
|
||||
# signal queue timeline if someone waits for us
|
||||
if tag in waited: cmds += [UOp(Ops.INS, arg="store", src=(make_signal(devices, queue), make_signal_value(devices, queue).index(0) + tag))]
|
||||
src.append(UOp.custom_function("hcq", make_submit(*cmds, devs=devices, queue=queue).sink()).call(name="hcq", aux=info))
|
||||
return src + finalizers
|
||||
|
||||
|
||||
+11
-11
@@ -90,7 +90,7 @@ def memory_barrier(ctx):
|
||||
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
|
||||
acquire_mem(ctx)))
|
||||
|
||||
def pm4_wait(ctx, x, y): return wait_reg_mem(ctx, y, mem=make_getaddr(x.buf_uop, ctx.devs))
|
||||
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=make_getaddr(dst, ctx.devs))
|
||||
|
||||
def pm4_barrier(ctx): return memory_barrier(ctx)
|
||||
|
||||
@@ -138,10 +138,10 @@ def pm4_program(ctx, call, prg):
|
||||
pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), pm4_program),
|
||||
|
||||
(UPat(Ops.WAIT, src=(UPat.var("x") >= UPat.var("y"),)), pm4_wait),
|
||||
(UPat(Ops.BARRIER), pm4_barrier),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="timestamp", src=(UPat(name="dst"),)), pm4_timestamp),
|
||||
(UPat(Ops.STORE, src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), pm4_wait),
|
||||
(UPat(Ops.INS, arg="barrier"), pm4_barrier),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), pm4_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
])
|
||||
|
||||
def pm4_submit(cmdbuf, devs):
|
||||
@@ -184,10 +184,10 @@ def sdma_copy(ctx, call):
|
||||
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz - off, ctx.max_copy_size) - 1), 0,
|
||||
*data64_le(src_addr + off), *data64_le(dst_addr + off)) for off in range(0, sz, ctx.max_copy_size)]))
|
||||
|
||||
def sdma_wait(ctx, x, y):
|
||||
def sdma_wait(ctx, dst, val):
|
||||
op = ctx.sdma.SDMA_OP_POLL_REGMEM | ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(WAIT_REG_MEM_FUNCTION_GEQ) \
|
||||
| ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
|
||||
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(x.buf_uop, ctx.devs)), y, 0xffffffff,
|
||||
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(dst, ctx.devs)), val, 0xffffffff,
|
||||
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))
|
||||
|
||||
def sdma_store(ctx, dst, val):
|
||||
@@ -202,10 +202,10 @@ def sdma_timestamp(ctx, dst):
|
||||
pm_sdma_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
|
||||
|
||||
(UPat(Ops.BARRIER), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.WAIT, src=(UPat.var("x") >= UPat.var("y"),)), sdma_wait),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
|
||||
(UPat(Ops.STORE, src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), sdma_wait),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
|
||||
])
|
||||
|
||||
def sdma_submit(cmdbuf, devs):
|
||||
|
||||
@@ -74,7 +74,6 @@ testing_minimal = [
|
||||
"torch==2.9.1",
|
||||
"pytest",
|
||||
"pytest-xdist",
|
||||
"pytest-timeout",
|
||||
"pytest-split",
|
||||
"hypothesis>=6.148.9",
|
||||
"z3-solver<4.15.4", # 4.15.4 has a segfault when creating many z3.Context()
|
||||
@@ -160,8 +159,6 @@ norecursedirs = [
|
||||
".hypothesis",
|
||||
".git",
|
||||
]
|
||||
timeout = 300
|
||||
timeout_func_only = true
|
||||
testpaths = ["test"]
|
||||
filterwarnings = [
|
||||
# Ignore SWIG warnings from importlib
|
||||
|
||||
@@ -290,7 +290,7 @@ class TestLinearizer(unittest.TestCase):
|
||||
@unittest.skipIf(MOCKGPU and isinstance(Device[Device.DEFAULT].renderer, (PTXRenderer, CUDARenderer)), "PTX indexes differently. might be ok?")
|
||||
def test_where_fold(self):
|
||||
a = Tensor.ones(4, 4).contiguous().realize()
|
||||
b = a.shrink(((1, 2), None)).pad(((1, 2), None))
|
||||
b = a.shrink(((1, 2), None)).pad(((1, 2), None)).bool()
|
||||
a.assign(b.where(2, a))
|
||||
linear, var_vals = a.linear_with_vars()
|
||||
assert len(linear.src) == 1
|
||||
|
||||
@@ -5,7 +5,6 @@ import torch
|
||||
from tinygrad.helpers import getenv, DEBUG, DEV, IMAGE, Context
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.renderer.cstyle import QCOMCLRenderer
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
|
||||
TINY_BACKEND = getenv("TINY_BACKEND")
|
||||
@@ -450,7 +449,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(45,35), (45,35), (45,35)], lambda x,y,z: x.lerp(y,z))
|
||||
helper_test_op(None, lambda x,y,z: x.lerp(y,z), vals=[[1.,2.,3.], [4.,5.,6.], 0.5])
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_tril(self):
|
||||
helper_test_op([(3,3)], lambda x: x.tril())
|
||||
helper_test_op([(3,3)], lambda x: x.tril(1))
|
||||
@@ -468,7 +466,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(5,3,3)], lambda x: x.tril(1))
|
||||
helper_test_op(None, lambda x: x.tril(), vals=[[[True] * 3] * 3], forward_only=True)
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_triu(self):
|
||||
helper_test_op([(3,3)], lambda x: x.triu())
|
||||
helper_test_op([(3,3)], lambda x: x.triu(1))
|
||||
@@ -771,6 +768,11 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: torch.tensor([2], dtype=torch.int) ** torch.tensor(-2, dtype=torch.int),
|
||||
lambda: Tensor([2]) ** Tensor(-2), forward_only=True)
|
||||
|
||||
def test_pow_int_base_float_exponent(self):
|
||||
for exponent in (0.5, 1.5, 2.0, -1.0, 0.0):
|
||||
helper_test_op([], lambda: torch.tensor([1, 2, 3, 4], dtype=torch.int) ** exponent,
|
||||
lambda: Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent, forward_only=True)
|
||||
|
||||
def test_sqrt(self):
|
||||
helper_test_op([(45,65)], lambda x: x.sqrt())
|
||||
helper_test_op(None, lambda x: x.sqrt(), vals=[[0.0]])
|
||||
@@ -791,7 +793,6 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_xor(y), expected=RuntimeError)
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_and(self):
|
||||
data = [[1,-8,1],[32,1,6]]
|
||||
tor = torch.tensor(data, dtype=torch.int)
|
||||
@@ -809,7 +810,6 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_and(y), expected=RuntimeError)
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_or(self):
|
||||
data = [[1,-8,1],[32,1,6]]
|
||||
tor = torch.tensor(data, dtype=torch.int)
|
||||
@@ -1229,7 +1229,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x: x.type(torch.int32).argmax().type(torch.int32), lambda x: x.argmax(), forward_only=True, vals=[[False, True]])
|
||||
helper_test_op(None, lambda x: x.type(torch.int32).argmax().type(torch.int32), lambda x: x.argmax(), forward_only=True, vals=[[True, False]])
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_argmin(self):
|
||||
# check if it returns the first index for multiple occurrences
|
||||
helper_test_op(None, lambda x: x.argmin().type(torch.int32), lambda x: x.argmin(), forward_only=True, vals=[[2, 2]])
|
||||
@@ -1535,7 +1534,6 @@ class TestOps(unittest.TestCase):
|
||||
def test_prod_dtype_arg(self):
|
||||
with self.assertRaises(AttributeError): Tensor([1.0, 2.0]).prod(dtype="")
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_min(self):
|
||||
helper_test_op([(3,3)], lambda x: x.min())
|
||||
helper_test_op([(45,3)], lambda x: x.min())
|
||||
@@ -1575,7 +1573,6 @@ class TestOps(unittest.TestCase):
|
||||
def test_any_zero_axis(self):
|
||||
helper_test_op([(1,0,3,0,5)], lambda x: x.any(axis=(1,3)), forward_only=True)
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_all(self):
|
||||
helper_test_op([(3,4,5,6)], lambda x: x.all(), forward_only=True)
|
||||
helper_test_op(None, lambda x: x.all(), vals=[[True, True]], forward_only=True)
|
||||
@@ -2953,7 +2950,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(2,5,6,5,3,4)], lambda x: x[...,c,:,e], lambda x: x[...,k,:,p])
|
||||
|
||||
@slow_test
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_slice_fancy_indexing_dim_collapse_int(self):
|
||||
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
|
||||
# dim collapse from int
|
||||
@@ -2964,7 +2960,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(2,5,6,5,3,4)], lambda x: x[1,:,3:11:2,d,0:2], lambda x: x[1,:,3:11:2,o,0:2])
|
||||
|
||||
@slow_test
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_slice_fancy_indexing_dim_inject_none(self):
|
||||
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
|
||||
# dim injection from None
|
||||
@@ -2999,7 +2994,6 @@ class TestOps(unittest.TestCase):
|
||||
lambda x: x[Tensor([[0,1,-1],[-1,-2,0]]), Tensor([2,1,-1])])
|
||||
|
||||
@slow_test
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_slice_fancy_indexing_list_indices(self):
|
||||
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
|
||||
helper_test_op([(2,5,6,5,3,4)], lambda x: x[((0,),)])
|
||||
@@ -3011,7 +3005,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(2,5,6,5,3,4)], lambda x: x[a,(2,1,0),c,(-2,1,0),e], lambda x: x[i,(2,1,0),k,(-2,1,0),p])
|
||||
|
||||
@slow_test
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
|
||||
def test_slice_fancy_indexing_tuple_indices(self):
|
||||
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
|
||||
helper_test_op([(2,5,6,5,3,4)], lambda x: x[(((0,),),)], lambda x: x[(((0,),),)])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest, pickle, types
|
||||
import unittest, pickle, types, tracemalloc
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, TinyJit, Variable, dtypes
|
||||
from tinygrad.helpers import GlobalCounters, ContextVar, Context
|
||||
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
|
||||
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, UOp
|
||||
|
||||
class TestPickle(unittest.TestCase):
|
||||
@@ -78,6 +78,22 @@ class TestPickle(unittest.TestCase):
|
||||
a2:UOp = pickle.loads(s)
|
||||
self.assertListEqual(a2.base.realized.as_memoryview().cast("I").tolist(), [0, 1, 2, 3])
|
||||
|
||||
@unittest.skipIf(DEV.interface.startswith("MOCK"), "mock device buffers live in host RAM, not VRAM")
|
||||
def test_pickle_oob_ram(self):
|
||||
N, M = 8, 10**6
|
||||
ts = [Tensor.rand(M, dtype='float32').realize() for _ in range(N)]
|
||||
tracemalloc.start()
|
||||
st = pickle.dumps(ts, protocol=5, buffer_callback=lambda pb: pb.release())
|
||||
self.assertLess(tracemalloc.get_traced_memory()[1], N*M*4)
|
||||
tracemalloc.reset_peak()
|
||||
def make_fake_buffers():
|
||||
for _ in range(N):
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
yield pickle.PickleBuffer(bytearray(M*4))
|
||||
pickle.loads(st, buffers=make_fake_buffers())
|
||||
self.assertLess(tracemalloc.get_traced_memory()[1], N*M*4)
|
||||
tracemalloc.stop()
|
||||
|
||||
def test_pickle_unrealized_tensor(self):
|
||||
t = Tensor.ones(10, 10)
|
||||
st = pickle.dumps(t)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, UOp
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
from tinygrad.uop.ops import KernelInfo
|
||||
|
||||
def wait_loop_kernel(C:UOp) -> UOp:
|
||||
N = 10
|
||||
|
||||
# a RANGE with no src is a bound-less loop header: a jump target with no induction variable.
|
||||
# the compare and conditional backedge are expanded by the renderers from the loop RANGE/END
|
||||
l = UOp.loop(0)
|
||||
|
||||
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
|
||||
|
||||
# i = 0
|
||||
i = i.after(i[0].store(0))
|
||||
|
||||
# i + 1, read loop-carried through after(l)
|
||||
inc = i.after(l)[0].load() + 1
|
||||
|
||||
# i = inc; END(store, l, cond): conditional backedge, loop again while inc < N (do-while)
|
||||
# NOTE: the cond uses the computed value, not a reload of the register
|
||||
st = i[0].store(inc)
|
||||
i = i.after(st.end(l, inc < N))
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="wait_loop"))
|
||||
|
||||
def nested_loop_kernel(C:UOp) -> UOp:
|
||||
r = UOp.range(4, 0)
|
||||
l = UOp.loop(1)
|
||||
|
||||
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
|
||||
i = i.after(i[0].store(0))
|
||||
|
||||
inc = i.after(l, r)[0].load() + 1
|
||||
st = i[0].store(inc)
|
||||
|
||||
lend = st.end(l, inc < (r.cast(dtypes.int)+1)*3)
|
||||
i = i.after(lend.end(r))
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="nested_loop", opts_to_apply=()))
|
||||
|
||||
def two_loops_kernel(C:UOp) -> UOp:
|
||||
# two sequential loops on the same counter: ++ until 10, then ++ until 25
|
||||
l1, l2 = UOp.loop(0), UOp.loop(1)
|
||||
|
||||
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
|
||||
i = i.after(i[0].store(0))
|
||||
|
||||
inc1 = i.after(l1)[0].load() + 1
|
||||
i = i.after(i[0].store(inc1).end(l1, inc1 < 10))
|
||||
|
||||
inc2 = i.after(l2)[0].load() + 1
|
||||
i = i.after(i[0].store(inc2).end(l2, inc2 < 25))
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="two_loops", opts_to_apply=()))
|
||||
|
||||
def loop_in_loop_kernel(C:UOp) -> UOp:
|
||||
# outer loop while i < 12, inner loop increments until i % 4 == 0 -> 12
|
||||
l1, l2 = UOp.loop(0), UOp.loop(1)
|
||||
|
||||
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
|
||||
i = i.after(i[0].store(0))
|
||||
|
||||
inc = i.after(l1, l2)[0].load() + 1
|
||||
st = i[0].store(inc)
|
||||
|
||||
# the outer END closes the inner END, and its cond reloads the register after the inner loop (in scope at the outer level)
|
||||
e2 = st.end(l2, inc % 4 != 0)
|
||||
oc = i.after(e2)[0].load()
|
||||
i = i.after(e2.end(l1, oc < 12))
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="loop_in_loop", opts_to_apply=()))
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, (NIRRenderer, X86Renderer)), "loops are not supported in LVP and X86")
|
||||
class TestWaitLoop(unittest.TestCase):
|
||||
def test_wait_loop(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
c = Tensor.custom_kernel(c, fxn=wait_loop_kernel)[0]
|
||||
c.realize()
|
||||
self.assertEqual(c.item(), 10)
|
||||
|
||||
def test_nested_loop_in_range(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
c = Tensor.custom_kernel(c, fxn=nested_loop_kernel)[0]
|
||||
c.realize()
|
||||
self.assertEqual(c.item(), 12)
|
||||
|
||||
def test_two_sequential_loops(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
c = Tensor.custom_kernel(c, fxn=two_loops_kernel)[0]
|
||||
c.realize()
|
||||
self.assertEqual(c.item(), 25)
|
||||
|
||||
def test_loop_in_loop(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
c = Tensor.custom_kernel(c, fxn=loop_in_loop_kernel)[0]
|
||||
c.realize()
|
||||
self.assertEqual(c.item(), 12)
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -315,6 +315,19 @@ class TestAutoCastType(unittest.TestCase):
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + True).dtype == dt
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_pad_scalar(self, dt):
|
||||
t = Tensor.ones(4, dtype=dt)
|
||||
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
|
||||
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
|
||||
assert t.pad(((1, 1),), value=True).dtype == dt
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_sort(self, dt):
|
||||
# sort pads with dtype.min/max, a scalar of its own dtype
|
||||
assert Tensor([3, 1, 2], dtype=dt).sort()[0].dtype == dt
|
||||
assert Tensor([3, 1, 2], dtype=dt).sort(descending=True)[0].dtype == dt
|
||||
|
||||
@given(strat.sampled_from(dtype_floats))
|
||||
def test_int_div_int(self, default_float):
|
||||
dtypes.default_float = default_float
|
||||
@@ -415,6 +428,9 @@ class TestAutoCastType(unittest.TestCase):
|
||||
self.check_where_alternate_input_other(3.1, True, dtypes.default_float)
|
||||
self.check_where_alternate_input_other(3, 2, dtypes.default_int)
|
||||
self.check_where_alternate_input_other(3, True, dtypes.default_int)
|
||||
|
||||
def test_where_non_bool_cond_raises(self):
|
||||
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0)
|
||||
self.check_where_alternate_input_other(False, True, dtypes.bool)
|
||||
|
||||
@given(strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes))
|
||||
|
||||
@@ -38,7 +38,6 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertEqual(((t_bool + 1) + t_i8).dtype, dtypes.int8)
|
||||
self.assertEqual(((t_bool + 1) + t_u16).dtype, dtypes.uint16)
|
||||
self.assertEqual((Tensor(3) + t_i8).dtype, dtypes.int8)
|
||||
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
|
||||
# zeros/ones are full with a python fill value, so they are weak too (jnp.zeros pins float32; deliberate divergence)
|
||||
self.assertEqual((Tensor.zeros(3) + t_f16).dtype, dtypes.float16)
|
||||
|
||||
@@ -47,6 +46,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertEqual((t_i8 + 1).dtype, dtypes.int8)
|
||||
self.assertEqual((t_f16 + 0.5).dtype, dtypes.float16)
|
||||
self.assertEqual((t_f32 + t_f16).dtype, dtypes.float32)
|
||||
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
|
||||
|
||||
@unittest.expectedFailure # TODO: dot of a weak const tensor defers to the other operand once python scalars are weak consts
|
||||
def test_dot_defers_weak(self):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
import unittest, math
|
||||
import numpy as np
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, KernelInfo
|
||||
from tinygrad.uop.ops import UOp, KernelInfo, Ops
|
||||
|
||||
class TestTensorGradient(unittest.TestCase):
|
||||
def test_example(self):
|
||||
@@ -98,6 +98,33 @@ class TestTensorGradient(unittest.TestCase):
|
||||
x = Tensor.randn(4, 4)
|
||||
np.testing.assert_allclose(x.pad(((1,0),(0,0))).gradient(x, gradient=g2)[0].numpy(), np.zeros((4, 4)))
|
||||
|
||||
def test_implicit_broadcast_where_gradient(self):
|
||||
# WHERE with a bare ()-shape branch: the scalar's gradient counts the positions where it is selected
|
||||
cond, x, w = Tensor([True, False, True]), Tensor([1.0, 2.0, 3.0]), Tensor(4.0)
|
||||
dw = Tensor(cond.uop.alu(Ops.WHERE, x.uop, w.uop)).sum().gradient(w)[0]
|
||||
self.assertEqual(dw.shape, ())
|
||||
self.assertEqual(dw.item(), 1.0)
|
||||
dw = Tensor(cond.uop.alu(Ops.WHERE, w.uop, x.uop)).sum().gradient(w)[0]
|
||||
self.assertEqual(dw.item(), 2.0)
|
||||
|
||||
def test_implicit_broadcast_alu_gradient(self):
|
||||
# MUL with a bare ()-shape src, no EXPAND in the graph
|
||||
x, w = Tensor([1.0, 2.0, 3.0]), Tensor(2.0)
|
||||
m = x.uop.alu(Ops.MUL, w.uop)
|
||||
self.assertIs(m.src[1], w.uop)
|
||||
dw = Tensor(m).sum().gradient(w)[0]
|
||||
self.assertEqual(dw.shape, ())
|
||||
self.assertEqual(dw.item(), 6.0)
|
||||
|
||||
def test_implicit_broadcast_intermediate_accumulation(self):
|
||||
# s is used directly and through an implicit broadcast edge, each edge's gradient reduces to s's shape before they sum
|
||||
x, p = Tensor([1.0, 2.0, 3.0]), Tensor(0.5)
|
||||
s = p.sin()
|
||||
z = Tensor(x.uop.alu(Ops.MUL, s.uop)).sum() + s
|
||||
dp = z.gradient(p)[0]
|
||||
self.assertEqual(dp.shape, ())
|
||||
self.assertAlmostEqual(dp.item(), 7*math.cos(0.5), places=5)
|
||||
|
||||
def test_bare_const_skipped_by_backward(self):
|
||||
Tensor.manual_seed(0)
|
||||
w = Tensor(1.0)
|
||||
|
||||
@@ -63,6 +63,13 @@ class TestMultiTensor(unittest.TestCase):
|
||||
np.testing.assert_equal((s + Tensor(UOp.const(dtypes.float, 1.0))).numpy(), [2, 3, 4, 5])
|
||||
np.testing.assert_equal((s + Tensor(UOp.const(dtypes.float, 1.0)).reshape((1,)).expand((4,))).numpy(), [2, 3, 4, 5])
|
||||
|
||||
def test_add_rank_expand_shard(self):
|
||||
# a sharded src keeps its own rank under implicit broadcast, its shard axis right-aligns into the output
|
||||
a = Tensor([1.,2.,3.,4.]).shard(devices_2, 0)
|
||||
b = Tensor([[10.,20.,30.,40.]]).shard(devices_2, None)
|
||||
self.assertEqual((a+b).uop.axis, 1)
|
||||
np.testing.assert_equal((a+b).numpy(), [[11.,22.,33.,44.]])
|
||||
|
||||
def test_shard_reduce(self):
|
||||
self._test_shard_op(lambda t:t.reshape(2, 3).sum(axis=1), [3.,3.], n=6)
|
||||
self._test_shard_op(lambda t:t.reshape(2, 3).sum(axis=0), [2.,2.,2.], n=6)
|
||||
@@ -602,8 +609,8 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
|
||||
np.testing.assert_allclose((a+a).numpy(), (b+b).numpy(), rtol=1e-7, atol=1e-3)
|
||||
np.testing.assert_equal((a+1).numpy(), (b+1).numpy())
|
||||
np.testing.assert_equal((1+a).numpy(), (1+b).numpy())
|
||||
np.testing.assert_allclose((a.where(a+a, a)).numpy(), (b.where(b+b, b)).numpy(), rtol=1e-7, atol=1e-3)
|
||||
np.testing.assert_allclose((a.where(1, 0)).numpy(), (b.where(1, 0)).numpy(), rtol=1e-7, atol=1e-3)
|
||||
np.testing.assert_allclose((a.bool().where(a+a, a)).numpy(), (b.bool().where(b+b, b)).numpy(), rtol=1e-7, atol=1e-3)
|
||||
np.testing.assert_allclose((a.bool().where(1, 0)).numpy(), (b.bool().where(1, 0)).numpy(), rtol=1e-7, atol=1e-3)
|
||||
|
||||
# reduce
|
||||
np.testing.assert_allclose(a.max().numpy(), b.max().numpy(), rtol=1e-7, atol=1e-3)
|
||||
|
||||
@@ -2,7 +2,7 @@ import heapq
|
||||
from typing import Any
|
||||
from collections import defaultdict
|
||||
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat, multirange_str
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.helpers import prod, getenv, TUPLE_ORDER
|
||||
|
||||
def linearize(sink:UOp) -> list[UOp]:
|
||||
@@ -85,9 +85,9 @@ pm_add_control_flow = PatternMatcher([
|
||||
])
|
||||
|
||||
def do_split_ends(e:UOp):
|
||||
ret = e.src[0]
|
||||
for r in sorted(UOp.sink(*e.src[1:]).ranges, key=lambda x: x.arg, reverse=True): ret = ret.end(r)
|
||||
return ret
|
||||
ret, backedge = e.src[0], tuple(x for x in e.src[1:] if x.dtype in (dtypes.void, dtypes.bool))
|
||||
for r in sorted(UOp.sink(*[x for x in e.src[1:] if x not in backedge]).ranges, key=lambda x: x.arg, reverse=True): ret = ret.end(r)
|
||||
return ret.end(*backedge) if len(backedge) else ret
|
||||
|
||||
pm_split_ends = PatternMatcher([
|
||||
# split the ends
|
||||
|
||||
@@ -21,8 +21,9 @@ class Scheduler:
|
||||
|
||||
@property
|
||||
def rngs(self):
|
||||
# always in order by axistype
|
||||
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.vmax > 0], key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
|
||||
# always in order by axistype. void RANGEs are loops, not opt axes
|
||||
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.dtype is not dtypes.void and u.vmax > 0],
|
||||
key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
|
||||
@property
|
||||
def shape_len(self) -> int: return len(self.rngs)
|
||||
@property
|
||||
|
||||
@@ -9,7 +9,9 @@ def flatten_range(r:UOp) -> UOp|None:
|
||||
off = range_start[r.op]
|
||||
rngs = r.src[off:]
|
||||
if not len(rngs): return None
|
||||
return r.replace(src=r.src[:off]+tuple(UOp.sink(*rngs).ranges))
|
||||
# ranges in the cond should not be ended
|
||||
backedge = tuple(x for x in rngs if x.dtype in (dtypes.void, dtypes.bool))
|
||||
return r.replace(src=r.src[:off]+tuple(UOp.sink(*[x for x in rngs if x not in backedge]).ranges)+backedge)
|
||||
|
||||
pm_flatten_range = PatternMatcher([
|
||||
# real ranges only
|
||||
@@ -19,6 +21,7 @@ pm_flatten_range = PatternMatcher([
|
||||
# index/range arithmetic uses FLOORDIV/FLOORMOD prior to late rewrite
|
||||
def count_divmod(x:UOp) -> int: return sum(u.op in {Ops.FLOORDIV, Ops.FLOORMOD} for u in x.backward_slice)
|
||||
def simplify_merge_adjacent(u:UOp) -> UOp|None:
|
||||
if not all(r.op is Ops.RANGE for r in u.ended_ranges): return None
|
||||
reduce_ranges = [x.ranges for x in u.backward_slice_with_self if x.op is Ops.REDUCE]
|
||||
# on END we only want to merge adjacent ranges, on REDUCE we want to try all combinations
|
||||
for r0, r1 in (zip(u.ended_ranges, u.ended_ranges[1:]) if u.op is Ops.END else itertools.permutations(u.ended_ranges, 2)):
|
||||
|
||||
+7
-5
@@ -100,8 +100,8 @@ class MultiBuffer:
|
||||
|
||||
class Buffer:
|
||||
profile_events:list[ProfileEvent] = []
|
||||
def __init__(self, device:str, size:int, dtype:DType, opaque:Any=None, options:BufferSpec|None=None, initial_value:bytes|None=None,
|
||||
uop_refcount=0, base:Buffer|None=None, offset:int=0, preallocate=False):
|
||||
def __init__(self, device:str, size:int, dtype:DType, opaque:Any=None, options:BufferSpec|None=None,
|
||||
initial_value:bytes|pickle.PickleBuffer|None=None, uop_refcount=0, base:Buffer|None=None, offset:int=0, preallocate=False):
|
||||
assert isinstance(dtype, DType)
|
||||
self.device, self.size, self.dtype, self.options, self.offset, self.allocated_views = device, size, dtype, options, offset, 0
|
||||
self._bufs: dict[str, Any] = {}
|
||||
@@ -113,6 +113,7 @@ class Buffer:
|
||||
if initial_value is not None:
|
||||
self.allocate()
|
||||
self.copy_from(Buffer("PYTHON", self.size, self.dtype, opaque=memoryview(bytearray(initial_value))))
|
||||
if isinstance(initial_value, pickle.PickleBuffer): initial_value.release()
|
||||
else:
|
||||
assert base._base is None, "base can't have a base"
|
||||
assert device == base.device, "base must have the same device"
|
||||
@@ -171,12 +172,13 @@ class Buffer:
|
||||
self.allocator.free(self._buf, self.nbytes, self.options)
|
||||
elif self._base is not None: self._base.allocated_views -= 1
|
||||
self._bufs.clear()
|
||||
def __reduce__(self):
|
||||
buf = None
|
||||
def __reduce_ex__(self, protocol):
|
||||
buf:bytearray|pickle.PickleBuffer|None = None
|
||||
if self._base is not None:
|
||||
return self.__class__, (self.device, self.size, self.dtype, None, None, None, 0, self.base, self.offset, self.is_allocated())
|
||||
if self.device == "NPY": return self.__class__, (self.device, self.size, self.dtype, self._buf, self.options, None, self.uop_refcount)
|
||||
if self.is_allocated(): buf = bytearray(self.as_memoryview())
|
||||
if self.is_allocated():
|
||||
buf = pickle.PickleBuffer(self.as_memoryview()) if protocol >= 5 else bytearray(self.as_memoryview())
|
||||
return self.__class__, (self.device, self.size, self.dtype, None, self.options, buf, self.uop_refcount)
|
||||
@property
|
||||
def trace_num(self) -> int:
|
||||
|
||||
@@ -6,7 +6,7 @@ from tinygrad.helpers import argfix, polyN
|
||||
from tinygrad.mixin.creation import CreationMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import UOp
|
||||
from tinygrad.uop.ops import UOp, sint
|
||||
|
||||
|
||||
class ElementwiseMixin(CreationMixin):
|
||||
@@ -18,9 +18,11 @@ class ElementwiseMixin(CreationMixin):
|
||||
def ufix(self, x: 'Self|ConstType|UOp') -> Self:
|
||||
return x if isinstance(x, type(self)) else self._wrap_uop(self._uop.ufix(x))
|
||||
|
||||
# implemented in OpMixin, broadcasting needs the movement ops
|
||||
def _broadcasted(self, y: 'Self|ConstType|UOp', reverse: bool = False) -> tuple[Self, Self]:
|
||||
raise NotImplementedError
|
||||
y = self.ufix(y)
|
||||
x, y = (self, y) if not reverse else (y, self)
|
||||
if x.dtype == y.dtype: return x, y
|
||||
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
|
||||
|
||||
def _binop(self, op: Ops, x: Self | ConstType, reverse: bool) -> Self:
|
||||
lhs, rhs = self._broadcasted(x, reverse)
|
||||
@@ -414,10 +416,19 @@ class ElementwiseMixin(CreationMixin):
|
||||
m = a.maximum(b)
|
||||
return ((a-m).exp() + (b-m).exp()).log() + m
|
||||
|
||||
def where(self, x: Self | ConstType, y: Self | ConstType) -> Self:
|
||||
ref: Self = x if isinstance(x, type(self)) else y if isinstance(y, type(self)) else \
|
||||
self.cast(least_upper_dtype(dtypes.from_py(x), dtypes.from_py(y)))
|
||||
return self.alu(Ops.WHERE, ref.ufix(x), ref.ufix(y))
|
||||
def where(self, x: 'Self | ConstType | sint', y: 'Self | ConstType | sint') -> Self:
|
||||
"""
|
||||
Returns a tensor of elements selected from either `x` or `y`, depending on `self`.
|
||||
`output_i = x_i if self_i else y_i`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
cond = Tensor([[True, True, False], [True, False, False]])
|
||||
print(cond.where(1, 3).numpy())
|
||||
```
|
||||
"""
|
||||
ref = x if isinstance(x, type(self)) else y if isinstance(y, type(self)) else self
|
||||
x, y = ref.ufix(x)._broadcasted(y)
|
||||
return self.alu(Ops.WHERE, x, y)
|
||||
|
||||
def masked_fill(self, mask:Self, value:Self|PyConst) -> Self:
|
||||
"""
|
||||
@@ -548,9 +559,7 @@ class ElementwiseMixin(CreationMixin):
|
||||
# TODO: int pow
|
||||
if not base.is_floating_point() and isinstance(x, ConstType) and not (isinstance(x, int) and x >= 0):
|
||||
raise RuntimeError("base needs to be float")
|
||||
ret = base.alu(Ops.POW, exponent)
|
||||
# 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
|
||||
return base.alu(Ops.POW, exponent)
|
||||
|
||||
def __pow__(self, x: Self | ConstType) -> Self:
|
||||
return self.pow(x)
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
from typing import cast
|
||||
import math, dataclasses
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata
|
||||
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):
|
||||
def broadcast_to_input(x:UOp) -> UOp: return x._broadcast_to(ret.src[0].shape)
|
||||
if op == Ops.ADD: return (broadcast_to_input(ctx),)
|
||||
if op == Ops.MAX:
|
||||
assert ret.op is Ops.REDUCE, "only works on REDUCE"
|
||||
mask = ret.src[0].eq(broadcast_to_input(ret)).cast(ctx.dtype)
|
||||
count = mask._rop(Ops.ADD, tuple(range(ret.arg[1])))
|
||||
return ((mask/broadcast_to_input(count)) * broadcast_to_input(ctx),)
|
||||
if op == Ops.MUL: return (broadcast_to_input(ctx * ret) / ret.src[0],)
|
||||
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)."""
|
||||
@@ -67,9 +62,7 @@ pm_gradient = PatternMatcher([
|
||||
(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, name="ret"), lambda ctx, ret:
|
||||
(ctx.cast(sum_acc_dtype(ctx.dtype))._rop(Ops.ADD, tuple(range(len(ret.marg))))
|
||||
.reshape(ret.src[0].shape).cast(ctx.dtype), 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)),)),
|
||||
@@ -119,6 +112,9 @@ def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp
|
||||
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
|
||||
|
||||
+3
-13
@@ -11,7 +11,7 @@ from tinygrad.helpers import all_int, argfix, argsort, ceildiv, flatten, flat_to
|
||||
from tinygrad.helpers import resolve_pool_pads, round_up, IMAGE, FLOAT16, WINO
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
from tinygrad.uop.ops import sint
|
||||
|
||||
ReductionStr = Literal["mean", "sum", "none"]
|
||||
|
||||
@@ -110,8 +110,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
consecutive = dims == list(range(dims[0], dims[0] + len(dims)))
|
||||
if v is None and len(dims) > 1 and consecutive and all_int(ishp := tuple(x.shape[d] for d in dims)):
|
||||
strides = tuple(prod(ishp[i+1:]) for i in range(len(dims)))
|
||||
try: linear_idx = type(self).usum(*[t._broadcast_to(big_shape) * s for t, s in zip(tensors, strides)])
|
||||
except ValueError as err: raise IndexError(f"cannot broadcast indices: {err}") from err
|
||||
linear_idx = type(self).usum(*[t * s for t, s in zip(tensors, strides)])
|
||||
valid = type(self).uprod(*[(t >= 0) & (t < s) for t, s in zip(tensors, ishp)])
|
||||
pre, post = x.shape[:dims[0]], x.shape[dims[-1]+1:]
|
||||
x = x.reshape(pre + (prod(ishp),) + post)[tuple([slice(None)] * len(pre)) + (valid.where(linear_idx, 0),)]
|
||||
@@ -285,8 +284,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
pads = tuple((smax(pB,0), smax(pA,0)) for pB,pA in pX) if has_neg else pX
|
||||
base = MovementMixin.pad(X, pads)
|
||||
if value == 0: return base
|
||||
if value is not Invalid: base = base.cast(least_upper_dtype(base.dtype, dtypes.from_py(value)))
|
||||
return MovementMixin.pad(X.const_like(1).cast(dtypes.bool), pads).where(base, base.const_like(value))
|
||||
return MovementMixin.pad(X.const_like(1).cast(dtypes.bool), pads).where(base, value)
|
||||
|
||||
def _pad_circular(self, pX:tuple[tuple[sint, sint], ...]) -> Self:
|
||||
# shrink first for negative pads, then wrap the non-negative remainder
|
||||
@@ -357,14 +355,6 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
if mode in {"reflect", "replicate"}: return self._pad_reflect_replicate(pX, mode)
|
||||
raise NotImplementedError(f"{mode=} is not supported")
|
||||
|
||||
def _broadcasted(self, y:Self|ConstType|UOp, reverse:bool=False) -> tuple[Self, Self]:
|
||||
if not isinstance(y, type(self)): y = self.ufix(y)
|
||||
x, y = (self, y) if not reverse else (y, self)
|
||||
out_shape = _broadcast_shape(x.shape, y.shape)
|
||||
x, y = x._broadcast_to(out_shape), y._broadcast_to(out_shape)
|
||||
if x.dtype == y.dtype: return x, y
|
||||
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
|
||||
|
||||
def dot(self, w:Self, dtype:DTypeLike|None=None) -> Self:
|
||||
"""
|
||||
Performs dot product between two tensors.
|
||||
|
||||
@@ -11,7 +11,7 @@ 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]._broadcast_to(x.shape).cast(dtypes.uint64) << 32) | key[0]._broadcast_to(x.shape).cast(dtypes.uint64))
|
||||
x = x.threefry((key[1].cast(dtypes.uint64) << 32) | key[0].cast(dtypes.uint64))
|
||||
return (x & 0xffffffff).cast(dtypes.uint32).cat(((x >> 32) & 0xffffffff).cast(dtypes.uint32))
|
||||
|
||||
@classmethod
|
||||
|
||||
+3
-1
@@ -617,6 +617,8 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
|
||||
def Add(x:Tensor,y:Tensor, broadcast=None, axis=None): return x + y
|
||||
def Sub(x:Tensor|int,y:Tensor): return x - y # some test has input as int
|
||||
def Div(x:Tensor,y:Tensor): return x.div(y, rounding_mode='trunc' if dtypes.is_int(x.dtype) else None)
|
||||
# ONNX Pow is (T, T1) -> T, the output takes the base dtype while Tensor.pow promotes base and exponent
|
||||
def Pow(x:Tensor,y:Tensor): return x.pow(y).round().cast(x.dtype) if dtypes.is_int(x.dtype) else x.pow(y)
|
||||
def Less(x:Tensor,y:Tensor): return x < y
|
||||
def LessOrEqual(x:Tensor,y:Tensor): return x <= y
|
||||
def Greater(x:Tensor,y:Tensor): return x > y
|
||||
@@ -1297,7 +1299,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
|
||||
|
||||
return {
|
||||
# Tensor ops
|
||||
**{op: getattr(Tensor, op.lower()) for op in ("Neg", "Reciprocal", "Pow", "Sqrt", "Sign", "Abs", "Exp", "Log", "Mish", "Sin", "Cos", "Tan",
|
||||
**{op: getattr(Tensor, op.lower()) for op in ("Neg", "Reciprocal", "Sqrt", "Sign", "Abs", "Exp", "Log", "Mish", "Sin", "Cos", "Tan",
|
||||
"Asin", "Acos", "Atan", "Relu", "Sigmoid", "MatMul", "Floor", "Ceil", "IsNaN", "Softplus", "HardSwish", "Where", "Mul", "Sinh", "Cosh",
|
||||
"Tanh", "Softsign", "Asinh", "Acosh", "Atanh", "Elu", "Celu", "Selu", "Round", "Erf")},
|
||||
# Implemented ops
|
||||
|
||||
@@ -39,9 +39,10 @@ class Estimates:
|
||||
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.scalar().itemsize)
|
||||
if u.op is Ops.RANGE:
|
||||
mult_stack.append(mults)
|
||||
mults *= cast(sint, u.src[0].ssimplify())
|
||||
# SPECIAL are already counted in mults
|
||||
mults = mults.substitute({x:x.const_like(0) for x in mults.toposort() if x.op is Ops.SPECIAL}) if isinstance(mults, UOp) else mults
|
||||
if u.dtype is not dtypes.void: # unbounded loop, unknown trip count
|
||||
mults *= cast(sint, u.src[0].ssimplify())
|
||||
# SPECIAL are already counted in mults
|
||||
mults = mults.substitute({x:x.const_like(0) for x in mults.toposort() if x.op is Ops.SPECIAL}) if isinstance(mults, UOp) else mults
|
||||
elif u.op is Ops.END: mults = mult_stack.pop(-1)
|
||||
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
|
||||
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
|
||||
|
||||
@@ -12,9 +12,11 @@ base_rewrite = PatternMatcher([
|
||||
# local/reg buffers
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx,x: ctx.render_buffer(x)),
|
||||
|
||||
# range/if/endif
|
||||
# range/loop/if/endif
|
||||
(UPat(Ops.RANGE, dtypes.void, name="x"), lambda ctx,x: "for (;;) {"),
|
||||
(UPat(Ops.RANGE, name="x"),
|
||||
lambda ctx,x: f"for ({ctx.render_dtype(x.dtype)} {ctx[x]} = 0; {ctx[x]} < {ctx[x.src[0]]}; {ctx[x]}++) {{"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE), UPat(name="c", dtype=dtypes.bool))), lambda ctx,c: f" if (!({ctx[c]})) {{ break; }}\n}}"),
|
||||
(UPat(Ops.IF, name="x"), lambda ctx,x: f"if ({ctx[x.src[0]]}) {{"),
|
||||
(UPat((Ops.ENDIF, Ops.END)), lambda ctx: "}"),
|
||||
|
||||
@@ -227,14 +229,14 @@ class CStyleLanguage(Renderer):
|
||||
|
||||
if u.op in {Ops.ENDIF, Ops.END}: depth -= 1
|
||||
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG) or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
|
||||
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
|
||||
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
|
||||
r[u] = l
|
||||
else:
|
||||
if u.op not in {Ops.RANGE, Ops.STORE, Ops.BUFFER} and u.dtype != dtypes.void:
|
||||
l = f"{self.render_type(u)} {r[u]} = {l}" + (";" if u.op is not Ops.SPECIAL else "")
|
||||
kernel.append(" "*depth + l)
|
||||
kernel.append("\n".join(" "*depth + line for line in l.split("\n")))
|
||||
if prefix: c[prefix] += 1 # if it was used, increment
|
||||
if u.op in {Ops.IF, Ops.RANGE}: depth += 1
|
||||
del self.r
|
||||
@@ -595,3 +597,8 @@ class QCOMCLRenderer(OpenCLRenderer):
|
||||
def supported_dtypes(self):
|
||||
return {d for d in Renderer.supported_dtypes(self)
|
||||
if (d != dtypes.float16 or (bool(IMAGE) and bool(FLOAT16))) and d not in dtypes.fp8s+(dtypes.bfloat16,dtypes.double)}
|
||||
|
||||
# QCOM's load vectorizer emits invalid IR for vectorized bool loads ("Range types must match load type"), type bool buffers as uchar
|
||||
def _render_dtype(self, dtype:DType, sz:int=1, addrspace=AddrSpace.ALU, mutable=True, override_ptr=False, shape=None):
|
||||
if dtype == dtypes.bool and addrspace == AddrSpace.GLOBAL: dtype = dtypes.uint8
|
||||
return super()._render_dtype(dtype, sz, addrspace, mutable, override_ptr, shape)
|
||||
|
||||
@@ -96,6 +96,11 @@ base_rewrite = PatternMatcher([
|
||||
(UPat(Ops.WHERE, name="x"), lambda ctx,x:
|
||||
f" {ctx[x]} = select {ldt(x.src[0].dtype)} {ctx[x.src[0]]}, {ldt(x.src[1].dtype)} {ctx[x.src[1]]}, {ldt(x.src[2].dtype)} {ctx[x.src[2]]}"),
|
||||
|
||||
# loop (a RANGE with no src is an unbounded loop header)
|
||||
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx,l: f" br label %loop_{ctx[l][1:]}\nloop_{ctx[l][1:]}:"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx,l,c:
|
||||
f" br i1 {ctx[c]}, label %loop_{ctx[l][1:]}, label %loop_exit_{ctx[l][1:]}\nloop_exit_{ctx[l][1:]}:"),
|
||||
|
||||
# range
|
||||
(UPat(Ops.RANGE, name="r"), lambda ctx,r:
|
||||
f" br label %loop_entry_{range_str(r)}\n"
|
||||
|
||||
@@ -116,6 +116,9 @@ string_rewrite = PatternMatcher([
|
||||
# simple
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
|
||||
f".shared .align 16 .b8 local{x.arg.slot}[{x.max_numel()*x.dtype.itemsize}];", f"mov.u64 {ctx.r[x]}, local{x.arg.slot}[0];"]),
|
||||
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx, l: f"WAITLOOP_{ctx.uops.index(l)}:"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx, l, c:
|
||||
f"@{ctx.r[c]} bra WAITLOOP_{ctx.uops.index(l)};"),
|
||||
(UPat(Ops.RANGE, name="r"), lambda ctx, r: [
|
||||
f"mov.u32 {ctx.r[r]}, -1;",
|
||||
f"bra END_{ctx.r[r][1:]};",
|
||||
@@ -211,6 +214,7 @@ class PTXRenderer(Renderer):
|
||||
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
|
||||
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
|
||||
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
|
||||
if u.op is Ops.RANGE and u.dtype == dtypes.void: prefix = None # loop headers don't have a register
|
||||
if prefix: r[u] = ssa(prefix, u, dtype)
|
||||
|
||||
l: str|list[str]|None = string_rewrite.rewrite(u, ctx=self)
|
||||
|
||||
@@ -48,7 +48,6 @@ class PythonProgram:
|
||||
st = time.perf_counter()
|
||||
warp = list(itertools.product(*[range(x) for x in local_size[::-1]]))
|
||||
warp_size = len(warp)
|
||||
void_ops = {Ops.END, Ops.BARRIER, Ops.IF, Ops.ENDIF, Ops.SINK, Ops.NOOP, Ops.GROUP, Ops.STORE}
|
||||
for idxs in itertools.product(*[range(x) for x in global_size[::-1]]):
|
||||
values: dict[UOp, Any] = {}
|
||||
pbufs: list[memoryview] = list(bufs)
|
||||
@@ -57,11 +56,15 @@ class PythonProgram:
|
||||
i = 0
|
||||
while i < len(self.uops):
|
||||
u = self.uops[i]
|
||||
src_values = [values[v] for v in u.src if v.op not in void_ops]
|
||||
src_dtypes = [v.dtype for v in u.src if v.op not in void_ops]
|
||||
src_values = [values[v] for v in u.src if v.dtype is not dtypes.void]
|
||||
src_dtypes = [v.dtype for v in u.src if v.dtype is not dtypes.void]
|
||||
if getenv("TRACE"): print(i, u.op, u.dtype, u.arg, src_values, src_dtypes)
|
||||
if u.op is Ops.END:
|
||||
i = self.uop_to_index[u.src[1]]
|
||||
if len(u.src) == 3:
|
||||
# conditional backedge on a loop: jump back while the condition is true
|
||||
if values[u.src[2]][0]: i = self.uop_to_index[u.src[1]]
|
||||
else: i += 1
|
||||
else: i = self.uop_to_index[u.src[1]]
|
||||
continue
|
||||
if u.op is Ops.IF:
|
||||
exec_masks.append([x and y for x,y in zip(exec_masks[-1], src_values[0])])
|
||||
@@ -71,7 +74,7 @@ class PythonProgram:
|
||||
exec_masks.pop()
|
||||
i += 1
|
||||
continue
|
||||
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP):
|
||||
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP) or (u.op is Ops.RANGE and u.dtype == dtypes.void):
|
||||
# in the python emulator, the warp is always in sync
|
||||
i += 1
|
||||
continue
|
||||
|
||||
@@ -196,6 +196,12 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
|
||||
if x.dtype == dtypes.weakint: continue # TODO: why do I need this?
|
||||
ending_ranges[x] = sum([ending_ranges.get(u, []) for u in consumer_map[x]], [])
|
||||
# ranges the consumers iterate that this node broadcasts over
|
||||
ended = [rctx.range_map[c][0][i] for c in consumer_map[x] if c in rctx.range_map and c.op in GroupOp.Broadcastable
|
||||
for i in broadcast_axes(x.shape, c.shape)]
|
||||
broadcast_ending_ranges = list(UOp.sink(*ended).ranges)
|
||||
# fusion decision: REDUCE before the broadcast
|
||||
if x.op is Ops.REDUCE: ending_ranges[x] += broadcast_ending_ranges
|
||||
|
||||
# *** the ranges on the output are
|
||||
# 1. new if this op is realized
|
||||
@@ -254,6 +260,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
if len(_realize_axis):
|
||||
rctx.realize_map[x] = _realize_axis
|
||||
out_rngs = tuple([(rctx.new_range(x.shape[i]) if i in _realize_axis else r) for i,r in enumerate(out_rngs)])
|
||||
ending_ranges[x] += broadcast_ending_ranges
|
||||
|
||||
# TODO: some ops don't have shape, enable this after the `.st` property is removed
|
||||
#assert len(out_rngs) == len(x.shape), \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from tinygrad.helpers import all_same, prod, getenv, ALLREDUCE_CAST
|
||||
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, graph_rewrite
|
||||
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, graph_rewrite, broadcast_axes, _broadcast_shape
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.schedule.allreduce import handle_allreduce
|
||||
|
||||
@@ -47,20 +47,17 @@ def shard_srcs(msrcs:tuple[UOp, ...], axis:int) -> list[UOp]:
|
||||
assert all_same(devices), f"all buffers must have the same device {devices}"
|
||||
dcount = len(devices[0])
|
||||
|
||||
out_shape = _broadcast_shape(*[x.shape for x in msrcs])
|
||||
srcs:list[UOp] = []
|
||||
for mlb in msrcs:
|
||||
if mlb.axis is None:
|
||||
# no axis, shard it
|
||||
assert mlb.op is not Ops.MULTI
|
||||
srcs.append(mlb._shard(axis, dcount))
|
||||
src_axis = axis - (len(out_shape)-len(mlb.shape))
|
||||
if mlb.axis == src_axis:
|
||||
# same axis, just copy through
|
||||
srcs.append(mlb.src[0])
|
||||
else:
|
||||
assert mlb.op is Ops.MULTI
|
||||
if mlb.axis == axis:
|
||||
# same axis, just copy through
|
||||
srcs.append(mlb.src[0])
|
||||
else:
|
||||
# axis mismatch, copy to all devices, and shard it correctly
|
||||
srcs.append(copy_multi(mlb, mlb.device)._shard(axis, dcount))
|
||||
# otherwise every device gets the full copy, sharded iff this src has the axis (broadcast srcs stay whole)
|
||||
full = mlb if mlb.axis is None else copy_multi(mlb, mlb.device)
|
||||
srcs.append(full if axis in broadcast_axes(mlb.shape, out_shape) else full._shard(src_axis, dcount))
|
||||
return srcs
|
||||
|
||||
def alu_multi(root:UOp):
|
||||
|
||||
+1
-27
@@ -6,7 +6,7 @@ if TYPE_CHECKING: import numpy
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtype, to_dtype, strong_dtype, _from_np_dtype, _to_np_dtype, PyConst
|
||||
from tinygrad.helpers import all_int, getenv, fully_flatten, fetch, Metadata, TRACEMETA, TracingKey
|
||||
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable, _broadcast_shape
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable
|
||||
from tinygrad.mixin.rand import RandMixin
|
||||
from tinygrad.schedule import create_linear_with_vars
|
||||
from tinygrad.device import Buffer, canonicalize_device
|
||||
@@ -489,32 +489,6 @@ class Tensor(RandMixin):
|
||||
def __delitem__(self, indices) -> None:
|
||||
raise TypeError("Tensor does not support deleting items")
|
||||
|
||||
# ***** broadcasted elementwise ops *****
|
||||
|
||||
def where(self:Tensor, x:Tensor|ConstType|sint, y:Tensor|ConstType|sint) -> Tensor:
|
||||
"""
|
||||
Returns a tensor of elements selected from either `x` or `y`, depending on `self`.
|
||||
`output_i = x_i if self_i else y_i`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
cond = Tensor([[True, True, False], [True, False, False]])
|
||||
print(cond.where(1, 3).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
Tensor.manual_seed(42)
|
||||
cond = Tensor.randn(2, 3)
|
||||
print(cond.numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print((cond > 0).where(cond, -float("inf")).numpy())
|
||||
```
|
||||
"""
|
||||
if isinstance(x, Tensor): x, y = x._broadcasted(y)
|
||||
elif isinstance(y, Tensor): y, x = y._broadcasted(x)
|
||||
else: x, y = self.ufix(x)._broadcasted(y)
|
||||
out_shape = _broadcast_shape(self.shape, x.shape)
|
||||
return self.cast(dtypes.bool)._broadcast_to(out_shape)._apply_uop(UOp.where, x._broadcast_to(out_shape), y._broadcast_to(out_shape))
|
||||
|
||||
# ***** op wrappers *****
|
||||
|
||||
# unlike Tensors, UOps are immutable, so these don't go in mixin
|
||||
|
||||
+11
-17
@@ -113,7 +113,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
match op:
|
||||
case Ops.STORE | Ops.CALL | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
|
||||
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | \
|
||||
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.WAIT | Ops.REWRITE_ERROR:
|
||||
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.REWRITE_ERROR:
|
||||
# always void
|
||||
return dtypes.void
|
||||
case Ops.CUSTOM | Ops.CUSTOMI | Ops.INS | Ops.PYLITERAL:
|
||||
@@ -131,7 +131,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
case Ops.SIN | Ops.LOG2 | Ops.EXP2 | Ops.SQRT | Ops.RECIPROCAL:
|
||||
return least_upper_float(src[0].dtype)
|
||||
case Ops.WHERE:
|
||||
assert src[0].dtype == dtypes.bool, f"where first arg isn't bool, it's {src[0].dtype}"
|
||||
if src[0].dtype != dtypes.bool: raise RuntimeError(f"where cond must be bool, got {src[0].dtype}")
|
||||
return promo_dtype(src[1:])
|
||||
case Ops.STACK:
|
||||
if len(src) == 0: return dtypes.void
|
||||
@@ -563,8 +563,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
def ufix(self, x):
|
||||
if isinstance(x, UOp): return x
|
||||
# float self keeps its dtype for any scalar, int self only for int/Invalid scalars
|
||||
if dtypes.is_float(self.dtype) or (dtypes.is_int(self.dtype) and isinstance(x, (int, InvalidType))): return self.const_like(x)
|
||||
return self.const_like(x, dtypes.from_py(x))
|
||||
dtype = self.dtype if dtypes.is_float(self.dtype) or (dtypes.is_int(self.dtype) and isinstance(x, (int, InvalidType))) else dtypes.from_py(x)
|
||||
return UOp.const(dtype, x)
|
||||
def broadcast(self, count:int):
|
||||
if count == 1: return self
|
||||
return UOp(Ops.STACK, src=(self,)*count)
|
||||
@@ -575,19 +575,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
def end(self, *src:UOp): return UOp(Ops.END, src=(self,)+src) if len(src) else self
|
||||
def after(self, *src:UOp, **kwargs): return UOp(Ops.AFTER, src=(self,)+src, **kwargs) if len(src) else self
|
||||
def barrier(self, *src:UOp): return UOp(Ops.BARRIER, src=(self,)+src)
|
||||
def wait(self, **kwargs): return UOp(Ops.WAIT, src=(self,), **kwargs)
|
||||
def ins(self, arg, **kwargs): return UOp(Ops.INS, kwargs.pop("dtype", self.dtype), kwargs.pop("src", self.src), arg, kwargs.pop("tag", self.tag))
|
||||
def contract(self, *rngs:UOp):
|
||||
assert all(x.arg[-1] == AxisType.UPCAST for x in rngs), "all contract ranges must be upcast"
|
||||
return UOp.stack(*[self.substitute(dict(zip(rngs, [r.const_like(i) for r,i in zip(rngs, idx)])))
|
||||
for idx in itertools.product(*[range(int(r.vmax)+1) for r in rngs])])
|
||||
def alu(self, op, *src:UOp, **kwargs):
|
||||
all_srcs = (self, *src)
|
||||
# broadcast shaped operands to a common shape (None and () are falsy, so only real shapes participate)
|
||||
if (shapes := [s for x in all_srcs if (s:=x._shape)]) and not all_same(shapes):
|
||||
out_shape = _broadcast_shape(*shapes)
|
||||
all_srcs = tuple(x._broadcast_to(out_shape) if x._shape else x for x in all_srcs)
|
||||
return UOp(op, src=all_srcs, **kwargs)
|
||||
def alu(self, op, *src:UOp, **kwargs): return UOp(op, src=(self, *src), **kwargs)
|
||||
@staticmethod
|
||||
def const(dtype:DType, b:ConstLike, shape:tuple[sint, ...]|None=None):
|
||||
if isinstance(b, UOp): return b.cast(dtype)
|
||||
@@ -602,6 +595,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
|
||||
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
|
||||
@staticmethod
|
||||
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, dtypes.void, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.LOOP)+arg)
|
||||
@staticmethod
|
||||
def special(end:sint, name:str, dtype=dtypes.weakint): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
|
||||
@staticmethod
|
||||
def wmma(a:UOp, b:UOp, acc:UOp, dims:tuple[int, int, int], device:str, threads:int, tc_upcast_axes=None):
|
||||
@@ -667,10 +662,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
|
||||
return in_tuple.src[self.arg].axis if in_tuple.op is Ops.TUPLE else None
|
||||
if self.op is Ops.PARAM: return self.arg.axis
|
||||
# NOTE: they all have to share an axis, we always choose [-1]
|
||||
if self.op in GroupOp.ALU: return axes[-1] if (axes := dedup([x.axis for x in self.src if x.axis is not None])) else None
|
||||
# STACK adds a leading axis
|
||||
if self.op is Ops.STACK: return axes[-1]+1 if (axes := dedup([x.axis for x in self.src if x.axis is not None])) else None
|
||||
# NOTE: they all have to share an axis, we always choose [-1]. src axes are right-aligned into the output shape
|
||||
if self.op in GroupOp.ALU.union({Ops.STACK}):
|
||||
return axes[-1] if (axes := dedup([x.axis+len(self.shape)-len(x.shape) for x in self.src if x.axis is not None])) else None
|
||||
if len(self.src) == 0: return None
|
||||
src_axis = self.src[0].axis
|
||||
if self.op is Ops.SHRINK and src_axis is not None and self.marg[src_axis] != (0, self.src[0].shape[src_axis]):
|
||||
@@ -1031,7 +1025,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.WHERE and dtypes.is_int(self.dtype): return min(self.src[1].vmin, self.src[2].vmin), max(self.src[1].vmax, self.src[2].vmax)
|
||||
# NOTE: returned UOp is assumed to be CONST
|
||||
if self.op is Ops.PARAM and self.arg.vmin_vmax is not None: return self.arg.vmin_vmax
|
||||
if self.op in (Ops.RANGE, Ops.SPECIAL): return 0, (self.src[0]-1).vmax
|
||||
if self.op in (Ops.RANGE, Ops.SPECIAL) and self.dtype is not dtypes.void: return 0, (self.src[0]-1).vmax
|
||||
if self.op is Ops.BIND: return self.src[0]._min_max # ignore the bound value
|
||||
if self.op is Ops.STACK: return min(x.vmin for x in self.src), max(x.vmax for x in self.src)
|
||||
if self.op is Ops.CONST and self.arg is not Invalid: return self.arg, self.arg
|
||||
|
||||
@@ -34,6 +34,7 @@ def strip_binary_parens(x:UOp, left:str, right:str, code_for_op) -> str:
|
||||
renderer = PatternMatcher([
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: x.arg.name if x.arg.name is not None else f"p{x.arg.slot}"),
|
||||
(UPat((Ops.SPECIAL), name="x"), lambda x: x.arg),
|
||||
(UPat(Ops.RANGE, dtypes.void, name="x"), lambda x: f"loop{x.arg[0]}"),
|
||||
(UPat(Ops.RANGE, name="x"), lambda x: f"r{range_str(x)}"),
|
||||
(UPat(Ops.CONST, name="x"), lambda x: str(x.arg)),
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({str(x.dtype)[7:]})({ctx[x.src[0]]})"),
|
||||
|
||||
@@ -74,12 +74,15 @@ spec_shared = PatternMatcher([
|
||||
# CAST
|
||||
(UPat((Ops.BITCAST, Ops.CAST), src=(UPat(),), name="x"), lambda x: isinstance(x.arg, DType)),
|
||||
|
||||
# RANGE can be in the big graph now
|
||||
# RANGE can be in the big graph now. a void RANGE is a bound-less loop header, the arg is an axis id like RANGE
|
||||
(UPat(Ops.RANGE, src=(UPat.var("x"),), allow_any_len=True, name="rng"), lambda rng,x:
|
||||
rng.dtype == x.dtype and isinstance(rng.arg, tuple) and len(rng.arg) >= 2 and \
|
||||
all(isinstance(ra, int) for ra in rng.arg[0:-1]) and isinstance(rng.arg[-1], AxisType)),
|
||||
(UPat(Ops.INDEX, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(dtypes.is_int(y.dtype) for y in x.src[1:]) or None),
|
||||
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:])),
|
||||
# END closes RANGEs
|
||||
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:]) or None),
|
||||
# a loop-ended END requires a trailing bool condition for the backedge (loop again while true)
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void), UPat(dtype=dtypes.bool))), lambda: True),
|
||||
|
||||
# PARAM
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
@@ -103,9 +106,6 @@ spec_shared = PatternMatcher([
|
||||
# BARRIER (on any length). TODO: this should only be in spec_program
|
||||
(UPat(Ops.BARRIER, dtypes.void), lambda: True),
|
||||
|
||||
# WAIT until a condition evaluates to true.
|
||||
(UPat(Ops.WAIT, dtypes.void, src=(UPat(dtype=dtypes.bool),)), lambda: True),
|
||||
|
||||
# assembly instruction
|
||||
(UPat(Ops.INS), lambda: True),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user