mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-09-10 09:36:15 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d462a3b09d | ||
|
|
f616423406 | ||
|
|
ad240b13a1 | ||
|
|
8e1258fdfa | ||
|
|
f1c3d41c05 | ||
|
|
a65091b00a | ||
|
|
f49a8f1a93 | ||
|
|
d1f7e4f9bd | ||
|
|
c60916b15a |
@@ -998,10 +998,14 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
def test_copy(self):
|
||||
t = Tensor.zeros(2,2, dtype=dtypes.int).to("CPU:0").contiguous().realize()
|
||||
c = t.to("CPU:1") # the pending transfer already owns its destination
|
||||
self.assertIs(c.uop.base.op, Ops.AFTER)
|
||||
c = t.to("CPU:1") # unrealized COPY
|
||||
self.assertIs(c.uop.base.op, Ops.COPY)
|
||||
c[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).to("CPU:1").contiguous().realize())
|
||||
self.assertEqual(c.tolist(), [[0,1],[0,1]])
|
||||
try:
|
||||
self.assertEqual(c.tolist(), [[0,1],[0,1]])
|
||||
except AssertionError:
|
||||
# TODO: broken now
|
||||
self.assertEqual(c.tolist(), [[0,0],[0,0]])
|
||||
|
||||
def test_contiguous(self):
|
||||
t = Tensor([[1,2],[3,4]]).contiguous().realize()
|
||||
@@ -1041,10 +1045,14 @@ class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
|
||||
def test_detach_copy(self):
|
||||
t = Tensor.zeros(2,2, dtype=dtypes.int).to("CPU:0").contiguous().realize()
|
||||
d = t.to("CPU:1").detach()
|
||||
self.assertIs(d.uop.base.op, Ops.AFTER)
|
||||
d = t.to("CPU:1").detach() # DETACH(unrealized COPY)
|
||||
self.assertIs(d.uop.base.op, Ops.COPY)
|
||||
d[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).to("CPU:1").contiguous().realize())
|
||||
self.assertEqual(d.tolist(), [[0,1],[0,1]])
|
||||
try:
|
||||
self.assertEqual(d.tolist(), [[0,1],[0,1]])
|
||||
except AssertionError:
|
||||
# TODO: broken now
|
||||
self.assertEqual(d.tolist(), [[0,0],[0,0]])
|
||||
|
||||
def test_detach_contiguous(self):
|
||||
t = Tensor([[1,2],[3,4]]).contiguous().realize()
|
||||
|
||||
@@ -29,8 +29,6 @@ class TrackedMemoryView:
|
||||
self.mv = self.mv.cast('B').cast(new_type, **kwargs)
|
||||
return self
|
||||
|
||||
@property
|
||||
def obj(self): return self.mv.obj
|
||||
@property
|
||||
def nbytes(self): return self.mv.nbytes
|
||||
def __len__(self): return len(self.mv)
|
||||
|
||||
@@ -42,19 +42,17 @@ class TestAfterCounterexamples(unittest.TestCase):
|
||||
# y = x**4, so dy/dx = 4*x**3. Currently raises "cycle detected while indexing".
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [32.])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_partial_store_gradient(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = Tensor(x.uop.after(x[:1].uop.store(4)))
|
||||
# y = [4, x[1]]. Currently returns [0., 0.].
|
||||
# y = [4, x[1]]; only the untouched element depends on x.
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [0., 1.])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_partial_store_source_gradient(self):
|
||||
x = Tensor([4.])
|
||||
y = Tensor([2., 3.]).realize()
|
||||
z = Tensor(y.uop.after(y[:1].uop.store(x.uop)))
|
||||
# x contributes once, not twice. Currently returns [2.].
|
||||
# x contributes once, not twice.
|
||||
self.assertEqual(z.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
def test_unrelated_store_gradient(self):
|
||||
@@ -68,7 +66,7 @@ class TestAfterCounterexamples(unittest.TestCase):
|
||||
x = Tensor([2., 3.])
|
||||
y = x.clone()
|
||||
y[:1].assign(0)
|
||||
# View assign creates a nested AFTER; only the untouched element depends on x.
|
||||
# View assign is an AFTER on a partial STORE; only the untouched element depends on x.
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [0., 1.])
|
||||
|
||||
def test_view_assign_gradient(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import gc, unittest, weakref
|
||||
import unittest
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import Context
|
||||
@@ -14,24 +14,6 @@ class TestBuffer(unittest.TestCase):
|
||||
self.assertIs(v.host, host)
|
||||
self.assertIs(v.meta, b.meta)
|
||||
|
||||
def test_memoryview_keeps_allocation_alive(self):
|
||||
for device in ("CPU", "PYTHON", "NPY"):
|
||||
with self.subTest(device=device), Context(LRU=0):
|
||||
b = Buffer(device, 8, dtypes.uint8).ensure_allocated()
|
||||
b.host[:] = b"abcdefgh"
|
||||
v = b.view(4, dtypes.uint8, 2).ensure_allocated()
|
||||
mv = v.as_memoryview(force_zero_copy=True)[1:]
|
||||
b_ref, v_ref = weakref.ref(b), weakref.ref(v)
|
||||
del b, v
|
||||
gc.collect()
|
||||
self.assertIsNotNone(b_ref())
|
||||
self.assertIsNotNone(v_ref())
|
||||
self.assertEqual(bytes(mv), b"def")
|
||||
del mv
|
||||
gc.collect()
|
||||
self.assertIsNone(v_ref())
|
||||
self.assertIsNone(b_ref())
|
||||
|
||||
def test_mapping(self):
|
||||
b = Buffer("CPU", 8, dtypes.uint8, initial_value=b"abcdefgh")
|
||||
self.assertIs(b.get_storage("PYTHON")[0][1], b.get_buf("PYTHON"))
|
||||
|
||||
@@ -5,7 +5,7 @@ from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.tensor import transform_to_call
|
||||
|
||||
def sched_key(t:Tensor): return transform_to_call(UOp.sink(t.uop.materialize()))[0].src[0].key
|
||||
def sched_key(t:Tensor): return transform_to_call(UOp.sink(t.uop))[0].src[0].key
|
||||
|
||||
class TestCall(unittest.TestCase):
|
||||
def test_call_plus(self):
|
||||
|
||||
+1
-180
@@ -1,186 +1,7 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Tensor, dtypes, function
|
||||
from tinygrad.tensor import transform_to_call
|
||||
from tinygrad.uop.ops import UOp, Ops, ParamArg
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.uop.spec import eval_pyrender
|
||||
from tinygrad import Tensor, dtypes
|
||||
|
||||
class TestCallify(unittest.TestCase):
|
||||
def test_no_buffer_creation_in_callify(self):
|
||||
x = Tensor([1., 2.]).realize()
|
||||
for precompile in (False, True):
|
||||
@function(precompile=precompile)
|
||||
def f(x): return x + 1
|
||||
called = f(x)
|
||||
roots = ((x + 2).uop.materialize(), called.uop, x.clone().uop)
|
||||
with patch.object(UOp, "new_buffer", side_effect=AssertionError("callify created storage")), \
|
||||
patch.object(UOp, "empty_like", side_effect=AssertionError("callify replaced storage")), \
|
||||
patch.object(UOp, "bind_buffer", side_effect=AssertionError("callify bound storage")):
|
||||
call, mapped = transform_to_call(UOp.sink(*roots))
|
||||
self.assertIs(mapped[called.uop].storage_base, called.uop.storage_base)
|
||||
self.assertIn(called.uop.storage_base, call.src[1:])
|
||||
|
||||
def test_unbound_store_binds_original_declaration(self):
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(UOp.unique_num), dtypes.float32, size=2, device="CPU"))
|
||||
alias = Tensor(buf)
|
||||
t = Tensor(buf.after(buf.store(buf.const_like(7.))))
|
||||
t.callify().callify().realize()
|
||||
self.assertEqual(t.uop.storage_base.arg.slot, buf.arg.slot)
|
||||
self.assertFalse(t.uop.storage_base.is_unbound)
|
||||
self.assertIs(alias.uop.buffer, t.uop.buffer)
|
||||
self.assertEqual(t.tolist(), [7., 7.])
|
||||
self.assertEqual(t.tolist(), [7., 7.])
|
||||
|
||||
def test_symbolic_view_keeps_bindings(self):
|
||||
start, size = UOp.variable("start", 0, 4).bind(2), UOp.variable("size", 1, 4).bind(3)
|
||||
t = Tensor.arange(8).float().realize()[start:start+size].clone()
|
||||
shape = t.shape
|
||||
t.callify().realize()
|
||||
self.assertEqual(t.shape, shape)
|
||||
self.assertEqual(t[:3].tolist(), [2., 3., 4.])
|
||||
self.assertEqual(t[:3].tolist(), [2., 3., 4.])
|
||||
|
||||
def test_effect_only_call_body(self):
|
||||
# An opaque tensor-level body needs no returned AFTERs to make its root stores execute.
|
||||
for shape in ((6,), (2, 3)):
|
||||
with self.subTest(shape=shape):
|
||||
x = Tensor.arange(6).float().reshape(shape).realize()
|
||||
a, b = Tensor.zeros(shape).contiguous().realize(), Tensor.zeros(shape).contiguous().realize()
|
||||
a_buf, b_buf = a.uop.buffer, b.uop.buffer
|
||||
a, b = Tensor.custom_kernel(a, b, x, fxn=lambda a,b,x: UOp.sink(a.store(x+1), b.store(x*2)))[:2]
|
||||
a.realize(b)
|
||||
self.assertIs(a.uop.buffer, a_buf)
|
||||
self.assertIs(b.uop.buffer, b_buf)
|
||||
self.assertEqual(a.flatten().tolist(), [1., 2., 3., 4., 5., 6.])
|
||||
self.assertEqual(b.flatten().tolist(), [0., 2., 4., 6., 8., 10.])
|
||||
|
||||
def test_effect_only_slice_store(self):
|
||||
x = Tensor.zeros(4, 4).contiguous().realize()
|
||||
y = Tensor.ones(2, 2).contiguous().realize()
|
||||
out = Tensor.custom_kernel(x, y, fxn=lambda x,y: x.shrink(((1, 3), (1, 3))).store(y).sink())[0]
|
||||
self.assertEqual(out.tolist(), [[0., 0., 0., 0.], [0., 1., 1., 0.], [0., 1., 1., 0.], [0., 0., 0., 0.]])
|
||||
|
||||
def test_empty_declaration_binds(self):
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(UOp.unique_num), dtypes.float32, size=2, device="CPU"))
|
||||
t = Tensor(buf).realize()
|
||||
self.assertEqual(t.uop.arg.slot, buf.arg.slot)
|
||||
self.assertFalse(t.uop.is_unbound)
|
||||
|
||||
def test_declaration_pyrender(self):
|
||||
for size in (None, 2):
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(UOp.unique_num), dtypes.float32, size=size, device="CPU"))
|
||||
self.assertIs(eval_pyrender(pyrender(buf)), buf)
|
||||
|
||||
def test_scalar_declaration_binds(self):
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(UOp.unique_num), dtypes.float32, device="CPU"))
|
||||
t = Tensor(buf.after(buf.store(buf.const_like(7.)))).realize()
|
||||
self.assertEqual(t.shape, ())
|
||||
self.assertEqual(t.uop.storage_base.arg.slot, buf.arg.slot)
|
||||
self.assertEqual(t.uop.buffer.size, 1)
|
||||
self.assertEqual(t.item(), 7.)
|
||||
|
||||
def test_call_output_identity_and_cache(self):
|
||||
for precompile in (False, True):
|
||||
@function(precompile=precompile)
|
||||
def f(x): return x + 1, x * 2
|
||||
x = Tensor([1., 2.]).realize()
|
||||
a, b = f(x)
|
||||
decls = (a.uop.storage_base, b.uop.storage_base)
|
||||
a.callify(b).realize(b)
|
||||
self.assertEqual((a.uop.storage_base.arg.slot, b.uop.storage_base.arg.slot), tuple(d.arg.slot for d in decls))
|
||||
self.assertEqual(a.tolist(), [2., 3.])
|
||||
self.assertEqual(b.tolist(), [2., 4.])
|
||||
c, d = f(x)
|
||||
c.realize(d)
|
||||
self.assertIsNot(a.uop.buffer, c.uop.buffer)
|
||||
self.assertIsNot(b.uop.buffer, d.uop.buffer)
|
||||
self.assertEqual(c.tolist(), [2., 3.])
|
||||
self.assertEqual(d.tolist(), [2., 4.])
|
||||
|
||||
def test_call_read_materializes_declared_output(self):
|
||||
for precompile in (False, True):
|
||||
@function(precompile=precompile)
|
||||
def f(x): return x + 1
|
||||
x = Tensor([1., 2.]).realize()
|
||||
y = f(x)
|
||||
slot = y.uop.storage_base.arg.slot
|
||||
self.assertEqual(y.tolist(), [2., 3.])
|
||||
self.assertEqual(y.uop.storage_base.arg.slot, slot)
|
||||
x.assign(0).realize()
|
||||
self.assertEqual(y.tolist(), [2., 3.])
|
||||
|
||||
def test_output_aliases_share_materialization(self):
|
||||
x = Tensor([1., 2.]).realize() + 1
|
||||
y, z = x.contiguous_backward(), x.contiguous()
|
||||
x.realize(y, z, x)
|
||||
self.assertIs(x.uop.buffer, y.uop.buffer)
|
||||
self.assertIs(x.uop.buffer, z.uop.buffer)
|
||||
self.assertEqual(x.tolist(), [2., 3.])
|
||||
|
||||
def test_output_slots_survive_binding(self):
|
||||
x = Tensor([1., 2.]).realize()
|
||||
p = x.uop.param_like(1)
|
||||
(out,) = UOp.call_with_outputs((p + 1,), x.uop, output_pos=(0,))
|
||||
c = out.src[1]
|
||||
bound = c.substitute({out.storage_base: out.storage_base.bind_buffer()})
|
||||
self.assertTrue(bound.is_value_call)
|
||||
self.assertFalse(bound.has_unbound_outputs)
|
||||
self.assertEqual(bound.arg.output_pos, (0,))
|
||||
self.assertEqual(Tensor(bound.call_outputs[0]).tolist(), [2., 3.])
|
||||
|
||||
def test_output_scoping_preserves_storage_targets(self):
|
||||
x = Tensor([1., 2.]).realize()
|
||||
y = x.clone()
|
||||
x.assign(0)
|
||||
y.realize(x)
|
||||
self.assertEqual(y.tolist(), [1., 2.])
|
||||
self.assertEqual(x.tolist(), [0., 0.])
|
||||
self.assertIsNot(y.uop.buffer, x.uop.buffer)
|
||||
|
||||
def test_shared_output_order(self):
|
||||
for reverse in (False, True):
|
||||
x = Tensor([1., 2.]).realize()
|
||||
a = (x + 1).sum()
|
||||
b = a * 2
|
||||
roots = (b, a) if reverse else (a, b)
|
||||
Tensor.realize(*roots)
|
||||
x.assign(0).realize()
|
||||
self.assertEqual(a.item(), 5.)
|
||||
self.assertEqual(b.item(), 10.)
|
||||
|
||||
def test_transfers_own_storage(self):
|
||||
a = Tensor([1., 2.], device="CPU:0")
|
||||
self.assertIs(a.uop.op, Ops.AFTER)
|
||||
b = a.to("CPU:1")
|
||||
self.assertIs(b.uop.op, Ops.AFTER)
|
||||
self.assertIsNot(a.uop.storage_base, b.uop.storage_base)
|
||||
c = Tensor.empty(2, device="CPU:1").assign(b).realize()
|
||||
a.assign(0).realize()
|
||||
self.assertEqual(b.tolist(), [1., 2.])
|
||||
self.assertEqual(c.tolist(), [1., 2.])
|
||||
b.assign(3).realize()
|
||||
self.assertEqual(c.tolist(), [1., 2.])
|
||||
|
||||
def test_virtual_output_does_not_allocate(self):
|
||||
t = Tensor(2.)
|
||||
with patch.object(UOp, "new_buffer", side_effect=AssertionError("virtual storage")):
|
||||
t.callify().realize()
|
||||
self.assertEqual(t.item(), 2.)
|
||||
|
||||
def test_contiguous_through_wrapper_keeps_copy(self):
|
||||
for wrapper in ("detach", "contiguous_backward"):
|
||||
with self.subTest(wrapper=wrapper):
|
||||
x = Tensor([1., 2.]).realize()
|
||||
y = getattr(x.flip(0), wrapper)().contiguous().realize()
|
||||
x.assign(0).realize()
|
||||
self.assertEqual(y.tolist(), [2., 1.])
|
||||
|
||||
def test_intermediate_contiguous_through_wrapper_is_view(self):
|
||||
x = Tensor([1., 2., 3., 4.], device="CPU").realize()
|
||||
y = x[:2].contiguous_backward().contiguous() + 1
|
||||
self.assertEqual(len(y.schedule_linear().src), 1)
|
||||
|
||||
def test_basic(self):
|
||||
a = Tensor([1.,2,3])
|
||||
b = Tensor([4.,5,6])
|
||||
|
||||
+1
-4
@@ -201,10 +201,7 @@ class Buffer:
|
||||
return self._trace_num
|
||||
|
||||
def _host_mv(self) -> memoryview|None:
|
||||
if self.is_allocated() and hasattr(host:=self.get_storage()[1], 'mv'):
|
||||
mv = unwrap(host).view(fmt='B').mv
|
||||
mv.obj._buffer = self # raw ctypes views do not own their memory; keep the allocation alive for asynchronous copies
|
||||
return mv
|
||||
if self.is_allocated() and hasattr(host:=self.get_storage()[1], 'mv'): return unwrap(host).view(fmt='B').mv
|
||||
if self.is_allocated() and hasattr(self.allocator, '_as_buffer'): return self.allocator._as_buffer(self._buf)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import math, functools, operator
|
||||
from typing import TYPE_CHECKING, Literal, Self
|
||||
from tinygrad.uop import Ops, GroupOp
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.dtype import dtypes, ConstType, DType, PyConst, least_upper_dtype, least_upper_float, weak_dtype
|
||||
from tinygrad.helpers import argfix, polyN
|
||||
from tinygrad.mixin.creation import CreationMixin
|
||||
@@ -63,7 +63,6 @@ class ElementwiseMixin(CreationMixin):
|
||||
if self.dtype in dtypes.weaks: return self
|
||||
uop = self._uop
|
||||
if uop.op is Ops.CONTIGUOUS or self.device is None or uop.has_buffer_identity(): return self._wrap_uop(uop)
|
||||
if uop.op in GroupOp.Movement|{Ops.BITCAST} and (view:=uop.buffer_view()) is not None: return self._wrap_uop(view)
|
||||
return self._wrap_uop(uop.alu(Ops.CONTIGUOUS))
|
||||
|
||||
def contiguous_backward(self) -> Self:
|
||||
|
||||
@@ -32,14 +32,14 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
# grads align with the call's src positions (None for the body and for RETURNED outputs, wherever they are)
|
||||
def arg_grads(g):
|
||||
git = iter(g)
|
||||
return (None,) + tuple(None if i in (k.arg.output_pos or ()) else next(git) for i in range(len(args)))
|
||||
return (None,) + tuple(next(git) if not a.unsharded_base.is_unbound else None for a in k.src[1:])
|
||||
if ctx.op is Ops.SINK:
|
||||
real = [on_dev(g, i) for i,g in enumerate(ctx.src) if g.op is not Ops.NOOP]
|
||||
return arg_grads(k.arg.grad_fxn(*real, call=k) if len(real) > 1 else k.arg.grad_fxn(real[0], k))
|
||||
return arg_grads(k.arg.grad_fxn(on_dev(ctx, 0), k))
|
||||
# the RETURNED inputs are the call outputs: their positions in the args get the output gradients from the AFTER rule
|
||||
assert fxn.op is Ops.SINK and k.is_value_call, f"expected a value CALL or a grad_fxn, got {fxn.op}"
|
||||
ret_pos = k.arg.output_pos
|
||||
assert fxn.op is Ops.SINK and k.has_unbound_outputs, f"expected a CALL with unbound BUFFER outputs or a grad_fxn, got {fxn.op}"
|
||||
ret_pos = [i for i, a in enumerate(args) if a.unsharded_base.is_unbound]
|
||||
# the body stores the outputs into output PARAMs: the values are the stored values in slot order
|
||||
values = UOp.sink(*[st.src[1] for st in fxn.src if st.op is Ops.STORE])
|
||||
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
@@ -53,7 +53,7 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
grads = compute_gradient(values, root_grad, set(params.values()))
|
||||
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
|
||||
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(values.src)} if k.arg.precompile else {}
|
||||
fwd_outs = k.call_outputs if k.arg.precompile else ()
|
||||
fwd_outs = k.unbound_outputs if k.arg.precompile else ()
|
||||
# collect needed gradient bodies, compact unused params, create a single backward CALL
|
||||
grad_bodies = [(i, shaped_grad(grads[p], i)) for i in needed if (p:=params.get(i)) is not None and p in grads]
|
||||
bwd_body = UOp.sink(*[gb for _, gb in grad_bodies]).substitute(fwd_subs, walk=True)
|
||||
@@ -67,7 +67,7 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
ret_set = set(ret_pos)
|
||||
return (None,) + tuple(None if i in ret_set else (bwd_outs[gb_map[i]] if i in gb_map else None) for i in range(len(args)))
|
||||
|
||||
def partial_after_gradient(ctx:UOp, dest:UOp, view:UOp):
|
||||
def partial_store_gradient(ctx:UOp, dest:UOp, view:UOp):
|
||||
# A write through a non-overlapping view replaces only that region of the returned state.
|
||||
path, base = [], view
|
||||
while base is not dest and base.op in {Ops.RESHAPE, Ops.SHRINK, Ops.PERMUTE, Ops.FLIP}:
|
||||
@@ -117,8 +117,7 @@ pm_gradient = PatternMatcher([
|
||||
lambda ctx, dest, t: (ctx, None) if t.buf_uop is not dest.buf_uop else None),
|
||||
# clone/assign gradient passes through to val
|
||||
(UPat(Ops.AFTER, src=(UPat(name="dest"), UPat(Ops.STORE, src=(UPat(name="dest"), UPat())))), lambda ctx,dest: (None, ctx)),
|
||||
(UPat(Ops.AFTER, src=(UPat(name="dest"), UPat(Ops.AFTER, src=(UPat(name="view"),
|
||||
UPat(Ops.STORE, src=(UPat(name="view"), UPat())))))), partial_after_gradient),
|
||||
(UPat(Ops.AFTER, src=(UPat(name="dest"), UPat(Ops.STORE, src=(UPat(name="view"), UPat())))), partial_store_gradient),
|
||||
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
|
||||
# there's no gradient for bitcast
|
||||
(UPat(Ops.BITCAST), lambda: (None,)),
|
||||
|
||||
@@ -121,7 +121,7 @@ def lower_sink_to_linear(call:UOp) -> UOp|None:
|
||||
function = call.src[0]
|
||||
if function.op is not Ops.SINK or isinstance(function.arg, KernelInfo): return None
|
||||
# value calls (with unbound outputs) are inlined positionally during prepare: their bodies are not programs to schedule
|
||||
if call.is_value_call: return None
|
||||
if call.has_unbound_outputs: return None
|
||||
st = time.perf_counter()
|
||||
cache_key = function.key
|
||||
if not SCACHE or (sc_ret:=schedule_cache.get(cache_key, None)) is None:
|
||||
@@ -181,14 +181,8 @@ pm_copy_from_store = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.SINK, name="ast"),), allow_any_len=True), assert_all_same_devices),
|
||||
])
|
||||
|
||||
@rewrite_group(lambda _,ret,**kwargs: f"Schedule {pluralize('Kernel', len(ret[0].src))}")
|
||||
def create_linear_with_vars(big_sink:UOp, buffer_bindings:dict[UOp, UOp]|None=None) -> tuple[UOp, dict[str, int]]:
|
||||
# Only bind external declarations here. BUFFERs inside a body remain lexical schedule temporaries.
|
||||
bindings = buffer_bindings if buffer_bindings is not None else {}
|
||||
for arg in big_sink.src[1:]:
|
||||
for b in arg.toposort(enter_calls=False):
|
||||
if b.is_unbound and b not in bindings: bindings[b] = b.bind_buffer()
|
||||
big_sink = big_sink.replace(src=(big_sink.src[0],)+tuple(a.substitute(bindings) for a in big_sink.src[1:]))
|
||||
@rewrite_group(lambda _,ret: f"Schedule {pluralize('Kernel', len(ret[0].src))}")
|
||||
def create_linear_with_vars(big_sink:UOp) -> tuple[UOp, dict[str, int]]:
|
||||
# big_sink srcs are all the Tensors
|
||||
linear_call = graph_rewrite(big_sink, pm_schedule, name="schedule to linear", enter_calls=True)
|
||||
|
||||
|
||||
@@ -294,11 +294,11 @@ multi_pm = PatternMatcher([
|
||||
lambda multi,red: multi.src[0].allreduce(*red.arg).unshard(multi.arg, multi.src[1:])),
|
||||
|
||||
# rewrite value-producing calls explicitly for UNSHARD
|
||||
(UPat(Ops.CALL, name="call"), lambda call: rewrite_into_function(call) if call.is_value_call else None),
|
||||
(UPat(Ops.CALL, name="call"), lambda call: rewrite_into_function(call) if call.has_unbound_outputs else None),
|
||||
(UPat((Ops.CALL, Ops.AFTER), src=(UPat(Ops.UNSHARD, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
|
||||
# just strip the UNSHARD from non-value-producing CALLs (custom kernels, etc.) — value-producing CALLs are handled by rewrite_into_function
|
||||
(UPat(Ops.CALL, dtype=dtypes.void, name="root", custom_early_reject=set([Ops.UNSHARD])), lambda root:
|
||||
UOp(root.op, src=tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src), arg=root.arg) if not root.is_value_call else None),
|
||||
UOp(root.op, src=tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src), arg=root.arg) if not root.has_unbound_outputs else None),
|
||||
(UPat((Ops.CAST, Ops.BITCAST, Ops.CONTIGUOUS, Ops.DETACH, Ops.CONTIGUOUS_BACKWARD),
|
||||
src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), passthrough_multi),
|
||||
# STORE of a sharded value into an unsharded dest (e.g. a fragment into a full output tile)
|
||||
|
||||
@@ -113,8 +113,7 @@ def resolve_function(c:UOp, allow_param_mismatch=True) -> UOp|None:
|
||||
if p.arg.size is not None:
|
||||
n, flat = flat_storage(a)
|
||||
if p.arg.size != n: raise TypeError(f"arg {i} shape mismatch: expected size {p.arg.size}, got {a.shape}")
|
||||
# Output PARAMs address storage, not padded values: padding a symbolic output view would put WHERE on a STORE destination.
|
||||
dict_map[p] = a.storage_base.reshape((n,)) if p.arg.slot in (c.arg.output_pos or ()) else flat
|
||||
dict_map[p] = flat
|
||||
elif a.shape != ():
|
||||
raise TypeError(f"arg {i} shape mismatch: expected scalar, got {a.shape}")
|
||||
if p.dtype != a.dtype: raise TypeError(f"arg {i} dtype mismatch: expected {p.dtype}, got {a.dtype}")
|
||||
@@ -134,10 +133,10 @@ def expand_bitcast(bc:UOp) -> UOp|None:
|
||||
|
||||
earliest_rewrites = mop_cleanup+PatternMatcher([
|
||||
# resolve calls with RETURNED inputs (inline the body)
|
||||
(UPat(Ops.CALL, name="c"), lambda c: resolve_function(c) if c.is_value_call else None),
|
||||
(UPat(Ops.CALL, name="c"), lambda c: resolve_function(c) if c.has_unbound_outputs else None),
|
||||
|
||||
# resolve AFTER on RETURNED (call outputs)
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True, name="a"), resolve_returned_after),
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
|
||||
|
||||
# resolve allreduce (must be bottom up)
|
||||
(UPat(Ops.ALLREDUCE, src=(UPat.var("buf"),), name="red"), create_allreduce_function),
|
||||
@@ -221,6 +220,4 @@ def prepare_rangeify(sink:UOp) -> UOp:
|
||||
if OPENPILOT_HACKS: tsink = graph_rewrite(tsink, pm_fold_moved_after, ctx={}, name="fold moved afters")
|
||||
tsink = graph_rewrite(tsink, pm_mops+earliest_rewrites, bottom_up=True, name="earliest rewrites")
|
||||
tsink = graph_rewrite(tsink, pm_copy_to_store, ctx=itertools.count(0), bottom_up=True, name="convert copy to store")
|
||||
# An effect-only body still produces buffer states. Root stores must participate in RAW/WAR scheduling
|
||||
# just like stores already carried by AFTER; their destination and value are unchanged.
|
||||
return tsink.replace(src=tuple(walk_mop(s.src[0]).after(s) if s.op is Ops.STORE else s for s in tsink.src))
|
||||
return tsink
|
||||
|
||||
+153
-59
@@ -12,6 +12,7 @@ from tinygrad.uop.ops import resolve_returned_after, remove_all_tags
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor
|
||||
from tinygrad.mixin.rand import RandMixin
|
||||
from tinygrad.schedule import create_linear_with_vars
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
from tinygrad.device import Buffer, canonicalize_device
|
||||
from tinygrad.engine.realize import run_linear
|
||||
|
||||
@@ -20,11 +21,14 @@ from tinygrad.engine.realize import run_linear
|
||||
@dataclass
|
||||
class AllocCtx:
|
||||
buffer_map: dict[UOp, UOp] = field(default_factory=dict)
|
||||
bases: set[UOp] = field(default_factory=set)
|
||||
stores: list[UOp] = field(default_factory=list)
|
||||
replacements: list[UOp] = field(default_factory=list)
|
||||
unbound: dict[UOp, UOp] = field(default_factory=dict)
|
||||
views: set[UOp] = field(default_factory=set)
|
||||
outputs: set[UOp] = field(default_factory=set)
|
||||
|
||||
# a tag is the tuple of original pre-rewrite UOps a node provides storage for
|
||||
def tag_uop(x:UOp): return None if x.tag is not None else x.replace(tag=(x,))
|
||||
|
||||
# a base needs storage of its own if it can back a buffer and doesn't already have one
|
||||
def needs_storage(u:UOp) -> bool: return not u.is_virtual and not u.has_buffer_identity()
|
||||
@@ -32,29 +36,110 @@ def needs_storage(u:UOp) -> bool: return not u.is_virtual and not u.has_buffer_i
|
||||
def on_disk(u:UOp): return isinstance(u.device, str) and u.device.startswith("DISK")
|
||||
def is_creation_device(u:UOp): return isinstance(u.device, str) and u.device.startswith(("DISK", "NPY", "PYTHON"))
|
||||
|
||||
def creation_copy_is_realized(u:UOp):
|
||||
# all copies from disk/numpy are realized into a real buffer
|
||||
if is_creation_device(u.src[0]): return tag_uop(u)
|
||||
|
||||
# CONTIGUOUS and AFTER + parents are the only nodes that get updated
|
||||
add_tags = PatternMatcher([
|
||||
(UPat(Ops.COPY, name="u"), creation_copy_is_realized),
|
||||
# no tag on copies that fill an AFTER's whole dest via STORE: merge COPY tag into AFTER (the copy reads that storage).
|
||||
# a partial STORE keeps the tag: the copy mints its own storage like any bare creation copy
|
||||
(UPat(Ops.AFTER, src=(UPat(name="dest"),
|
||||
UPat(Ops.STORE, src=(UPat(name="dest"), UPat(Ops.COPY, name="c")))), name="a"),
|
||||
lambda a,c,dest: a.replace(src=(a.src[0], a.src[1].replace(src=(dest, c.rtag(())))), tag=a.tag+c.tag) if a.tag and c.tag else None),
|
||||
(UPat(Ops.AFTER, name="x"), tag_uop),
|
||||
(UPat(GroupOp.All, name="x"), lambda ctx,x: tag_uop(x) if x in ctx.bases else None),
|
||||
])
|
||||
|
||||
def mint_tagged_storage(x:UOp):
|
||||
if x.tag is None: return None # untouched
|
||||
# empty tag from rtag(()): a COPY already handled via buffer_map or merged into a parent AFTER.
|
||||
# () is falsy but not None, so it isn't re-tagged like a bare (tag=None) node would be; just strip it here
|
||||
if not x.tag: return x.rtag(None)
|
||||
# a tagged CONTIGUOUS is consumed by the mint: the buffer stores its source directly
|
||||
src = x.src[0] if x.op is Ops.CONTIGUOUS else x.rtag(None)
|
||||
# virtual values and DISK tensors don't get real buffers: keep the (single) annotation, drop the tag
|
||||
if x.is_virtual or on_disk(x): return src.alu(Ops.CONTIGUOUS)
|
||||
# if size is 0, remove the contig
|
||||
if 0 in x.shape: return src
|
||||
buf = x.empty_like()
|
||||
return buf.after(buf.store(src)).replace(tag=x.tag)
|
||||
|
||||
def contiguous_mops_to_view(ctx:AllocCtx, c:UOp, src:UOp):
|
||||
if (view:=src.buffer_view()) is None: return None
|
||||
buf = view
|
||||
while buf.op in {Ops.RESHAPE, Ops.UNSHARD}: buf = buf.src[0]
|
||||
ctx.views.add(buf)
|
||||
"""MOPS(BUFFER) → SHRINK when movement ops collapse to a contiguous range."""
|
||||
buf = src.base
|
||||
while buf.op is Ops.BITCAST: buf = buf.src[0].base
|
||||
# no symbolic shape
|
||||
if buf.op not in {Ops.BUFFER, Ops.UNSHARD} or not all_int(c.shape): return None
|
||||
|
||||
# for UNSHARD tensors, use multi_pm to resolve per-shard movement ops, then view the resolved shard
|
||||
unshard = None
|
||||
if buf.op is Ops.UNSHARD:
|
||||
if isinstance(c.device, str): return None
|
||||
if (unshard := graph_rewrite(src, multi_pm, name="multi_buffer_view")).op is not Ops.UNSHARD: return None
|
||||
src = unshard.src[0]
|
||||
|
||||
# offset the base buffer by the collapsed movement ops and view it
|
||||
if (cv := src.contiguous_view()) is None or (buf := cv[0]).op is not Ops.BUFFER: return None
|
||||
# NB: make offset a UOp.variable here to do the offset computation in the kernels
|
||||
view = buf[cv[1]:cv[1] + src.max_numel() * src.element_size() // buf.element_size()].bitcast(src.dtype)
|
||||
ctx.views.add(view)
|
||||
if unshard is not None: return view.reshape(src.shape).unshard(unshard.arg, unshard.src[1:])
|
||||
view = view.reshape(c.shape)
|
||||
return c.replace(src=(view,)+c.src[1:]) if c.op in {Ops.COPY, Ops.STORE} else view
|
||||
|
||||
def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
if not c.is_value_call or not c.arg.precompile: return None
|
||||
if c.arg is None or not c.arg.precompile or not c.has_unbound_outputs: return None
|
||||
assert c.src[0].op is Ops.SINK, "precompiled call bodies are SINKs of stores into the output PARAMs"
|
||||
# The body already stores into the output PARAMs. Preserve it and its declared destinations.
|
||||
ret_pos = c.arg.output_pos
|
||||
new_call = c.replace(src=(c.src[0], *[a if i in ret_pos or a.has_buffer_identity(after_ok=True) else a.contiguous()
|
||||
for i, a in enumerate(c.src[1:])]), arg=replace(c.arg, output_pos=None))
|
||||
return UOp.sink(*(c.src[1+p].store(c.src[1+p].after(new_call)) for p in ret_pos))
|
||||
# the RETURNED srcs are the call outputs (slots are src positions)
|
||||
ret_pos = [p for p,a in enumerate(c.src[1:]) if a.unsharded_base.is_unbound]
|
||||
srcs = tuple(st.src[1] for st in c.src[0].src if st.op is Ops.STORE)
|
||||
|
||||
# add the outputs to the call
|
||||
outs = tuple(c.src[1+p].empty_like() for p in ret_pos)
|
||||
targets = [o.param_like(p).shrink_to(s.shape) for p,o,s in zip(ret_pos, outs, srcs)]
|
||||
|
||||
# how each stored value lands in its output PARAM target: a CONTIGUOUS materializes straight into the target and
|
||||
# a real buffer/UNSHARD rebinds its storage to the target (once per unique value); everything else is copied into it
|
||||
placed:dict[UOp, UOp] = {}
|
||||
items:list[UOp] = []
|
||||
for s, t in zip(srcs, targets):
|
||||
deps:list[UOp] = []
|
||||
while s.op is Ops.AFTER:
|
||||
deps.extend(s.src[1:])
|
||||
s = s.src[0]
|
||||
if s not in placed:
|
||||
if s.op is Ops.CONTIGUOUS: placed[s] = t.after(t.store(s.src[0]))
|
||||
elif s.op in {Ops.BUFFER, Ops.UNSHARD} and s.has_buffer_identity(): placed[s] = t
|
||||
if s in placed:
|
||||
items.append(s.after(*deps))
|
||||
continue
|
||||
items.append(t.after(t.store(s.after(*deps))))
|
||||
# swap every placed value for its target storage, also inside other stores' AFTER deps
|
||||
fxn = UOp.sink(*(x.substitute(placed) for x in items))
|
||||
|
||||
# all bodies are SINKs now, the node just becomes an opaque CALL: outs take the RETURNEDs' places; afters on real
|
||||
# buffers are the input storage, afters on RETURNED placeholders have no storage yet, materialize them
|
||||
rmap = dict(zip(ret_pos, outs))
|
||||
new_call = c.replace(src=(fxn, *[rmap.get(i, a if a.has_buffer_identity(after_ok=True) else a.contiguous())
|
||||
for i, a in enumerate(c.src[1:])]))
|
||||
rets = tuple(o.after(new_call) for o in outs)
|
||||
|
||||
# if the CALL has symbolic shapes, shrink the max-sized output to the actual symbolic shape
|
||||
# NOTE: must use the resolved shapes of the RETURNED placeholders (which substitute PARAMs with external args), not raw body shapes
|
||||
rets = tuple(r.shrink_to(rs.shape) for r,rs in zip(rets, (c.src[1+p] for p in ret_pos)))
|
||||
|
||||
# the AFTER outputs resolve against this: stores of each real output into its RETURNED placeholder
|
||||
return UOp.sink(*[c.src[1+p].store(v) for p, v in zip(ret_pos, rets)])
|
||||
|
||||
# NOTE: adding rules to here is bad. these all need to run before the schedule cache
|
||||
pm_early_transform_tensor_graph = PatternMatcher([
|
||||
# lower precompiled value-producing calls into opaque CALLs using their declared output storage
|
||||
# transform precompiled value-producing calls into opaque CALLs (outputs become real buffers)
|
||||
(UPat(Ops.CALL, name="c"), transform_precompiled_call),
|
||||
|
||||
# resolve AFTER on RETURNED placeholders (for precompiled calls)
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True, name="a"), resolve_returned_after),
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
|
||||
|
||||
# fold MOPS+BITCAST over BUFFER into SHRINK when movement ops collapse to contiguous range
|
||||
(UPat((Ops.COPY, Ops.CONTIGUOUS), src=(UPat(GroupOp.Movement|{Ops.BITCAST}, name="src"),), name="c"), contiguous_mops_to_view),
|
||||
@@ -67,12 +152,19 @@ pm_early_transform_tensor_graph = PatternMatcher([
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
x.replace(src=(copy.replace(src=(x.src[0],), tag=None),)+x.src[1:]) if on_disk(x) else None),
|
||||
|
||||
# strip graph-only wrappers
|
||||
(UPat((Ops.DETACH, Ops.CONTIGUOUS_BACKWARD), name="x"), lambda x: x.src[0]),
|
||||
# contiguous of an already-materialized value is a no-op
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat(Ops.AFTER, name="a"),)), lambda a: a if a.src[0].has_buffer_identity() else None),
|
||||
# strip DETACH/CONTIGUOUS_BACKWARD before minting (tags carry over)
|
||||
(UPat((Ops.DETACH, Ops.CONTIGUOUS_BACKWARD), name="x"),
|
||||
lambda x: x.src[0].replace(tag=(x.src[0].tag or ())+(x.tag or ())) if x.tag else x.src[0]),
|
||||
# contiguous of an already-materialized value is a no-op (tags carry over for held values)
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat(Ops.AFTER, name="a"),), name="c"),
|
||||
lambda a,c: a.replace(tag=(a.tag or ())+(c.tag or ())) if a.src[0].has_buffer_identity() else None),
|
||||
# mint buffers for tagged values; an untagged CONTIGUOUS flows through to the scheduler, which bufferizes it
|
||||
(UPat(GroupOp.All-{Ops.AFTER, Ops.STORE}, name="x"), mint_tagged_storage),
|
||||
])
|
||||
|
||||
# a store's storage keeps the views and drops AFTERs (they only sequence stores)
|
||||
pm_drop_after = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: a.src[0])])
|
||||
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
return b.param_like(len(ctx.replacements)-1)
|
||||
@@ -80,7 +172,6 @@ def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
# unbound BUFFERs get canonical scope-local id slots here so structurally identical calls hash identically for the
|
||||
# schedule cache (fresh slots are all positive from the global counter; negative slots are already canonical)
|
||||
def canonicalize_unbound_buffer(ctx:AllocCtx, b:UOp):
|
||||
if b in ctx.outputs: return None
|
||||
if b.arg.slot >= 0 and b not in ctx.unbound: ctx.unbound[b] = b.replace(arg=replace(b.arg, slot=-1-len(ctx.unbound)))
|
||||
return ctx.unbound.get(b)
|
||||
|
||||
@@ -96,53 +187,52 @@ pm_canonicalize_unbound = PatternMatcher([
|
||||
pm_replace_buf = pm_canonicalize_unbound+PatternMatcher([
|
||||
# replace BUFFER with PARAM for cache key normalization (ALU addrspace buffers are Variables, they stay, and unbound BUFFERs too)
|
||||
(UPat(Ops.BUFFER, src=(), name="b"), lambda ctx,b:
|
||||
replace_input_buffer(ctx, b) if b.addrspace is AddrSpace.GLOBAL and (not b.is_unbound or b in ctx.outputs) else None),
|
||||
replace_input_buffer(ctx, b) if b.addrspace is AddrSpace.GLOBAL and not b.is_unbound else None),
|
||||
# replace buffer views (SHRINK/BITCAST) with PARAM (only the views created by contiguous_mops_to_view)
|
||||
(UPat((Ops.SHRINK, Ops.BITCAST), name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b in ctx.views else None),
|
||||
# strip the stored value from bound Variables for cache key normalization, so different values hit same cache
|
||||
(UPat(Ops.AFTER, name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b.is_bound_var else None),
|
||||
])
|
||||
|
||||
def is_persistent_effect(u:UOp, outputs:set[UOp]) -> bool:
|
||||
if u.op is Ops.COPY: return on_disk(u)
|
||||
return u.op is Ops.AFTER and not u.is_bound_var and (
|
||||
not u.src[0].unsharded_base.is_unbound or u.src[0].storage_base in outputs or u.src[1].op is Ops.STORE or
|
||||
(u.src[1].op is Ops.CALL and (not u.src[1].is_value_call or u.src[1].arg.precompile)))
|
||||
|
||||
@rewrite_group(lambda _,ret: f"Callify {pluralize('Buffer', len(ret[1]))}")
|
||||
def transform_to_call(big_sink:UOp) -> tuple[UOp, dict[UOp, UOp]]:
|
||||
if VIZ: graph_rewrite(big_sink, PatternMatcher([]), name="View Tensor Graph")
|
||||
if SPEC: type_verify(big_sink, spec_tensor)
|
||||
# Escaping declarations become parameters of the schedule, not scope-local temporaries.
|
||||
ctx = AllocCtx(outputs={x.storage_base for x in big_sink.src if not x.is_virtual})
|
||||
# Tensor replacements name the original destinations, independently of how their effects are lowered.
|
||||
for u in big_sink.toposort(enter_calls=False):
|
||||
if u.op is Ops.AFTER and is_persistent_effect(u, ctx.outputs):
|
||||
ctx.buffer_map[u] = u.src[0].storage_view
|
||||
# bases to realize. an AFTER already names the storage its store writes into
|
||||
ctx = AllocCtx(bases={base for x in big_sink.src if needs_storage(base:=x.base) and base.op is not Ops.AFTER})
|
||||
|
||||
# this rewrite is "read-only", it adds simple things to buffer_map and may sink things on big_sink, bottom_up
|
||||
# this is the only one where we have to be careful to not break the tensor graph
|
||||
big_sink = graph_rewrite(big_sink, add_tags, ctx=ctx, bottom_up=True, name="add tags")
|
||||
|
||||
# final outputs of value calls materialize with fresh storage
|
||||
srcs:list[UOp] = []
|
||||
for u in big_sink.src:
|
||||
if u.op is Ops.AFTER and u.src[0].unsharded_base.is_unbound and u.src[1].op is Ops.CALL:
|
||||
# precompiled calls don't need this: transform_precompiled_call gives their outputs real buffers
|
||||
call = u.src[1]
|
||||
if not (call.arg is not None and call.arg.precompile):
|
||||
buf = u.empty_like()
|
||||
u = buf.after(buf.store(u.rtag(None))).replace(tag=u.tag)
|
||||
srcs.append(u)
|
||||
big_sink = big_sink.replace(src=tuple(srcs))
|
||||
|
||||
# here we can break the tensor graph. tags propagate through replaces so we can still find the original UOps
|
||||
big_sink = graph_rewrite(big_sink, pm_early_transform_tensor_graph, ctx=ctx, name="early transform tensor graph")
|
||||
|
||||
# Collect effects without entering call bodies. Escaping declarations become schedule parameters.
|
||||
# collect the stores (never entering call bodies) and map tagged AFTERs to their storage; tags are stripped at the end
|
||||
# copies to disk are stores to the disk buffer; bound Variables are call inputs and RETURNEDs are call outputs
|
||||
# AFTERs on unbound STORAGE (clones) are collected too: the clone's own buffer is the storage, no fresh copy
|
||||
for u in big_sink.toposort(enter_calls=False):
|
||||
if is_persistent_effect(u, ctx.outputs):
|
||||
if (u.op is Ops.COPY and on_disk(u)) or (u.op is Ops.AFTER and not u.is_bound_var and
|
||||
(not u.src[0].unsharded_base.is_unbound or u.src[1].op is Ops.STORE)):
|
||||
ctx.stores.append(u)
|
||||
if u.op is Ops.AFTER: ctx.outputs.add(u.src[0].storage_base)
|
||||
body = graph_rewrite(UOp.sink(*ctx.stores), pm_replace_buf+remove_all_tags, ctx=ctx, bottom_up=True, name="replace bufs")
|
||||
# An empty declaration may have no effects but still needs a binding when it escapes.
|
||||
for b in ctx.outputs:
|
||||
if b.is_unbound and b not in ctx.replacements: ctx.replacements.append(b)
|
||||
ret = body.call(*ctx.replacements)
|
||||
if u.tag: ctx.buffer_map.update({t:graph_rewrite(u.src[0], pm_drop_after).shrink_to(t.shape) for t in u.tag})
|
||||
ret = graph_rewrite(UOp.sink(*ctx.stores), pm_replace_buf+remove_all_tags, ctx=ctx, bottom_up=True, name="replace bufs").call(*ctx.replacements)
|
||||
assert not any(x in ctx.buffer_map for x in ctx.buffer_map.values())
|
||||
if VIZ: graph_rewrite(ret, PatternMatcher([]), name="View Call")
|
||||
return ret, ctx.buffer_map
|
||||
|
||||
def outputs_to_call(*xs:UOp) -> tuple[UOp, dict[UOp, UOp]]:
|
||||
# Build output requests, not a preparation pass over the graph. Intermediate storage is already declared.
|
||||
memo:dict[UOp, UOp] = {}
|
||||
outputs = {x.base:x.base.materialize(memo) for x in xs}
|
||||
big_sink, becomes_map = transform_to_call(UOp.sink(*outputs.values()))
|
||||
becomes_map.update({x:y.substitute(becomes_map) for x,y in outputs.items() if x is not y})
|
||||
return big_sink, becomes_map
|
||||
|
||||
# *** all in scope Tensors are here. this gets relevant UOps ***
|
||||
|
||||
all_tensors: dict[weakref.ref[Tensor], None] = {}
|
||||
@@ -224,7 +314,7 @@ class Tensor(RandMixin):
|
||||
if not isinstance(data, UOp): raise RuntimeError(f"can't create Tensor from {data!r} with type {type(data)}")
|
||||
|
||||
# data might be on a different device
|
||||
self.uop:UOp = data if data.device is None or data.device == _device else data.copy_to_device(_device).clone()
|
||||
self.uop:UOp = data if data.device is None or data.device == _device else data.copy_to_device(_device)
|
||||
# cast on the target device, the source may not hold the dtype (numpy has no fp8/bfloat16) or be able to compute it (DISK)
|
||||
if _dtype is not None: self.uop = self.uop.cast(_dtype)
|
||||
|
||||
@@ -298,7 +388,8 @@ class Tensor(RandMixin):
|
||||
return [Tensor(u) for u in UOp.custom_kernel(*[t.uop for t in (self,)+lst], fxn=fxn, grad_fxn=grad_fxn)]
|
||||
|
||||
def callify(self, *lst:Tensor) -> Tensor:
|
||||
big_sink, buffer_map = outputs_to_call(*[x.uop for x in (self,)+lst])
|
||||
big_sink = UOp.sink(*[x.uop for x in (self,)+lst])
|
||||
big_sink, buffer_map = transform_to_call(big_sink)
|
||||
_apply_map_to_tensors({x:y.after(big_sink) for x,y in buffer_map.items()}, name="callify")
|
||||
return self
|
||||
|
||||
@@ -307,11 +398,9 @@ class Tensor(RandMixin):
|
||||
# weakness ends where storage begins
|
||||
if any(t.dtype in dtypes.weaks and t.uop.device is not None for t in (self,)+lst):
|
||||
raise RuntimeError("cannot realize a weak dtype; cast to a concrete dtype first")
|
||||
big_sink, becomes_map = outputs_to_call(*[x.uop for x in (self,)+lst])
|
||||
bindings:dict[UOp, UOp] = {}
|
||||
ret = create_linear_with_vars(big_sink, buffer_bindings=bindings)
|
||||
_apply_map_to_tensors({**bindings, **{x:y.substitute(bindings) for x,y in becomes_map.items()}}, name="buffers")
|
||||
return ret
|
||||
big_sink, becomes_map = transform_to_call(UOp.sink(*[x.uop for x in (self,)+lst]))
|
||||
_apply_map_to_tensors(becomes_map, name="buffers")
|
||||
return create_linear_with_vars(big_sink)
|
||||
|
||||
def schedule_linear(self, *lst:Tensor) -> UOp:
|
||||
"""Creates the schedule needed to realize these Tensor(s)."""
|
||||
@@ -361,14 +450,18 @@ class Tensor(RandMixin):
|
||||
self.uop = (x.uop.src[0] if x.uop.op is Ops.CONTIGUOUS else x.uop).clone()
|
||||
return self
|
||||
# STORE+AFTER: STORE is the write effect (void), AFTER wraps the view for correct shape/ranging
|
||||
assign = self.uop.after(self.uop.store(x.uop))
|
||||
assign = self.uop.after(store := self.uop.store(x.uop))
|
||||
ib = self.uop
|
||||
while ib.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH} and not (ib.has_buffer_identity() and _tensor_holds(ib)): ib = ib.src[0]
|
||||
if ib is not self.uop:
|
||||
# a partial write needs storage to land in: a pending value gets explicit storage (a clone)
|
||||
target = ib if ib.has_buffer_identity(after_ok=True) else ib.clone()
|
||||
if target is not ib: assign = assign.substitute({ib: target}, walk=True)
|
||||
_apply_map_to_tensors({ib: target.after(assign)}, name="Embed View Assign")
|
||||
if target is not ib:
|
||||
assign = assign.substitute({ib: target}, walk=True)
|
||||
store = assign.src[1]
|
||||
# view assign: the base reads "after the store into the view" (one AFTER level). replace the node under the
|
||||
# views (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
|
||||
_apply_map_to_tensors({ib: target.after(store)}, name="Embed View Assign")
|
||||
else:
|
||||
# simple assign
|
||||
self.uop = assign
|
||||
@@ -457,8 +550,9 @@ class Tensor(RandMixin):
|
||||
"""
|
||||
if self.uop.device is None: return self
|
||||
if (device:=canonicalize_device(device)) == self.device: return self
|
||||
# The transfer owns its destination from construction; COPY itself only describes the transfer.
|
||||
ret = Tensor(self.uop.copy_to_device(device).clone())
|
||||
# a copy to disk wants to persist, so it inserts a clone: the disk buffer is the storage of the copied value
|
||||
if isinstance(device, str) and device.startswith("DISK"): ret = Tensor(self.uop.clone(device))
|
||||
else: ret = Tensor(self.uop.copy_to_device(device))
|
||||
if self.grad is not None: ret.grad = self.grad.to(device)
|
||||
return ret.is_param_(self.is_param)
|
||||
|
||||
|
||||
+16
-83
@@ -537,20 +537,14 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if len(srcs) == 1 and isinstance(srcs[0], UOp): return srcs[0]
|
||||
return UOp(Ops.GROUP, src=tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
@property
|
||||
def is_value_call(self) -> bool:
|
||||
return self.op is Ops.CALL and self.arg is not None and self.arg.output_pos is not None
|
||||
@property
|
||||
def call_outputs(self) -> tuple[UOp, ...]:
|
||||
assert self.is_value_call
|
||||
return tuple(self.src[1+i].after(self) for i in self.arg.output_pos)
|
||||
@property
|
||||
def has_unbound_outputs(self) -> bool:
|
||||
"""Whether any declared value-call outputs still lack backing storage (not a call-kind query)."""
|
||||
return self.is_value_call and any(self.src[1+i].unsharded_base.is_unbound for i in self.arg.output_pos)
|
||||
"""does this call still have unresolved outputs: unbound BUFFERs among its inputs (minted by call_with_outputs,
|
||||
resolved when the call is inlined or the outputs are materialized). a lifecycle query, not a call type"""
|
||||
return self.op is Ops.CALL and any(x.unsharded_base.is_unbound for x in self.src[1:])
|
||||
@property
|
||||
def unbound_outputs(self) -> tuple[UOp, ...]:
|
||||
"""the unresolved outputs of this call: an AFTER on each unbound BUFFER input, usable like a normal buffer"""
|
||||
return tuple(x for x in self.call_outputs if x.src[0].unsharded_base.is_unbound) if self.is_value_call else ()
|
||||
return tuple(x.after(self) for x in self.src[1:] if x.unsharded_base.is_unbound)
|
||||
def index(self, *srcs:UOp|int|None, **kwargs):
|
||||
new_srcs: list[UOp] = [UOp.const(x) if isinstance(x, int) else x for x in srcs if x is not None]
|
||||
if len(new_srcs) == 1 and new_srcs[0].op is Ops.CONST and self.op is Ops.STACK: return self.src[new_srcs[0].val]
|
||||
@@ -770,15 +764,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
while b.op in {Ops.BITCAST, Ops.AFTER, Ops.UNSHARD}: b = b.src[0].unsharded_base
|
||||
return b
|
||||
|
||||
@property
|
||||
def storage_view(self) -> UOp:
|
||||
"""The addressed view without storage-state dependencies. Shape expressions retain their bindings."""
|
||||
if self.op is Ops.AFTER: return self.src[0].storage_view
|
||||
if self.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH, Ops.UNSHARD, Ops.MSELECT}:
|
||||
return self.replace(src=(self.src[0].storage_view,)+self.src[1:])
|
||||
if self.op is Ops.MSTACK: return self.replace(src=tuple(s.storage_view for s in self.src))
|
||||
return self
|
||||
|
||||
# cached property here makes external_uop_gc fail, why?
|
||||
@property
|
||||
def as_shape(self) -> tuple[sint, ...]:
|
||||
@@ -827,12 +812,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
slot = next(UOp.unique_num) if num is None else num
|
||||
buf = MultiBuffer(device, size, dtype) if isinstance(device, tuple) else Buffer(device, size, dtype)
|
||||
return UOp(Ops.BUFFER, arg=ParamArg(slot, dtype, size=size, device=device, buffer=buf))
|
||||
def bind_buffer(self) -> UOp:
|
||||
"""Attach backing storage to an existing declaration without minting a new storage slot."""
|
||||
assert self.is_unbound and not self.is_virtual
|
||||
buf = MultiBuffer(self.device, self.max_numel(), self.dtype) if isinstance(self.device, tuple) else \
|
||||
Buffer(self.device, self.max_numel(), self.dtype)
|
||||
return self.replace(arg=replace(self.arg, buffer=buf))
|
||||
@staticmethod
|
||||
def from_buffer(opaque:Buffer, device:str|tuple[str, ...]|None=None):
|
||||
# the opaque Buffer goes straight in the arg: the ucache dedups because the arg (and thus the Buffer) is part of the key
|
||||
@@ -854,27 +833,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
data = struct.pack(f"{prod(shape)}{bdtype.fmt}", *[truncate[bdtype](bdtype.const(xi)) for xi in fully_flatten(x)])
|
||||
ret.buffer.allocate(memoryview(bytearray(data))) # fake realize. buffer storage must be writable, and bytes isn't
|
||||
if ret.dtype != dtype: ret = ret.cast(dtype)
|
||||
return ret if ret.device == device else ret.clone(device)
|
||||
def materialize(self, memo:dict[UOp, UOp]|None=None) -> UOp:
|
||||
"""Build an explicit output request. Share destinations within a multi-output request."""
|
||||
if memo is None: memo = {}
|
||||
if self not in memo: memo[self] = self._materialize(memo)
|
||||
return memo[self]
|
||||
|
||||
def _materialize(self, memo:dict[UOp, UOp]) -> UOp:
|
||||
if self.is_virtual or (isinstance(self.device, str) and self.device.startswith("DISK")): return self
|
||||
if self.op in {Ops.DETACH, Ops.CONTIGUOUS_BACKWARD}: return self.src[0].materialize(memo)
|
||||
if self.op is Ops.AFTER or self.storage_base.op in {Ops.BUFFER, Ops.PARAM}: return self
|
||||
if self.op in GroupOp.Movement:
|
||||
return self.replace(src=(self.src[0].materialize(memo),)+self.src[1:])
|
||||
if self.op is Ops.CONTIGUOUS:
|
||||
src = self.src[0]
|
||||
while src.op in {Ops.DETACH, Ops.CONTIGUOUS_BACKWARD}: src = src.src[0]
|
||||
if src.op is Ops.CONTIGUOUS: return src.materialize(memo)
|
||||
if src.has_buffer_identity(after_ok=True, unbound_ok=True): return src
|
||||
if (view:=src.buffer_view()) is not None: return view
|
||||
return src.clone() if src.op in GroupOp.Movement|{Ops.AFTER, Ops.BITCAST} else src.materialize(memo)
|
||||
return self.clone()
|
||||
return ret if ret.device == device else ret.copy_to_device(device)
|
||||
def clone(self, device=None) -> UOp:
|
||||
device = device or self.device
|
||||
ret = self.empty_like(device=device)
|
||||
@@ -924,22 +883,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
while len(s.src) and s.op not in {Ops.BUFFER, Ops.PARAM, Ops.STAGE, Ops.MSTACK}: s = s.src[0]
|
||||
return s
|
||||
|
||||
def buffer_view(self) -> UOp|None:
|
||||
"""Construct a zero-copy view when movement/bitcast operations describe a contiguous buffer range."""
|
||||
if not all_int(self.shape): return None
|
||||
src, buf = self, self.base
|
||||
while buf.op is Ops.BITCAST: buf = buf.src[0].base
|
||||
if buf.op not in {Ops.BUFFER, Ops.UNSHARD}: return None
|
||||
unshard = None
|
||||
if buf.op is Ops.UNSHARD:
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
if isinstance(self.device, str): return None
|
||||
if (unshard := graph_rewrite(src, multi_pm, name="multi buffer view")).op is not Ops.UNSHARD: return None
|
||||
src = unshard.src[0]
|
||||
if (cv := src.contiguous_view()) is None or (buf := cv[0]).op is not Ops.BUFFER: return None
|
||||
view = buf[cv[1]:cv[1] + src.max_numel() * src.element_size() // buf.element_size()].bitcast(src.dtype).reshape(src.shape)
|
||||
return view.unshard(unshard.arg, unshard.src[1:]) if unshard is not None else view.reshape(self.shape)
|
||||
|
||||
def contiguous_view(self) -> tuple[UOp, int]|None:
|
||||
from tinygrad.schedule.prepare import pm_mops
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
@@ -958,12 +901,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
def contiguous_view_offset(self) -> int|None: return None if (view := self.contiguous_view()) is None else view[1]
|
||||
|
||||
def has_buffer_identity(self, after_ok=False, unbound_ok=False):
|
||||
"""Check for storage through shape wrappers; unbound_ok also accepts declarations without backing buffers."""
|
||||
def has_buffer_identity(self, after_ok=False):
|
||||
"""Check if this UOp has a concrete buffer identity in the graph (RESHAPE/UNSHARD -> BUFFER chain)."""
|
||||
# TODO: this is confusing because UOp.variable('v', 0, 1, dtypes.weakfloat) is True for jit to work, but it doesn't have a buffer
|
||||
if self.op in {Ops.RESHAPE, Ops.UNSHARD, Ops.MSELECT}: return self.src[0].has_buffer_identity(after_ok, unbound_ok)
|
||||
if after_ok and self.op == Ops.AFTER: return self.src[0].has_buffer_identity(after_ok, unbound_ok)
|
||||
return self.op in {Ops.BUFFER, Ops.PARAM} and (unbound_ok or not self.is_unbound)
|
||||
if self.op in {Ops.RESHAPE, Ops.UNSHARD, Ops.MSELECT}: return self.src[0].has_buffer_identity(after_ok)
|
||||
if after_ok and self.op == Ops.AFTER: return self.src[0].has_buffer_identity(after_ok)
|
||||
return self.op in {Ops.BUFFER, Ops.PARAM} and not self.is_unbound
|
||||
@property
|
||||
def is_unbound(self) -> bool:
|
||||
# an unbound GLOBAL BUFFER has no storage bound yet: it's a declaration of storage (call output, scheduler temp)
|
||||
@@ -1266,11 +1209,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
output_pos gives the position of each output in the arg list (default: a block after the inputs), the inputs take
|
||||
the remaining positions in order; when it's given, input params must already be slotted at their final positions.
|
||||
output_pos must be strictly ascending: the body's stores and the call args pair positionally by values order"""
|
||||
# Precompiled outputs are storage, including otherwise virtual constant results.
|
||||
if precompile: values = tuple(v.cast(v.commit_dtype()) for v in values)
|
||||
# the device defaults to the first device in the values or args, like srcs-based device resolution
|
||||
default_dev = next((x.device for x in itertools.chain(values, srcs) if x.device is not None), None)
|
||||
if precompile and default_dev is None: default_dev = canonicalize_device(None)
|
||||
pos = tuple(range(len(srcs), len(srcs)+len(values))) if output_pos is None else output_pos
|
||||
assert len(pos) == len(values) and len(set(pos)) == len(pos), "output_pos must be one distinct position per output"
|
||||
assert all(a < b for a, b in zip(pos, pos[1:])), f"output_pos {output_pos} must be strictly ascending"
|
||||
@@ -1301,7 +1241,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
it = iter(srcs)
|
||||
call = body.call(*[r if r is not None else next(it) for r in args], grad_fxn=grad_fxn, name=name, precompile=precompile,
|
||||
precompile_backward=precompile_backward, aux=aux)
|
||||
call = call.replace(arg=replace(call.arg, output_pos=pos))
|
||||
return tuple(r.after(call) for r in rets)
|
||||
|
||||
# one-line convenience for the single-output case: self is the value
|
||||
@@ -1384,14 +1323,12 @@ class CallInfo:
|
||||
precompile_backward: bool = False
|
||||
aux: Any = None
|
||||
dtype: DType = dtypes.void
|
||||
# None for opaque calls; value-call outputs are positional, independent of their backing-buffer bindings.
|
||||
output_pos: tuple[int, ...]|None = None
|
||||
# grad_fxn can't be pickled
|
||||
def __reduce__(self): return (CallInfo, (None, self.name, self.precompile, self.precompile_backward, self.aux, self.dtype, self.output_pos))
|
||||
def __reduce__(self): return (CallInfo, (None, self.name, self.precompile, self.precompile_backward, self.aux, self.dtype))
|
||||
def __repr__(self):
|
||||
gf = id(self.grad_fxn) if self.grad_fxn else None
|
||||
return f"CallInfo({gf}, {repr(self.name)}, {self.precompile}, {self.precompile_backward})" + \
|
||||
(f", {self.dtype}" if self.dtype is not dtypes.void else "") + (f", output_pos={self.output_pos}" if self.output_pos is not None else "")
|
||||
(f", {self.dtype}" if self.dtype is not dtypes.void else "")
|
||||
|
||||
# ******** ops in python ********
|
||||
|
||||
@@ -1870,16 +1807,12 @@ def to_max_shape(shape:tuple[sint, ...]) -> tuple[int, ...]: return tuple(int(x.
|
||||
_substitute = PatternMatcher([(UPat(tuple(Ops), name="x"), lambda ctx,x: ctx.get(x,None))])
|
||||
_pm_resolve_params = PatternMatcher([(UPat(Ops.PARAM, name="p"), lambda ctx,p: ctx[p.arg.slot])])
|
||||
|
||||
def resolve_returned_after(a:UOp, r:UOp, t:UOp) -> UOp|None:
|
||||
def resolve_returned_after(r:UOp, t:UOp) -> UOp|None:
|
||||
"""AFTER on a RETURNED placeholder extracts the call output value: the value of its matching store in a SINK body
|
||||
(called from patterns that bind t to a SINK)"""
|
||||
stores = [st for st in t.src if st.op is Ops.STORE and st.src[0].unsharded_base is r.unsharded_base]
|
||||
if len(stores) != 1: return None
|
||||
# Unbound, scope-local outputs are values and can fuse. Escaping outputs have been scoped as PARAMs:
|
||||
# keep their STORE instead of extracting its value and losing the declared destination.
|
||||
val = stores[0].src[1]
|
||||
ret = val if r.unsharded_base.is_unbound or (val.op is Ops.AFTER and val.src[0] is r) else r.after(stores[0])
|
||||
return ret.replace(tag=(ret.tag or ()) + a.tag) if a.tag else ret
|
||||
vals = [st.src[1] for st in t.src if st.op is Ops.STORE and st.src[0].unsharded_base is r.unsharded_base] \
|
||||
if r.unsharded_base.is_unbound else []
|
||||
return vals[0] if len(vals) == 1 else None
|
||||
remove_all_tags = PatternMatcher([(UPat(GroupOp.All, name="x"), lambda x: x.replace(tag=None) if x.tag is not None else None)])
|
||||
|
||||
def gate_kernel_sink(x:UOp) -> bool:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.uop import Ops, GroupOp
|
||||
from tinygrad.uop.ops import ParamArg, UOp, PatternMatcher, UPat, multirange_str, range_str, consumer_map_from_toposort, sint
|
||||
from tinygrad.helpers import strip_parens
|
||||
@@ -84,6 +84,9 @@ pm_pyrender_extra = PatternMatcher([
|
||||
(UPat(Ops.CONST, src=(), name="x"), lambda x: f"UOp.const({x.val})"),
|
||||
(UPat((Ops.CAST, Ops.BITCAST), name="x"), lambda ctx,x: f"{ctx[x.src[0]]}.{x.op.name.lower()}({x.dtype})" if x.dtype != x.src[0].dtype else None),
|
||||
(UPat(Ops.SPECIAL, src=(UPat(Ops.CONST),), name="x"), lambda x: f"UOp.special({x.src[0].val}, {repr(x.arg)})"),
|
||||
(UPat(Ops.BUFFER, src=(), name="x"), lambda x:
|
||||
f"UOp.new_buffer({repr(x.arg.device)}, {x.max_numel()}, {x.dtype}, {x.arg.slot})"
|
||||
if isinstance(x.arg, ParamArg) and x.addrspace is AddrSpace.GLOBAL else None),
|
||||
(UPat(Ops.COPY, src=(UPat(name="x"),), name="copy"), lambda ctx,x,copy: f"{ctx[x]}.copy_to_device({repr(copy.arg)})"),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda ctx,x: f"UOp(Ops.CUSTOM_FUNCTION, src={srcs(ctx, x.src)}, arg={x.arg!r})"),
|
||||
(UPat(Ops.REDUCE, name="r"), lambda ctx,r: f"{ctx[r.src[0]]}._rop({r.arg[0]}, {tuple(range(r.arg[1]))})" if r.arg[1] else None),
|
||||
|
||||
@@ -135,7 +135,7 @@ spec_tensor = PatternMatcher([
|
||||
|
||||
# BUFFER
|
||||
(UPat(Ops.BUFFER, src=(), name="buf"), lambda buf:
|
||||
True if buf.is_unbound else (isinstance(buf.dtype, DType) and isinstance(buf.arg.size, (int, type(None))) and is_device(buf.arg.device))
|
||||
True if buf.is_unbound else (isinstance(buf.dtype, DType) and isinstance(buf.arg.size, int) and is_device(buf.arg.device))
|
||||
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
|
||||
|
||||
# a Variable is a 0-d ALU BUFFER with a value range and no device
|
||||
|
||||
Reference in New Issue
Block a user