forked from tinygrad/tinygrad
* delete Ops.FUNCTION/GETTUPLE/TUPLE: call outputs are AFTER on RETURNED placeholders value-producing calls: the body is a plain parametric program that stores outputs into output PARAMs (slots after the input PARAMs). the RETURNED placeholders are inputs to the call, bound to the output PARAMs positionally wherever the call is resolved, and callers AFTER on them like normal buffers. gradient flows through the generic AFTER rule; everything is just Ops.CALL. * RETURNED identity is its placement in the call srcs, not a nonce slot=-1 merging collapses duplicate-signature outputs into one uop (t+1,t+2 grads and multi-grad backward calls dedupe wrongly), and skipping the uop cache breaks schedule_cache (stale linear hits since structural keys assume interning). instead the RETURNED's placement (output index among call srcs) is its identity: identical call constructions merge deterministically, positions never collide. * resolve RETURNED afters in the tensor graph like values (master parity with gettuple) - remove the CONTIGUOUS wrap of tagged call-output afters, it forced call outputs (e.g. local shard amax) into their own buffer/kernel instead of inlining - inline RETURNED afters at transform time via returned_after_finalize, dissolving to values for consumers; calls with bound-variable or unresolved UNSHARD args keep the schedule-time resolution path - allow movement ops (flat-storage views) in kernel graph value positions in the spec - port embedding backward + extra/llama_kernels (local_abs_max, rmsnorm) to the new API * use SINK, not GROUP, for gradient value containers spec.py only blesses GROUP of stores/groups/loops; the gradient value bundles (the forward values, root_grad seeds, and the after->call gradient edge) are plain value containers, and SINK-of-values is already in the spec. also fix extra/llama_kernels/rmsnorm: returned_outputs is a property * CALL is positional: RETURNS work in any src position, convention lives in call_outputs - all resolution paths (gradient, precompile transform, binding) locate RETURNEDs by identity, not by "last srcs"; only call_outputs builds the args-first layout - grad_fxn padding aligns grads with the call's actual src positions - add test_two_return/precompiled * source-compat shim for maketuple/gettuple so foreign code built before the redesign keeps working UOp.maketuple returns a _LegacyTupleValues holder; .call builds call_outputs; CALL.gettuple(i) is returned_outputs[i]. the produced graphs are identical to the new-api versions, so nn/extra/mlperf code is reverted to upstream text * simplify function.py call construction + drop the resolved-call cache - function.py: single and tuple returns both build the call through call_outputs - tensor.py: resolve_function is deterministic and interned, the global cache was unneeded * bind zero-offset views of flat storage to the storage instead of padding them call args need offset 0 and enough length, not views: flat_storage collapses the zero-offset contiguous view chain to the sized storage base, so resolved call args are storage-bare like master (no PAD/SHRINK chains in the kernel graph) * spec.py: drop stray rebase-collision edits, keep only the RETURNED changes * test_multitensor: revert to master, the gettuple shim covers it * materialize all tagged RETURNED afters into real buffers call outputs need real storage regardless of whether they are finals of the current realize: deferred/stateful outputs (the fp8 grad-amax mailbox) are consumed by later realize steps as call args, where a resolved value would have no ranges * call input buffers: wrap RETURNED-based afters, not real-buffer afters precompiled call input binding kept any AFTER unwrapped; an AFTER on a RETURNED placeholder has no storage behind it, so its value leaked into the kernel graph with no consumer able to register ranges (llama3 8B fp8 mailbox pipeline crash). materialize afters whose base has no buffer identity instead. this was the fix matching master for the REDUCE-has-no-ranges crash and restores the llama-kernels amax kernel count * call slots are src positions, always; never rearrange one upstream cause behind the three P1 findings: the raw CALL machinery binds positionally (resolve_function params, gradient padding) but a second args-first convention crept in where RETURNEDs get moved to trailing slots. position is identity now: - transform_precompiled_call keeps RETURNEDs' original src positions: outs take their places, other args become input buffers; no slot renumbering - implicit gradients are emitted aligned to original src positions (None at RETURNED positions) - flat_storage drops the hand-rolled contiguity analysis: reshape itself is the flat-prefix check (it raises ValueError); strided views materialize first * nits on call slot positions; regression tests for interspersed RETURNED - flat_storage back to pad_to().reshape() (reshape keeps movement views, it is not a contiguity check) - input_buffer checks has_buffer_identity(after_ok=True) - TestArgOrder: interspersed RETURNED (plain + precompiled transform), its gradient, padded and strided function inputs * device fixes * TestArgOrder: padded regression uses zero-start padded/shrunk view * TestArgOrder: clone to force buffer identity in padded/strided regression tests * slim: revert prepare formatting, drop reverted-bug tests, restore viz guards, clean comments, mirror returned on param * gut transform_precompiled_call, delete returned_after_finalize the transform keeps master's shape; the prepare-stage resolve_AFTER rule already inlines plain call outputs, and materialization is owned by the input-buffer rule (afters on real buffers bind, afters on RETURNEDs contiguous) * update spec for returned * transform_precompiled_call: inline the input-buffer rule, drop sorted() (body stores are already slot-ordered) * drop dead RETURNED-era rules: prepare's after-shell strip (leftover from returned_after_finalize, which is gone), redundant pattern-covered SINK check, defensive slot-sorts (bodies are slot-ordered by construction) * drop final_tags: final outputs of value calls materialize at sink construction The set of finals is already known precisely (the big_sink's srcs), so track nothing: wrap each final AFTER-on-RETURNED in CONTIGUOUS right after numbering. Precompiled calls are excluded - transform_precompiled_call in the flatten pass gives their outputs real buffers, and wrapping before that transform leaves a stale tag that breaks the output copy. * drop unused default_dtype import
674 lines
25 KiB
Python
674 lines
25 KiB
Python
import numpy as np
|
|
import unittest
|
|
from tinygrad.function import function
|
|
from tinygrad import Tensor, GlobalCounters, Device
|
|
from tinygrad.dtype import Invalid
|
|
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
|
|
from test.helpers import assert_kernel_count, KernelCountException
|
|
|
|
class TestFunction(unittest.TestCase):
|
|
def test_simple(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b
|
|
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
np.testing.assert_equal(f(a,b).numpy(), [5,7,9])
|
|
|
|
def test_two_return(self, precompile=False):
|
|
@function(precompile=precompile)
|
|
def f(a:Tensor, b:Tensor) -> tuple[Tensor, Tensor]:
|
|
return (a+b, (a+b)*2)
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
c = f(a,b)
|
|
np.testing.assert_equal((c[0]+c[1]).numpy(), [5*3,7*3,9*3])
|
|
def test_two_return_precompiled(self): self.test_two_return(True)
|
|
|
|
def test_simple_same(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b
|
|
|
|
a = Tensor([1,2,3])
|
|
np.testing.assert_equal(f(a,a).numpy(), [2,4,6])
|
|
|
|
def test_depth_restored_on_exception(self):
|
|
from tinygrad.function import _function
|
|
@function
|
|
def f(a:Tensor) -> Tensor: raise ValueError("error")
|
|
with self.assertRaises(ValueError): f(Tensor([1]))
|
|
self.assertEqual(_function.depth, 0)
|
|
|
|
def test_implicit(self):
|
|
inp = Tensor([7,8,9])
|
|
@function(allow_implicit=True)
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b+inp
|
|
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
np.testing.assert_equal(f(a,b).numpy(), [12,15,18])
|
|
|
|
def test_implicit_same_as_input(self):
|
|
inp = Tensor([7,8,9])
|
|
@function(allow_implicit=True)
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b+inp
|
|
|
|
a = Tensor([1,2,3])
|
|
np.testing.assert_equal(f(a, inp).numpy(), [15,18,21])
|
|
|
|
def test_implicit_2(self):
|
|
inp = Tensor([7,8,9])
|
|
@function(allow_implicit=True)
|
|
def f(a:Tensor, b:Tensor) -> Tensor:
|
|
return a+b+inp
|
|
inp2 = Tensor([7,8,10])
|
|
@function(allow_implicit=True)
|
|
def g(a:Tensor, b:Tensor) -> Tensor:
|
|
return a+b+inp2
|
|
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
c = f(a,b)
|
|
d = g(a,b)
|
|
c.realize(d)
|
|
np.testing.assert_equal(c.numpy(), [12,15,18])
|
|
np.testing.assert_equal(d.numpy(), [12,15,19])
|
|
|
|
def test_implicit_unrealized(self):
|
|
inp = Tensor([1,2,3]) + Tensor([4,5,6])
|
|
@function(allow_implicit=True)
|
|
def f(a:Tensor) -> Tensor: return a + inp
|
|
|
|
np.testing.assert_equal(f(Tensor([10,20,30])).numpy(), [15,27,39])
|
|
|
|
def test_detach(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a.detach() + b
|
|
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
np.testing.assert_equal(f(a, b).numpy(), [5,7,9])
|
|
|
|
def test_contiguous_backward(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return (a + b).contiguous_backward()
|
|
|
|
a = Tensor([1,2,3])
|
|
b = Tensor([4,5,6])
|
|
np.testing.assert_equal(f(a, b).numpy(), [5,7,9])
|
|
|
|
def test_method(self):
|
|
class Foo:
|
|
def __init__(self): self.w = Tensor([10,20,30])
|
|
@function
|
|
def __call__(self, x:Tensor) -> Tensor: return x + self.w
|
|
|
|
foo = Foo()
|
|
np.testing.assert_equal(foo(Tensor([1,2,3])).numpy(), [11,22,33])
|
|
|
|
def test_grad_gemm(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a @ b
|
|
|
|
a = Tensor([[1.,2.],[3.,4.]])
|
|
b = Tensor([[5.,6.],[7.,8.]])
|
|
(f(a, b).contiguous() * b).sum().backward()
|
|
Tensor.realize(a, b, a.grad, b.grad)
|
|
# L = sum((a@b) * b), dL/d(a@b) = b, dL/da = b @ b^T, dL/db = a^T @ b + (a@b)
|
|
na, nb = a.numpy(), b.numpy()
|
|
np.testing.assert_allclose(a.grad.numpy(), nb @ nb.T)
|
|
np.testing.assert_allclose(b.grad.numpy(), na.T @ nb + na @ nb)
|
|
|
|
def test_grad_implicit(self):
|
|
w = Tensor([1., 2., 3.])
|
|
w.realize() # TODO: this is required
|
|
@function(allow_implicit=True)
|
|
def f(x:Tensor) -> Tensor: return x * w
|
|
|
|
x = Tensor([4., 5., 6.])
|
|
f(x).sum().backward()
|
|
np.testing.assert_allclose(w.grad.numpy(), [4., 5., 6.])
|
|
|
|
def test_symbolic_index(self):
|
|
table = Tensor([10,20,30,40]).contiguous().realize()
|
|
@function(allow_implicit=True)
|
|
def f(x:Tensor, start_pos:int|UOp) -> Tensor:
|
|
return x + table[start_pos]
|
|
|
|
v = UOp.variable("start_pos", 0, 3)
|
|
np.testing.assert_equal(f(Tensor([1,2,3]), v.bind(0)).numpy(), [11,12,13])
|
|
|
|
def test_symbolic_shape_input(self):
|
|
table = Tensor([10,20,30,40]).contiguous().realize()
|
|
@function
|
|
def f(x:Tensor) -> Tensor: return x * 2
|
|
sz = UOp.variable("sz", 1, 3)
|
|
slic = table[:sz.bind(2)]
|
|
np.testing.assert_equal(f(slic)[:2].numpy(), [20,40])
|
|
|
|
def test_nested_calls(self):
|
|
w = Tensor([10., 20., 30.])
|
|
@function(allow_implicit=True)
|
|
def f(a:Tensor) -> Tensor: return a + w
|
|
@function(allow_implicit=True)
|
|
def g(a:Tensor) -> Tensor: return a * w
|
|
|
|
a = Tensor([1., 2., 3.])
|
|
np.testing.assert_allclose(g(f(a)).numpy(), [110., 440., 990.])
|
|
|
|
def test_nested_calls_backward(self):
|
|
w = Tensor([[1., 2.], [3., 4.]]).contiguous().realize()
|
|
@function(allow_implicit=True)
|
|
def inner(x:Tensor) -> Tensor: return x + w
|
|
@function(allow_implicit=True)
|
|
def outer(a:Tensor, b:Tensor) -> Tensor: return inner(a.reshape(1,2) + b.reshape(1,2))
|
|
|
|
a = Tensor([1., 2.])
|
|
b = Tensor([3., 4.])
|
|
outer(a, b).sum().backward()
|
|
np.testing.assert_allclose(a.grad.numpy(), [2., 2.])
|
|
np.testing.assert_allclose(b.grad.numpy(), [2., 2.])
|
|
|
|
def test_unused_param_backward(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor, c:Tensor) -> Tensor: return a + c # b is unused
|
|
|
|
a = Tensor([1., 2., 3.])
|
|
b = Tensor([4., 5., 6.])
|
|
c = Tensor([7., 8., 9.])
|
|
f(a, b, c).sum().backward()
|
|
np.testing.assert_allclose(a.grad.numpy(), [1., 1., 1.])
|
|
np.testing.assert_allclose(b.grad.numpy(), [0., 0., 0.])
|
|
np.testing.assert_allclose(c.grad.numpy(), [1., 1., 1.])
|
|
|
|
def test_name(self):
|
|
@function
|
|
def f(a:Tensor) -> Tensor: return a + 1
|
|
assert f(Tensor([1])).uop.src[1].arg.name.endswith("f")
|
|
|
|
def test_method_name(self):
|
|
class Foo:
|
|
@function
|
|
def __call__(self, x:Tensor) -> Tensor: return x + 1
|
|
assert Foo()(Tensor([1])).uop.src[1].arg.name.endswith("Foo.__call__")
|
|
|
|
def test_callable_instance(self):
|
|
class Foo:
|
|
def __init__(self): self.w = Tensor([10,20,30])
|
|
def __call__(self, x:Tensor) -> Tensor: return x + self.w
|
|
foo = Foo()
|
|
f = function(foo, allow_implicit=True)
|
|
np.testing.assert_equal(f(Tensor([1,2,3])).numpy(), [11,22,33])
|
|
assert f(Tensor([1,2,3])).uop.src[1].arg.name.endswith("Foo")
|
|
|
|
def test_iadd(self):
|
|
@function
|
|
def f(x:Tensor) -> Tensor:
|
|
x += 1
|
|
return x
|
|
|
|
a = Tensor([1,2,3]).realize()
|
|
np.testing.assert_equal(f(a).numpy(), [2,3,4])
|
|
np.testing.assert_equal(a.numpy(), [3,4,5]) # TODO: should be [1,2,3]
|
|
|
|
def test_implicit_assign(self):
|
|
a = Tensor([1,2,3])
|
|
a += 1
|
|
c = Tensor([2,2,2]).contiguous()
|
|
@function
|
|
def f(b:Tensor) -> Tensor: return a+b+c
|
|
b = Tensor([10,20,30]).realize()
|
|
np.testing.assert_equal(f(b).numpy(), [14,25,36])
|
|
|
|
def test_assign_input(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor:
|
|
a.assign(b+1)
|
|
return a
|
|
|
|
a = Tensor([1,2,3]).realize()
|
|
b = Tensor([10,20,30]).realize()
|
|
np.testing.assert_equal(f(a,b).numpy(), [11,21,31])
|
|
np.testing.assert_equal(a.numpy(), [11,21,31]) # TODO: should be [1,2,3]
|
|
np.testing.assert_equal(b.numpy(), [10,20,30])
|
|
|
|
def test_view_assign_explicit_buffer(self):
|
|
"""view assign on an explicit param's buffer should not create implicit inputs."""
|
|
class State:
|
|
def __init__(self): self.buf = Tensor.zeros(2, 4).contiguous().realize()
|
|
@function(allow_implicit=False)
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
self.buf[:, 0:2].assign(x)
|
|
return self.buf[:, 0:2]
|
|
s = State()
|
|
np.testing.assert_equal(s(Tensor([[5., 6.], [7., 8.]])).numpy(), [[5., 6.], [7., 8.]])
|
|
|
|
def test_single_after_store(self):
|
|
"""AFTER(buf, STORE(view, data)) should write data through the view into buf, same as the double-after pattern."""
|
|
@function
|
|
def f(buf:Tensor, x:Tensor, start_pos:int|UOp) -> Tensor:
|
|
slice_uop = buf[:, start_pos:start_pos+1].uop
|
|
assigned = Tensor(buf.uop.after(slice_uop.store(x.uop)))
|
|
return assigned
|
|
|
|
buf = Tensor.zeros(2, 8).contiguous().realize()
|
|
x = Tensor([[1.], [2.]]).realize()
|
|
v = UOp.variable("sp", 0, 7)
|
|
r0 = f(buf, x, v.bind(0)).numpy()
|
|
np.testing.assert_equal(r0, [[1.,0.,0.,0.,0.,0.,0.,0.], [2.,0.,0.,0.,0.,0.,0.,0.]])
|
|
|
|
def test_single_after_store_precompile(self):
|
|
"""precompiled AFTER(buf, STORE(view, data)) should return buf after the store."""
|
|
@function(precompile=True)
|
|
def f(buf:Tensor, x:Tensor, start_pos:int|UOp) -> Tensor:
|
|
slice_uop = buf[:, start_pos:start_pos+1].uop
|
|
assigned = Tensor(buf.uop.after(slice_uop.store(x.uop)))
|
|
return assigned
|
|
|
|
x = Tensor([[1.], [2.]]).realize()
|
|
v = UOp.variable("sp", 0, 7)
|
|
for sp in (0, 2):
|
|
with self.subTest(sp=sp):
|
|
buf = Tensor.zeros(2, 8).clone().realize()
|
|
expected = np.zeros((2, 8), dtype=np.float32)
|
|
expected[:, sp] = [1., 2.]
|
|
np.testing.assert_equal(f(buf, x, v.bind(sp)).numpy(), expected)
|
|
np.testing.assert_equal(buf.numpy(), expected)
|
|
|
|
@unittest.expectedFailure
|
|
def test_assign_slice(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor:
|
|
a[1:] = b[1:]+1
|
|
return a
|
|
|
|
a = Tensor([1,2,3]).realize()
|
|
b = Tensor([10,20,30]).realize()
|
|
np.testing.assert_equal(f(a,b).numpy(), [1,21,31])
|
|
np.testing.assert_equal(a.numpy(), [1,2,3])
|
|
np.testing.assert_equal(b.numpy(), [10,20,30])
|
|
|
|
class TestFunctionMulti(unittest.TestCase):
|
|
devices_2 = ("CPU:0", "CPU:1")
|
|
|
|
def test_simple_multi(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b
|
|
|
|
a = Tensor([1,2,3,4]).shard(self.devices_2, axis=None)
|
|
b = Tensor([10,20,30,40]).shard(self.devices_2, axis=None)
|
|
np.testing.assert_equal(f(a,b).numpy(), [11,22,33,44])
|
|
|
|
def test_simple_multi_sharded(self):
|
|
@function
|
|
def f(a:Tensor, b:Tensor) -> Tensor: return a+b
|
|
|
|
a = Tensor([1,2,3,4]).shard(self.devices_2, axis=0)
|
|
b = Tensor([10,20,30,40]).shard(self.devices_2, axis=0)
|
|
np.testing.assert_equal(f(a,b).numpy(), [11,22,33,44])
|
|
|
|
def test_data_parallel_multi(self):
|
|
@function
|
|
def f(x:Tensor, w:Tensor) -> Tensor: return x @ w
|
|
|
|
x = Tensor([[1.,2.],[3.,4.],[5.,6.],[7.,8.]]).shard(self.devices_2, axis=0)
|
|
w = Tensor([[1.,0.],[0.,1.]]).shard(self.devices_2, axis=None)
|
|
np.testing.assert_allclose(f(x, w).numpy(), [[1.,2.],[3.,4.],[5.,6.],[7.,8.]])
|
|
|
|
def test_grad_implicit_multi(self):
|
|
w = Tensor([1., 2., 3., 4.]).shard(self.devices_2, axis=None)
|
|
w.realize()
|
|
@function(allow_implicit=True)
|
|
def f(x:Tensor) -> Tensor: return x * w
|
|
|
|
x = Tensor([4., 5., 6., 7.]).shard(self.devices_2, axis=None)
|
|
f(x).sum().backward()
|
|
np.testing.assert_allclose(w.grad.numpy(), [4., 5., 6., 7.])
|
|
|
|
def test_call_axis(self):
|
|
@function
|
|
def f(x:Tensor, w:Tensor) -> Tensor: return x @ w
|
|
|
|
x = Tensor([[1.,0.],[0.,1.],[1.,1.],[0.,0.]]).shard(self.devices_2, axis=0)
|
|
w = Tensor([[1.,2.],[3.,4.]]).shard(self.devices_2, axis=None)
|
|
result = f(x, w)
|
|
# CALL output should inherit axis=0 from the sharded input
|
|
self.assertEqual(result.uop.axis, 0)
|
|
# reduce on the sharded axis should remove it
|
|
self.assertIsNone(result.sum().uop.axis)
|
|
|
|
def test_call_axis_shard_inside(self):
|
|
@function
|
|
def f(x:Tensor, w:Tensor) -> Tensor:
|
|
return x.shard(self.devices_2, axis=0) @ w.shard(self.devices_2, axis=None)
|
|
|
|
x = Tensor([[1.,0.],[0.,1.],[1.,1.],[0.,0.]])
|
|
w = Tensor([[1.,2.],[3.,4.]])
|
|
result = f(x, w)
|
|
self.assertEqual(result.uop.axis, 0)
|
|
np.testing.assert_allclose(result.numpy(), x.numpy() @ w.numpy())
|
|
|
|
def test_data_parallel_backward(self):
|
|
@function
|
|
def f(x:Tensor, w:Tensor) -> Tensor: return x @ w
|
|
|
|
x = Tensor([[1.,0.],[0.,1.],[1.,1.],[0.,0.]]).shard(self.devices_2, axis=0)
|
|
w = Tensor([[1.,2.],[3.,4.]]).shard(self.devices_2, axis=None)
|
|
w.realize()
|
|
f(x, w).sum().backward()
|
|
# d/dx = ones @ w^T = [[1,3],[1,3],[1,3],[1,3]], but sum so ones(4,2) @ w^T? no:
|
|
# L = sum(x @ w), dL/dx = ones(4,2) @ w^T... actually dL/d(xw) = ones(4,2), dL/dx = ones(4,2) @ w^T
|
|
np.testing.assert_allclose(x.grad.numpy(), np.ones((4,2)) @ np.array([[1,3],[2,4]]))
|
|
|
|
def test_data_parallel_backward_4(self):
|
|
devices_4 = tuple(f"CPU:{i}" for i in range(4))
|
|
@function
|
|
def f(x:Tensor, w:Tensor) -> Tensor: return x @ w
|
|
|
|
x = Tensor(np.arange(16).reshape(8,2).astype(np.float32)).shard(devices_4, axis=0)
|
|
w = Tensor([[1.,2.],[3.,4.]]).shard(devices_4, axis=None)
|
|
w.realize()
|
|
f(x, w).sum().backward()
|
|
np.testing.assert_allclose(x.grad.numpy(), np.ones((8,2)) @ np.array([[1,3],[2,4]]))
|
|
|
|
def test_data_parallel_backward_implicit(self):
|
|
devices_4 = tuple(f"CPU:{i}" for i in range(4))
|
|
w = Tensor([[1.,2.],[3.,4.]]).shard(devices_4, axis=None)
|
|
w.realize()
|
|
@function(allow_implicit=True)
|
|
def f(x:Tensor) -> Tensor: return x @ w
|
|
|
|
x = Tensor(np.arange(16).reshape(8,2).astype(np.float32)).shard(devices_4, axis=0)
|
|
f(x).sum().backward()
|
|
np.testing.assert_allclose(x.grad.numpy(), np.ones((8,2)) @ np.array([[1,3],[2,4]]))
|
|
|
|
def test_data_parallel_backward_twice(self):
|
|
devices_4 = tuple(f"CPU:{i}" for i in range(4))
|
|
w = Tensor([[1.,2.],[3.,4.]]).shard(devices_4, axis=None)
|
|
w.realize()
|
|
# pre-init grads like the training loop does
|
|
w.grad = w.zeros_like().contiguous().realize()
|
|
@function(allow_implicit=True)
|
|
def f(x:Tensor) -> Tensor: return x @ w
|
|
|
|
expected = np.ones((8,2)) @ np.array([[1,3],[2,4]])
|
|
for _ in range(2):
|
|
x = Tensor(np.arange(16).reshape(8,2).astype(np.float32)).shard(devices_4, axis=0)
|
|
f(x).sum().backward()
|
|
np.testing.assert_allclose(x.grad.numpy(), expected)
|
|
|
|
class TestFunctionTuple(unittest.TestCase):
|
|
def test_tuple(self, precompile=False):
|
|
x = Tensor.ones(3).contiguous()
|
|
@function(precompile=precompile)
|
|
def f(t:Tensor): return (t+1, t+2)
|
|
t1, t2 = f(x)
|
|
t1.realize(t2)
|
|
print(t1.tolist(), t2.tolist())
|
|
assert t1.tolist() == [2,2,2]
|
|
assert t2.tolist() == [3,3,3]
|
|
def test_tuple_precompile(self): self.test_tuple(True)
|
|
|
|
def test_grad_tuple(self, precompile=False):
|
|
x = Tensor.ones(3).contiguous()
|
|
y = Tensor.ones(3).contiguous()
|
|
@function(precompile=precompile)
|
|
def f(u1:Tensor, u2:Tensor): return (u1+1, u2+2)
|
|
t1, t2 = f(x,y)
|
|
(t1+t2).sum().backward()
|
|
x.grad.realize(y.grad)
|
|
def test_grad_tuple_precompile(self): self.test_grad_tuple(True)
|
|
|
|
def test_grad_fxn_tuple(self):
|
|
# grad_fxn for tuple: receives one gradient per output as positional args
|
|
def grad_fxn(d_out0:UOp, d_out1:UOp, call:UOp):
|
|
# f(u1, u2) = (u1+1, u2+2)
|
|
# df/du1 = d_out0, df/du2 = d_out1
|
|
return (d_out0, d_out1)
|
|
|
|
x = Tensor.ones(3).contiguous()
|
|
y = Tensor.ones(3).contiguous()
|
|
@function(grad_fxn=grad_fxn)
|
|
def f(u1:Tensor, u2:Tensor): return (u1+1, u2+2)
|
|
t1, t2 = f(x, y)
|
|
(t1+t2).sum().backward()
|
|
np.testing.assert_allclose(x.grad.numpy(), [1., 1., 1.])
|
|
np.testing.assert_allclose(y.grad.numpy(), [1., 1., 1.])
|
|
|
|
def test_grad_fxn_more_outputs_than_inputs(self):
|
|
def grad_fxn(grad:UOp, call:UOp): return (grad,)
|
|
|
|
x = Tensor([2.]).contiguous()
|
|
@function(grad_fxn=grad_fxn)
|
|
def f(x:Tensor): return (x+1, x+2)
|
|
_, y = f(x)
|
|
self.assertEqual(y.sum().gradient(x)[0].item(), 1.0)
|
|
|
|
def test_grad_unused_tuple_output_recursive(self):
|
|
# only one output is used
|
|
@function(precompile=True, precompile_backward=True)
|
|
def f(x:Tensor, w:Tensor):
|
|
a = x @ w
|
|
b = (x @ w) * 2 # shares x@w with a
|
|
return (a, b)
|
|
|
|
x = Tensor([[1., 2.], [3., 4.]]).contiguous()
|
|
w = Tensor([[1., 0.], [0., 1.]]).contiguous()
|
|
Tensor.realize(x, w)
|
|
t1, _ = f(x, w)
|
|
t1.sum().backward()
|
|
Tensor.realize(x.grad, w.grad)
|
|
# only t1 = x @ w flows to loss; dL/dw = x.T @ ones(2,2)
|
|
np.testing.assert_allclose(w.grad.numpy(), np.array([[1., 2.], [3., 4.]]).T @ np.ones((2, 2)))
|
|
np.testing.assert_allclose(x.grad.numpy(), np.ones((2, 2)) @ np.array([[1., 0.], [0., 1.]]).T)
|
|
|
|
def test_custom_kernel_save_unused_output(self):
|
|
def my_kernel(C:UOp, D:UOp, A:UOp) -> UOp:
|
|
i = UOp.range(A.shape[0], 0)
|
|
j = UOp.range(D.shape[0], 1)
|
|
store_c = C[i].store(A[i] * 2.0).end(i)
|
|
store_d = D[j].store(A[j]).end(j)
|
|
return UOp.sink(store_c, store_d, arg=KernelInfo(name="my_kernel"))
|
|
|
|
def my_grad(d_c:UOp, call:UOp):
|
|
a_input = call.src[3]
|
|
return (None, None, (Tensor(d_c) * 2.0 + Tensor(a_input) * 0).uop)
|
|
|
|
@function(precompile=True, precompile_backward=True)
|
|
def f(a:Tensor):
|
|
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
|
d = Tensor.invalids(3, dtype=a.dtype, device=a.device)
|
|
c, d = Tensor.custom_kernel(c, d, a, fxn=my_kernel, grad_fxn=my_grad)[:2]
|
|
return c, d
|
|
|
|
a = Tensor([1., 2., 3., 4.]).contiguous()
|
|
Tensor.realize(a)
|
|
c, _ = f(a)
|
|
c.sum().backward()
|
|
Tensor.realize(a.grad)
|
|
np.testing.assert_allclose(a.grad.numpy(), [2., 2., 2., 2.])
|
|
|
|
def test_custom_kernel_both_outputs_used(self):
|
|
def my_kernel(C:UOp, D:UOp, A:UOp) -> UOp:
|
|
i = UOp.range(A.shape[0], 0)
|
|
store_c = C[i].store(A[i] * 2.0)
|
|
store_d = D[i].store(A[i] * 3.0)
|
|
return UOp.group(store_c, store_d).end(i).sink(arg=KernelInfo(name="my_kernel"))
|
|
|
|
def my_grad(d_c:UOp, d_d:UOp, call:UOp):
|
|
return (None, None, (Tensor(d_c) + Tensor(d_d)).uop)
|
|
|
|
@function(precompile=True, precompile_backward=True)
|
|
def f(a:Tensor):
|
|
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
|
d = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
|
c, d = Tensor.custom_kernel(c, d, a, fxn=my_kernel, grad_fxn=my_grad)[:2]
|
|
return (c, d)
|
|
|
|
a = Tensor([1., 2., 3., 4.]).contiguous()
|
|
Tensor.realize(a)
|
|
c, d = f(a)
|
|
(c.sum() + d.sum()).backward() # dL/da = (1 + 1) since grad_fxn passes d_combined through
|
|
Tensor.realize(a.grad)
|
|
np.testing.assert_allclose(a.grad.numpy(), [2., 2., 2., 2.])
|
|
|
|
def test_custom_kernel_precompile_no_copy_kernel(self):
|
|
def my_kernel(C:UOp, A:UOp) -> UOp:
|
|
i = UOp.range(A.shape[0], 0)
|
|
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="my_kernel"))
|
|
|
|
def my_grad(d_c:UOp, call:UOp):
|
|
return (None, (Tensor(d_c) * 2.0).uop)
|
|
|
|
@function(precompile=True, precompile_backward=True)
|
|
def f(a:Tensor):
|
|
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
|
c = Tensor.custom_kernel(c, a, fxn=my_kernel, grad_fxn=my_grad)[0]
|
|
return c
|
|
|
|
def count_kernels(t:Tensor):
|
|
linear, _ = t.linear_with_vars()
|
|
return sum((len(call.device) if isinstance(call.device, tuple) else 1)
|
|
for call in linear.src if call.src[0].op is Ops.SINK)
|
|
|
|
a = Tensor([1., 2., 3., 4.]).contiguous()
|
|
Tensor.realize(a)
|
|
c = f(a)
|
|
|
|
if count_kernels(c) != 1: raise KernelCountException(1, count_kernels(c))
|
|
|
|
c.sum().backward()
|
|
Tensor.realize(a.grad)
|
|
np.testing.assert_allclose(a.grad.numpy(), [2., 2., 2., 2.])
|
|
|
|
def test_custom_kernel_precompile_multidevice(self):
|
|
# a custom_kernel output placeholder (invalids) under multi-device @function(precompile=True) must return the
|
|
# kernel's computed result. read it back through .numpy() so the cross-device gather reads the output buffer
|
|
devs = ("CPU:0", "CPU:1")
|
|
def double_kernel(C:UOp, A:UOp) -> UOp:
|
|
C, A = C.flatten(), A.flatten()
|
|
i = UOp.range(A.numel(), 0)
|
|
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="double_kernel"))
|
|
def double_grad(d_c:UOp, call:UOp): return (None, (Tensor(d_c) * 2.0).uop)
|
|
|
|
a = Tensor.full((4, 4), 7.0).contiguous().shard(devs, axis=0).realize()
|
|
|
|
@function(precompile=True, precompile_backward=True)
|
|
def f(a:Tensor):
|
|
c = Tensor(Tensor.invalids(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
|
|
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
|
|
|
|
np.testing.assert_allclose(f(a).numpy(), 14.0)
|
|
|
|
# g is f with empty output instead of invalids
|
|
@function(precompile=True, allow_implicit=True)
|
|
def g(a:Tensor):
|
|
c = Tensor(Tensor.empty(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
|
|
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
|
|
|
|
np.testing.assert_allclose(g(a).numpy(), 14.0)
|
|
|
|
def test_custom_kernel_inplace_output_is_implicit(self):
|
|
# a custom_kernel output the kernel also READS (in-place add) is not write-only, so it must be captured as an input
|
|
def inplace_add(C:UOp, A:UOp) -> UOp:
|
|
i = UOp.range(A.shape[0], 0)
|
|
return C[i].store(C[i].load() + A[i]).end(i).sink(arg=KernelInfo(name="inplace_add"))
|
|
@function(precompile=True, allow_implicit=False)
|
|
def f(a:Tensor): return Tensor.custom_kernel(Tensor.empty(*a.shape, dtype=a.dtype, device=a.device), a, fxn=inplace_add)[0]
|
|
with self.assertRaisesRegex(RuntimeError, "implicit buffer"): f(Tensor([1., 2., 3., 4.]).contiguous().realize())
|
|
|
|
def test_custom_kernel_write_only_persistent_output_is_implicit(self):
|
|
# a write-only custom_kernel output that is a realized buffer must be captured
|
|
def write(C:UOp, A:UOp) -> UOp:
|
|
i = UOp.range(A.shape[0], 0)
|
|
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="write"))
|
|
state = Tensor([100., 200., 300., 400.], device="CPU").contiguous().realize()
|
|
@function(precompile=True, allow_implicit=True)
|
|
def f(a:Tensor): return Tensor.custom_kernel(state, a, fxn=write)[0]
|
|
f(Tensor([1., 2., 3., 4.], device="CPU").contiguous().realize()).realize()
|
|
np.testing.assert_allclose(state.numpy(), [2., 4., 6., 8.])
|
|
|
|
def test_custom_kernel_program_invalids_not_captured(self):
|
|
# llama FP8 kernels are PROGRAM with bare-buffer sinks (no analyzable stores), so the invalids scratch
|
|
# still must not be captured as an input -- else it is read before the kernel writes it
|
|
src = "void k(float* restrict data0, float* restrict data1) { for (int i=0;i<4;i++) data0[i]=data1[i]*2.0f; }"
|
|
lib = Device["CPU"].compiler.compile(src)
|
|
def prog(C:UOp, A:UOp) -> UOp:
|
|
sink = UOp.sink(C.base, A.base, arg=KernelInfo(name="k"))
|
|
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)),
|
|
UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=lib)),
|
|
arg=ProgramInfo(name="k", global_size=(1, 1, 1), local_size=(1, 1, 1), globals=(0, 1)))
|
|
|
|
@function(precompile=True)
|
|
def f(a:Tensor):
|
|
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
|
return Tensor.custom_kernel(c, a, fxn=prog)[0]
|
|
|
|
a = Tensor([1., 2., 3., 4.], device="CPU").contiguous().realize()
|
|
np.testing.assert_allclose(f(a).numpy(), [2., 4., 6., 8.])
|
|
|
|
def test_invalid_store_into_realized_buffer_is_captured(self):
|
|
# only fresh invalids() scratch is skipped; a realized buffer is a real input even if an Invalid store
|
|
# writes into part of it (its other elements must be preserved), so it is still captured
|
|
state = Tensor([10., 20., 30., 40.], device="CPU").contiguous().realize()
|
|
@function(precompile=True, allow_implicit=True)
|
|
def f(a:Tensor):
|
|
after = state.uop.after(state.uop.shrink(((0, 2),)).store(Invalid))
|
|
return Tensor(after).contiguous() + a
|
|
out = f(Tensor([1., 1., 1., 1.], device="CPU").contiguous().realize())
|
|
np.testing.assert_allclose(out.numpy(), [11., 21., 31., 41.])
|
|
|
|
def test_custom_kernel_precompile_further_compute(self, multi=False, kernel_count:int=2):
|
|
devs = ("CPU:0", "CPU:1")
|
|
def my_kernel(C:UOp, A:UOp) -> UOp:
|
|
C, A = C.flatten(), A.flatten()
|
|
i = UOp.range(A.numel(), 0)
|
|
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="my_kernel"))
|
|
|
|
@function(precompile=True)
|
|
def f(a:Tensor):
|
|
c = Tensor.invalids(*a.uop.shard_shape, dtype=a.dtype, device=a.device)
|
|
if multi: c = Tensor(c.uop.unshard(a.uop.axis), device=a.device)
|
|
c = Tensor.custom_kernel(c, a, fxn=my_kernel)[0]
|
|
return c + 1
|
|
|
|
a = Tensor([1., 2., 3., 4.]).contiguous()
|
|
if multi: a = a.shard(devs, axis=0)
|
|
a.realize()
|
|
out = f(a)
|
|
GlobalCounters.reset()
|
|
out.realize()
|
|
assert_kernel_count(kernel_count)
|
|
np.testing.assert_allclose(out.numpy(), [3., 5., 7., 9.])
|
|
|
|
def test_custom_kernel_precompile_further_compute_multi(self): self.test_custom_kernel_precompile_further_compute(multi=True, kernel_count=4)
|
|
|
|
class TestFunctionGrad(unittest.TestCase):
|
|
def test_function_grad_ops(self, precompile=False, precompile_backward=False):
|
|
N = 64
|
|
x = Tensor.ones(N,N).contiguous()
|
|
w1 = Tensor.ones(N,N).contiguous()
|
|
w2 = Tensor.ones(N,N).contiguous()
|
|
w3 = Tensor.ones(N,N).contiguous()
|
|
ref = Tensor.ones(N,N).contiguous()
|
|
Tensor.realize(x, w1, w2, w3, ref)
|
|
@function(precompile=precompile, precompile_backward=precompile_backward)
|
|
def f(x, w1, w2, w3) -> tuple[Tensor, ...]:
|
|
p1 = x@w1
|
|
p2 = p1@w2
|
|
p3 = p2@w3
|
|
return p1, p2, p3, p3.contiguous()
|
|
ret = f(x, w1, w2, w3)[-1]
|
|
loss = (ret-ref).square().mean().backward()
|
|
print("RESET")
|
|
GlobalCounters.reset()
|
|
loss.realize(w1.grad, w2.grad, w3.grad)
|
|
print(GlobalCounters.global_ops, GlobalCounters.global_mem)
|
|
self.assertLessEqual(GlobalCounters.global_ops, 5000000)
|
|
def test_function_grad_ops_precompile(self): self.test_function_grad_ops(precompile=True)
|
|
def test_function_grad_ops_precompile_backward(self):
|
|
self.test_function_grad_ops(precompile=True, precompile_backward=True)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|