mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-09-08 20:26:14 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c9287199 | ||
|
|
4c64311bd2 | ||
|
|
6f306649d5 | ||
|
|
ae588e9be5 |
Binary file not shown.
+18
-1
@@ -150,6 +150,18 @@ A value \op{Call} is void: its \op{Sink} body stores to output \op{Param}s bound
|
||||
|
||||
\smallskip
|
||||
Assign is \op{Store} followed by \op{After}: write the value, then return the buffer with an ordering dependency.
|
||||
\op{After} orders consumers after its dependencies; it neither declares a write nor snapshots memory.
|
||||
In particular, \op{After}$(b, \op{Store}(d,v))$ returns $b$, not $v$, when $b$ and $d$ are disjoint.
|
||||
Views may share storage despite having different UOps. Differentiation follows the returned value:
|
||||
a matching unconditional full overwrite routes its gradient to the stored value; an unrelated write does not create a gradient path.
|
||||
Partial or uncertain aliased mutation gradients may be rejected.
|
||||
|
||||
\smallskip
|
||||
\textbf{Tensor scheduling contract.} Within a lazy Tensor schedule, reads retain their assignment dependencies.
|
||||
A read must follow those dependencies and precede other writes that would destroy the required contents.
|
||||
Lowering must preserve these requirements until accesses are ordered, even when arguments share storage.
|
||||
Unsatisfiable requirements raise rather than read overwritten contents. This is a frontend requirement, not snapshot semantics for \op{After}.
|
||||
An executed \texttt{clone()} preserves data in fresh storage; \texttt{contiguous()} need not allocate.
|
||||
|
||||
%% ============================================================
|
||||
\subsection*{{\color{elwyellow}Elementwise Ops} \normalfont\small--- all inputs same shape, output same shape, applied per-element}
|
||||
@@ -236,7 +248,7 @@ Ternary & $(P, A, B)$
|
||||
\op{Custom} & (args\ldots) & fmt & Inject custom code string into generated source. \\
|
||||
\op{AtomicAdd} & (idx, val) & --- & Atomic read-modify-write: \texttt{buf[idx] += val}. \\[4pt]
|
||||
\op{CustomFunction} & (meta\ldots) & name & Opaque device function (e.g.\ HW decode). Via \op{Call}. \\
|
||||
\op{Program} & (linear, source, binary) & --- & Compiled kernel: instructions, source, and machine code. \\
|
||||
\op{Program} & (sink, \ldots) & metadata? & Kernel through compilation stages. \\
|
||||
\op{Source} & () & str & Human-readable rendered source code. \\
|
||||
\op{Binary} & () & bytes & Compiled machine code. \\
|
||||
\bottomrule
|
||||
@@ -244,6 +256,11 @@ Ternary & $(P, A, B)$
|
||||
|
||||
\smallskip
|
||||
These ops are not part of the core specification and are subject to change.
|
||||
\op{Program} contains a \op{Sink}, followed progressively by \op{Linear}, \op{Source}, and \op{Binary}.
|
||||
Access analysis derives reads and writes from the memory operands of \op{Load}/\op{Store}, resolving \op{Param}s through \op{Call} arguments.
|
||||
Compilation records these sets in \texttt{ProgramInfo.ins/outs} as zero-based argument slots; a read-modify-write belongs in both.
|
||||
Listing a parameter, returning an \op{After}, or declaring a write does not establish full initialization.
|
||||
Opaque code without computed access information is unsupported by assignment scheduling; its effects must not be guessed from its argument list.
|
||||
|
||||
%% ============================================================
|
||||
\subsection*{Derived Properties}
|
||||
|
||||
+18
-23
@@ -40,10 +40,10 @@ class TestAssign(unittest.TestCase):
|
||||
def test_assign_copy(self):
|
||||
a = Tensor([1.,2,3], device="PYTHON")
|
||||
c = Tensor.empty(3).assign(a.to(None))
|
||||
# it should copy into the empty buffer
|
||||
# The creation copy has its own storage, independent of the assignment destination.
|
||||
GlobalCounters.reset()
|
||||
c.realize()
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
|
||||
def test_assign_slice(self):
|
||||
X = Tensor([1,2,3,4]).realize()
|
||||
@@ -619,7 +619,7 @@ class TestAssign(unittest.TestCase):
|
||||
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
|
||||
GlobalCounters.reset()
|
||||
base.assign(contig).realize()
|
||||
assert_kernel_count(4 if is_hcq2_device() else 2) # TODO: first copy is dead, could be 1
|
||||
assert_kernel_count(6 if is_hcq2_device() else 4) # TODO: first copy is dead
|
||||
self.assertEqual(base.tolist(), [1,4,3])
|
||||
|
||||
def test_nested_after_contiguous_store_no_init(self):
|
||||
@@ -629,7 +629,7 @@ class TestAssign(unittest.TestCase):
|
||||
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
|
||||
GlobalCounters.reset()
|
||||
base.assign(contig).realize()
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
self.assertEqual(base.tolist(), [1,4,3])
|
||||
|
||||
def test_assign_temporary_copy_reshape(self):
|
||||
@@ -637,7 +637,7 @@ class TestAssign(unittest.TestCase):
|
||||
c = Tensor.empty(2, 2).assign(a.to(None))
|
||||
GlobalCounters.reset()
|
||||
c.realize()
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
self.assertEqual(c.tolist(), [[1., 2], [3, 4]])
|
||||
|
||||
class TestAssignOrdering(unittest.TestCase):
|
||||
@@ -828,6 +828,13 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
b_np *= 0.9
|
||||
np.testing.assert_allclose(param.item(), p_np, atol=1e-5)
|
||||
|
||||
def test_after_store_to_other_buffer(self):
|
||||
x, state = Tensor([2.]).realize(), Tensor([0.]).realize()
|
||||
ordered = Tensor(x.uop.after(state.uop.store(x.uop * 3)))
|
||||
self.assertEqual((ordered + x).tolist(), [4.])
|
||||
self.assertEqual(state.tolist(), [6.])
|
||||
self.assertEqual(x.tolist(), [2.])
|
||||
|
||||
def test_war_reader_already_depends_on_write(self):
|
||||
x = Tensor([1.0]).contiguous().realize()
|
||||
y = Tensor([2.0]).contiguous().realize()
|
||||
@@ -835,12 +842,8 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
x.assign(x * 2)
|
||||
y.assign(y + x)
|
||||
z = y + x_expr
|
||||
Tensor.realize(x, y, z)
|
||||
try:
|
||||
np.testing.assert_allclose([x.item(), y.item(), z.item()], [2.0, 4.0, 15.0])
|
||||
except AssertionError:
|
||||
# TODO: broken now, x_expr reads x after the assign
|
||||
np.testing.assert_allclose([x.item(), y.item(), z.item()], [2.0, 4.0, 16.0])
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
Tensor.realize(x, y, z)
|
||||
|
||||
def test_war_multi_read_then_assign(self):
|
||||
devices = ("CPU:0", "CPU:1")
|
||||
@@ -875,12 +878,8 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
a.assign(b + 1) # a == 11
|
||||
v1 = a * 3 # reads 11 -> 33
|
||||
a.assign(b + 100) # a == 110
|
||||
out = (a + v1).numpy()
|
||||
try:
|
||||
np.testing.assert_allclose(out, 143)
|
||||
except AssertionError:
|
||||
# TODO: broken now, v1 reads a after the second assign
|
||||
np.testing.assert_allclose(out, 440)
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
(a + v1).numpy()
|
||||
|
||||
def test_two_reads_between_three_assigns(self):
|
||||
a = Tensor.zeros(4).realize()
|
||||
@@ -995,12 +994,8 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
x.assign(x+1)
|
||||
return y+x
|
||||
a = Tensor([1.]).realize()
|
||||
out = outer(a).item()
|
||||
try:
|
||||
self.assertEqual([out, a.item()], [7., 3.])
|
||||
except AssertionError:
|
||||
# TODO: broken now, the inner assign is run twice
|
||||
self.assertEqual([out, a.item()], [6., 4.])
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
outer(a).item()
|
||||
|
||||
class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
def test_copy(self):
|
||||
|
||||
@@ -105,6 +105,47 @@ def backward_gemm_custom(gradient:UOp, kernel:UOp) -> tuple[UOp, UOp]:
|
||||
# **** tests ****
|
||||
|
||||
class TestCustomKernel(unittest.TestCase):
|
||||
def test_readonly_after_args(self):
|
||||
for chained in (False, True):
|
||||
for corealize in (False, True):
|
||||
with self.subTest(chained=chained, corealize=corealize):
|
||||
x = Tensor([2.]).realize()
|
||||
a, x1 = Tensor.empty(1).custom_kernel(x, fxn=custom_add_one_kernel)
|
||||
b, x2 = Tensor.empty(1).custom_kernel(x1 if chained else x, fxn=custom_add_one_kernel)
|
||||
if corealize:
|
||||
y = x1 + b
|
||||
Tensor.realize(y, x2)
|
||||
self.assertEqual(y.tolist(), [5.])
|
||||
else:
|
||||
self.assertEqual((x1 + x2).tolist(), [4.])
|
||||
self.assertEqual(a.tolist(), [3.])
|
||||
self.assertEqual(b.tolist(), [3.])
|
||||
self.assertEqual(x.tolist(), [2.])
|
||||
|
||||
def test_aliased_args_different_sizes(self):
|
||||
def kernel(out:UOp, a:UOp, b:UOp):
|
||||
i = UOp.range(4, 0)
|
||||
return out[i].store(a[i] + b[0]).end(i).sink(arg=KernelInfo(name="aliased_sizes"))
|
||||
x = Tensor([1., 2., 3., 4.]).realize()
|
||||
out = Tensor.empty(4).custom_kernel(x, x[:1], fxn=kernel)[0]
|
||||
self.assertEqual(out.tolist(), [2., 3., 4., 5.])
|
||||
|
||||
def test_unindexed_access_before_assign(self):
|
||||
def kernel(out:UOp, x:UOp): return out.store(x + 1).sink(arg=KernelInfo(name="unindexed"))
|
||||
x = Tensor([2.]).realize()
|
||||
y = Tensor.empty(1).custom_kernel(x, fxn=kernel)[0]
|
||||
x.assign(x * 2)
|
||||
Tensor.realize(x, y)
|
||||
self.assertEqual(x.tolist(), [4.])
|
||||
self.assertEqual(y.tolist(), [3.])
|
||||
|
||||
def test_readonly_after_does_not_hide_write(self):
|
||||
x = Tensor([2.]).realize()
|
||||
_, before = Tensor.empty(1).custom_kernel(x, fxn=custom_add_one_kernel)
|
||||
x.assign(x * 2)
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
(before + x).realize()
|
||||
|
||||
def test_empty(self):
|
||||
a = Tensor.empty(1)
|
||||
a = Tensor.custom_kernel(a, fxn=lambda _: UOp.sink(arg=KernelInfo()))[0]
|
||||
|
||||
@@ -268,11 +268,6 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
self.helper_test_exception([], lambda: torch.meshgrid(x, indexing="bad"), lambda: xt.meshgrid(indexing="bad"), expected=RuntimeError)
|
||||
|
||||
def test_meshgrid_scalar(self):
|
||||
for indexing in ("ij", "xy"):
|
||||
with self.subTest(indexing=indexing):
|
||||
helper_test_op([()], lambda x: torch.meshgrid(x, indexing=indexing)[0], lambda x: x.meshgrid(indexing=indexing)[0])
|
||||
|
||||
def test_arange(self):
|
||||
helper_test_op([], lambda: torch.arange(10, dtype=torch.int32), lambda: Tensor.arange(10), forward_only=True)
|
||||
helper_test_op([], lambda: torch.arange(36, dtype=torch.int32), lambda: Tensor.arange(36), forward_only=True)
|
||||
@@ -1188,20 +1183,6 @@ class TestOps(unittest.TestCase):
|
||||
def test_small_cummax(self):
|
||||
helper_test_op([(10)], lambda x: torch.cummax(x, dim=0).values, lambda x: Tensor.cummax(x, axis=0)[0])
|
||||
helper_test_op([(10)], lambda x: torch.cummax(x, dim=0).indices.int(), lambda x: Tensor.cummax(x, axis=0)[1], forward_only=True)
|
||||
|
||||
def test_cumextrema_ties(self):
|
||||
for op in ("cummax", "cummin"):
|
||||
for axis in (0, 1, -1):
|
||||
for values in ([[2, 2, 1, 3, 3, 0, 0]] * 2, [[0, 0, 0]] * 2):
|
||||
with self.subTest(op=op, axis=axis, values=values):
|
||||
helper_test_op(None, lambda x: getattr(torch, op)(x, dim=axis).indices.int(),
|
||||
lambda x: getattr(x, op)(axis)[1], vals=[values], forward_only=True)
|
||||
|
||||
def test_cumextrema_ties_split(self):
|
||||
for op in ("cummax", "cummin"):
|
||||
helper_test_op(None, lambda x: getattr(torch, op)(x, dim=-1).indices.int(), lambda x: getattr(x, op)(-1)[1],
|
||||
vals=[[[2.0, 2.0, 1.0, 3.0, 3.0, 0.0, 0.0] * 100] * 2], forward_only=True)
|
||||
|
||||
@slow_test
|
||||
def test_simple_cummax(self):
|
||||
helper_test_op([(512)], lambda x: torch.cummax(x, dim=0).values, lambda x: Tensor.cummax(x, axis=0)[0])
|
||||
@@ -1668,10 +1649,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x: x.isclose(torch.tensor(1.0)), lambda x: x.isclose(1.0),
|
||||
vals=[[1.0, 1.0 + 1e-7, 2.0, math.inf, -math.inf, math.nan]], forward_only=True)
|
||||
|
||||
def test_isclose_overflow(self):
|
||||
helper_test_op(None, lambda x,y: x.isclose(y, rtol=3),
|
||||
vals=[[3e38, -3e38, 3e38, 0.0], [-3e38, 3e38, 3e38, 1.0]], forward_only=True)
|
||||
|
||||
def test_mean(self):
|
||||
helper_test_op([(3,4,5,6)], lambda x: x.mean())
|
||||
helper_test_op([()], lambda x: x.mean())
|
||||
@@ -1716,16 +1693,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(15, 25, 35)], lambda x: x.var(keepdim=True))
|
||||
helper_test_op([(15, 25, 35)], lambda x: x.var(0, keepdim=True, correction=0))
|
||||
|
||||
def test_var_std_integer(self):
|
||||
for op in ("var", "std"):
|
||||
for axis in (None, 0, 1):
|
||||
for correction in (0, 1):
|
||||
for keepdim in (False, True):
|
||||
with self.subTest(op=op, axis=axis, correction=correction, keepdim=keepdim):
|
||||
helper_test_op(None, lambda x: getattr(x.float(), op)(dim=axis, correction=correction, keepdim=keepdim),
|
||||
lambda x: getattr(x, op)(axis=axis, correction=correction, keepdim=keepdim),
|
||||
vals=[[[0, 1, 3], [1, 2, 4]]], forward_only=True)
|
||||
|
||||
@slow_test
|
||||
def test_std(self):
|
||||
helper_test_op([(15, 25, 35)], lambda x: x.std())
|
||||
@@ -1842,21 +1809,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x: torch.logcumsumexp(x, dim=0), lambda x: x.logcumsumexp(), atol=1e-7, grad_atol=1e-7, vals=[[0.0, 100.0]])
|
||||
helper_test_op(None, lambda x: torch.logcumsumexp(x, dim=0), lambda x: x.logcumsumexp(), vals=[[-math.inf, 0.0, 1.0]], forward_only=True)
|
||||
|
||||
def test_logcumsumexp_scalar_invalid_axis(self):
|
||||
for axis in (-2, 1):
|
||||
with self.subTest(axis=axis):
|
||||
self.helper_test_exception([()], lambda x: torch.logcumsumexp(x, dim=axis), lambda x: x.logcumsumexp(axis), expected=IndexError)
|
||||
|
||||
def test_logcumsumexp_empty(self):
|
||||
for shape, axis in (((0,), 0), ((2, 0, 3), 1), ((2, 0, 3), -1)):
|
||||
with self.subTest(shape=shape, axis=axis):
|
||||
helper_test_op([shape], lambda x: torch.logcumsumexp(x, dim=axis), lambda x: x.logcumsumexp(axis))
|
||||
|
||||
def test_logcumsumexp_nonfinite(self):
|
||||
for values in ([-math.inf, -math.inf], [0., math.inf, -math.inf], [0., math.nan, 1.]):
|
||||
with self.subTest(values=values):
|
||||
helper_test_op(None, lambda x: torch.logcumsumexp(x, dim=0), lambda x: x.logcumsumexp(), vals=[values], forward_only=True)
|
||||
|
||||
def test_sinh(self):
|
||||
helper_test_op([(45,65)], lambda x: x.sinh(), grad_atol=1e-6)
|
||||
# TODO: backward nan instead of inf
|
||||
@@ -2237,12 +2189,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(3,5)], lambda x: x.diagonal(offset=2)) # offset on rectangular
|
||||
self.helper_test_exception([(3,3)], lambda x: x.diagonal(dim1=0, dim2=0), expected=RuntimeError)
|
||||
|
||||
def test_diagonal_outside_matrix(self):
|
||||
for shape, dims in (((2, 3), (0, 1)), ((2, 3, 4), (-2, -1)), ((2, 3, 4), (2, 0))):
|
||||
for offset in (-10, -4, 4, 10):
|
||||
with self.subTest(shape=shape, dims=dims, offset=offset):
|
||||
helper_test_op([shape], lambda x: x.diagonal(offset=offset, dim1=dims[0], dim2=dims[1]))
|
||||
|
||||
def test_roll(self):
|
||||
helper_test_op([(2, 4)], lambda x: x.roll(1))
|
||||
helper_test_op([(2, 4)], lambda x: x.roll((1,)))
|
||||
@@ -3380,16 +3326,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(12,10)], lambda x: torch.nn.CrossEntropyLoss(label_smoothing=s)(x, torch.tensor(classes)),
|
||||
lambda x: x.sparse_categorical_crossentropy(Tensor(classes), label_smoothing=s))
|
||||
|
||||
def test_sparse_categorical_crossentropy_default_ignore_index(self):
|
||||
classes = [-1, 0, 2, -1]
|
||||
for reduction in ("none", "sum", "mean"):
|
||||
for smoothing in (0.0, 0.3, 1.0):
|
||||
with self.subTest(reduction=reduction, smoothing=smoothing):
|
||||
helper_test_op([(4, 3)],
|
||||
lambda x: torch.nn.functional.cross_entropy(x, torch.tensor(classes), ignore_index=-1,
|
||||
reduction=reduction, label_smoothing=smoothing),
|
||||
lambda x: x.sparse_categorical_crossentropy(Tensor(classes), reduction=reduction, label_smoothing=smoothing))
|
||||
|
||||
def test_nll_loss(self):
|
||||
target = np.random.randint(0, 10, (32,), dtype=np.int32).tolist()
|
||||
helper_test_op([(32,10)],
|
||||
@@ -3503,31 +3439,6 @@ class TestOps(unittest.TestCase):
|
||||
if not COMPILE_ONLY: assert t == -1
|
||||
|
||||
class TestOpsUint8(unittest.TestCase):
|
||||
def test_lerp_integer_end(self):
|
||||
for dtype in dtypes.ints:
|
||||
with self.subTest(dtype=dtype):
|
||||
actual = Tensor([[10], [100]], dtype=dtypes.uint8).lerp(Tensor([20, 20, 100], dtype=dtype), Tensor([0., 0.5, 1.]))
|
||||
self.assertEqual(actual.dtype, dtypes.uint8)
|
||||
actual.realize()
|
||||
if not COMPILE_ONLY: np.testing.assert_equal(actual.numpy(), [[10, 15, 100], [100, 60, 100]])
|
||||
|
||||
def test_lerp_float_end(self):
|
||||
helper_test_op(None, lambda x,y,w: x.float().lerp(y, w), lambda x,y,w: x.cast(dtypes.uint8).lerp(y, w),
|
||||
vals=[[[10], [100]], [20.5, 9.5, -5.5], [0., 0.5, 1.]], forward_only=True)
|
||||
|
||||
def test_interpolate_bilinear_full_range(self):
|
||||
for values in ([[0, 255]], [[255, 0]], [[1, 200]], [[0, 255], [255, 0]]):
|
||||
for size in ((1, 3), (5, 10)):
|
||||
for align_corners in (False, True):
|
||||
with self.subTest(values=values, size=size, align_corners=align_corners):
|
||||
image = torch.tensor([[values]], dtype=torch.uint8)
|
||||
expected = torch.nn.functional.interpolate(image, size=size, mode="bilinear", align_corners=align_corners)
|
||||
actual = Tensor(image.numpy()).interpolate(size, align_corners=align_corners)
|
||||
self.assertEqual(actual.dtype, dtypes.uint8)
|
||||
# Midpoints are exact; other weights can differ by one with 7-bit fixed-point coefficients.
|
||||
actual.realize()
|
||||
if not COMPILE_ONLY: np.testing.assert_allclose(actual.numpy(), expected.numpy(), rtol=0, atol=0 if size == (1, 3) else 1)
|
||||
|
||||
def test_cast(self):
|
||||
helper_test_op([(2,3,64,64)], lambda x: x.type(torch.uint8), lambda x: x.cast('uint8'), forward_only=True, low=0, high=255)
|
||||
|
||||
|
||||
@@ -737,6 +737,25 @@ class TestZeroShapeTensor(unittest.TestCase):
|
||||
np.testing.assert_allclose(a.numpy(), b.numpy())
|
||||
self.assertIsNot(a.uop.base.buffer, b.uop.base.buffer)
|
||||
|
||||
def test_clone_unrealized_copy_does_not_alias(self):
|
||||
for realize_clone in (False, True):
|
||||
with self.subTest(realize_clone=realize_clone):
|
||||
a = Tensor([2.])
|
||||
b = a.clone()
|
||||
if realize_clone: b.realize()
|
||||
b.assign(7.).realize()
|
||||
self.assertEqual(a.tolist(), [2.])
|
||||
self.assertEqual(b.tolist(), [7.])
|
||||
self.assertIsNot(a.uop.base.buffer, b.uop.base.buffer)
|
||||
|
||||
def test_clone_preserves_creation_copy(self):
|
||||
source = Tensor([2.], device="PYTHON")
|
||||
copied = source.to("CPU")
|
||||
cloned = copied.clone().realize()
|
||||
source.assign(7.).realize()
|
||||
self.assertEqual(copied.tolist(), [2.])
|
||||
self.assertEqual(cloned.tolist(), [2.])
|
||||
|
||||
def test_clone_deviceless_const(self):
|
||||
t = Tensor(UOp.const(2.0).cast(dtypes.float)).clone()
|
||||
np.testing.assert_equal(t.numpy(), 2.0)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, Context, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
|
||||
|
||||
|
||||
class TestCallAccess(unittest.TestCase):
|
||||
def test_computed_reads_writes_and_unused_arguments(self):
|
||||
out, x, unused = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(3))
|
||||
body = out.store(x + 1).sink(arg=KernelInfo())
|
||||
self.assertEqual(body.call(out, x, unused).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_computed_read_modify_write(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
body = out.store(out + x).sink(arg=KernelInfo())
|
||||
self.assertEqual(body.call(out, x).call_access(), ((out, x), (out,)))
|
||||
|
||||
def test_computed_empty_effects(self):
|
||||
x = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
self.assertEqual(UOp.sink(x, arg=KernelInfo()).call(x).call_access(), ((), ()))
|
||||
|
||||
def test_computed_program_accesses(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
sink = out.store(x.load()).sink(arg=KernelInfo())
|
||||
program = UOp(Ops.PROGRAM, src=(sink,), arg=ProgramInfo.from_sink(sink))
|
||||
self.assertEqual(program.call(out, x).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_nested_linear_parameter_scopes(self):
|
||||
a, b, c = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(3))
|
||||
inner = a.store(b + 1).sink(arg=KernelInfo()).call(b, a)
|
||||
body = UOp(Ops.LINEAR, src=(inner,))
|
||||
self.assertEqual(body.call(a, b, c).call_access(), ((a,), (b,)))
|
||||
|
||||
def test_copy_accesses(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
self.assertEqual(UOp(Ops.COPY, src=(x,), arg=out.device).call(out, x).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_unknown_opaque_accesses_reject(self):
|
||||
x = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
bodies = (UOp(Ops.PROGRAM, src=(UOp.sink(x),)),
|
||||
UOp(Ops.CUSTOM, src=(x,), arg=("", dtypes.void)).sink(arg=KernelInfo()))
|
||||
for body in bodies:
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot compute accesses"): body.call(x).call_access()
|
||||
|
||||
@Context(DEV="CPU")
|
||||
def test_unknown_effects_do_not_replace_tensors_on_failure(self):
|
||||
def kernel(x): return UOp(Ops.PROGRAM, src=(UOp.sink(x, arg=KernelInfo()),))
|
||||
x = Tensor([2.]).realize().custom_kernel(fxn=kernel)[0]
|
||||
before = x.uop
|
||||
for _ in range(2):
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot compute accesses"): x.realize()
|
||||
self.assertIs(x.uop, before)
|
||||
|
||||
def test_bad_access_slots(self):
|
||||
arg = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
for slot in (-1, 1):
|
||||
p = UOp(Ops.PROGRAM, src=(UOp.sink(arg),), arg=ProgramInfo(globals=(0,), ins=(slot,), outs=()))
|
||||
with self.assertRaisesRegex(RuntimeError, "invalid CALL access slot"): p.call(arg, arg).call_access()
|
||||
|
||||
def test_compiled_writable_alias_rejects(self):
|
||||
a, b = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
sink = a.store(b.load()).sink(arg=KernelInfo())
|
||||
program = UOp(Ops.PROGRAM, src=(sink,), arg=ProgramInfo.from_sink(sink))
|
||||
with self.assertRaisesRegex(RuntimeError, "aliased opaque"): program.call(a, a).call_access()
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -151,17 +151,6 @@ class TestTypeSpec(unittest.TestCase):
|
||||
_assert_eq(Tensor.arange(5.0, 3.0), dtypes.default_float, np.arange(5.0, 3.0))
|
||||
|
||||
class TestAutoCastType(unittest.TestCase):
|
||||
@unittest.skipUnless(dtypes.float64 in supported_dtypes, "need float64")
|
||||
def test_linspace_float64_precision(self):
|
||||
for start, stop in ((1., 1.+1e-8), (1.+1e-8, 1.), (1e10, 1e10+1)):
|
||||
with self.subTest(start=start, stop=stop):
|
||||
out = Tensor.linspace(start, stop, 3, dtype=dtypes.float64)
|
||||
self.assertEqual(out.dtype, dtypes.float64)
|
||||
np.testing.assert_allclose(out.numpy(), np.linspace(start, stop, 3), rtol=1e-15, atol=0)
|
||||
with Context(DEFAULT_FLOAT=dtypes.float64):
|
||||
out = Tensor.linspace(10**10, 10**10+2, 3, dtype=dtypes.int64)
|
||||
np.testing.assert_array_equal(out.numpy(), [10**10, 10**10+1, 10**10+2])
|
||||
|
||||
def test_int_sqrt(self):
|
||||
_assert_eq(Tensor([1, 4, 9, 16]).sqrt(), dtypes.default_float, [1, 2, 3, 4])
|
||||
|
||||
@@ -233,13 +222,6 @@ class TestAutoCastType(unittest.TestCase):
|
||||
t.square().mean().backward()
|
||||
np.testing.assert_allclose(t.grad.numpy().flatten(), [60000 * 2 / (N*N)] * N*N)
|
||||
|
||||
def test_var_integer_fractional(self):
|
||||
for dtype in [*dtype_ints, dtypes.bool]:
|
||||
with self.subTest(dtype=dtype):
|
||||
out = Tensor([0, 1], dtype=dtype).var()
|
||||
self.assertEqual(out.dtype, dtypes.float32)
|
||||
np.testing.assert_allclose(out.numpy(), 0.5)
|
||||
|
||||
@unittest.skipUnless(dtypes.half in supported_dtypes, "need half")
|
||||
def test_var_half_precision_large_n(self):
|
||||
# the element count (70000) exceeds half max (65504): the denominator must not be materialized in half
|
||||
|
||||
@@ -88,6 +88,72 @@ class TestTensorGradient(unittest.TestCase):
|
||||
np.testing.assert_allclose(x.grad.numpy(), [2.0, 2.0, 2.0, 2.0]) # gradient flows through clone
|
||||
np.testing.assert_allclose(base.grad.numpy(), [0.0, 0.0, 0.0, 0.0]) # ...but detach blocks it from base
|
||||
|
||||
def test_gradient_through_single_assign(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
y.assign(y.square())
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [4., 6.])
|
||||
|
||||
def test_gradient_through_assign_requires_old_versions(self):
|
||||
for count in (2, 3):
|
||||
with self.subTest(count=count):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
for _ in range(count): y.assign(y.square())
|
||||
g = y.sum().gradient(x)[0]
|
||||
before = (x.uop, y.uop, g.uop)
|
||||
# Reject incompatible versions, including on retry: failed scheduling must not replace them with buffers.
|
||||
for _ in range(2):
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"): g.realize()
|
||||
self.assertEqual((x.uop, y.uop, g.uop), before)
|
||||
|
||||
def test_gradient_through_assign_with_snapshots(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
for _ in range(2): y.assign(y.clone().square())
|
||||
g = y.sum().gradient(x)[0]
|
||||
gg = g.sum().gradient(x)[0]
|
||||
Tensor.realize(g, gg)
|
||||
self.assertEqual(g.tolist(), [32., 108.])
|
||||
self.assertEqual(gg.tolist(), [48., 108.])
|
||||
|
||||
def test_gradient_after_unrelated_store(self):
|
||||
x, v, dst = Tensor([2.]).realize(), Tensor([3.]).realize(), Tensor.empty(1)
|
||||
y = Tensor(x.uop.after(dst.uop.store(v.uop)))
|
||||
self.assertEqual([g.tolist() for g in y.sum().gradient(x, v)], [[1.], [0.]])
|
||||
self.assertEqual(y.tolist(), [2.])
|
||||
self.assertEqual(dst.tolist(), [3.])
|
||||
|
||||
def test_gradient_after_multiple_unrelated_stores(self):
|
||||
x, a, b = Tensor([2.]).realize(), Tensor.empty(1), Tensor.empty(1)
|
||||
y = Tensor(x.uop.after(a.uop.store(x.uop * 3), b.uop.store(x.uop * 4)))
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
def test_gradient_after_readonly_call(self):
|
||||
x = Tensor([2.]).realize()
|
||||
def kernel(dst, src): return dst.store(src * 3).sink(arg=KernelInfo())
|
||||
for grad_fxn in (None, lambda g, k: (None, g * 3)):
|
||||
_, unchanged = Tensor.empty(1).custom_kernel(x, fxn=kernel, grad_fxn=grad_fxn)
|
||||
self.assertEqual(unchanged.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
def test_gradient_after_unrelated_call(self):
|
||||
x, v, dst = Tensor([2.]).realize(), Tensor([3.]).realize(), Tensor.empty(1)
|
||||
p, q = dst.uop.param_like(0), v.uop.param_like(1)
|
||||
call = p.store(q * 3).sink(arg=KernelInfo()).call(dst.uop, v.uop, grad_fxn=lambda g, k: (None, g * 3))
|
||||
y = Tensor(x.uop.after(call))
|
||||
self.assertEqual([g.tolist() for g in y.sum().gradient(x, v)], [[1.], [0.]])
|
||||
|
||||
def test_gradient_after_aliased_store_view_rejects(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = Tensor(x.uop.after(x.uop.shrink(((0, 1),)).store(4.)))
|
||||
with self.assertRaisesRegex(RuntimeError, "aliased write"): y.sum().gradient(x)
|
||||
|
||||
def test_gradient_after_duplicate_call_output_rejects(self):
|
||||
x = Tensor([2.]).realize()
|
||||
def kernel(a, b): return a.store(b * 2).sink(arg=KernelInfo())
|
||||
y = x.custom_kernel(x, fxn=kernel, grad_fxn=lambda g, k: (g, g))[0]
|
||||
with self.assertRaisesRegex(RuntimeError, "ambiguous CALL"): y.sum().gradient(x)
|
||||
|
||||
def test_setitem_on_grad_used_tensor_raises(self):
|
||||
x = Tensor([1.0, 2.0, 3.0, 4.0]).realize()
|
||||
_ = (x * 2.0).sum()
|
||||
@@ -136,6 +202,21 @@ class TestTensorGradient(unittest.TestCase):
|
||||
self.assertIsNone(w.grad)
|
||||
|
||||
class TestMultiOutputGradient(unittest.TestCase):
|
||||
def test_custom_kernel_inplace_gradient(self):
|
||||
def double(x:UOp): return x[0].store(x[0]*2).sink(arg=KernelInfo(name="double_inplace"))
|
||||
def backward(g:UOp, call:UOp): return (g*2,)
|
||||
x = Tensor([2.]).realize()
|
||||
y = x.custom_kernel(fxn=double, grad_fxn=backward)[0]
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [2.])
|
||||
self.assertEqual(y.tolist(), [4.])
|
||||
|
||||
def test_custom_kernel_unchanged_output_gradient(self):
|
||||
def noop(x:UOp): return x[0].store(x[0]).sink(arg=KernelInfo(name="identity"))
|
||||
def backward(g:UOp, call:UOp): return (g,)
|
||||
x = Tensor([2.]).realize()
|
||||
y = x.custom_kernel(fxn=noop, grad_fxn=backward)[0]
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
@staticmethod
|
||||
def addmul_kernel(C:UOp, D:UOp, A:UOp, B:UOp) -> UOp:
|
||||
C, D, A, B = C.flatten(), D.flatten(), A.flatten(), B.flatten()
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import unittest
|
||||
from collections import OrderedDict, namedtuple
|
||||
from types import SimpleNamespace
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.nn.state import get_parameters, get_state_dict, load_state_dict
|
||||
|
||||
|
||||
class TestStateDict(unittest.TestCase):
|
||||
def test_container_subclasses(self):
|
||||
class TensorDict(dict): pass
|
||||
class TensorList(list): pass
|
||||
class TensorTuple(tuple): pass
|
||||
weight = Tensor([1., 2.])
|
||||
for container, key in ((TensorDict(weight=weight), "weight"), (TensorList([weight]), "0"), (TensorTuple([weight]), "0")):
|
||||
with self.subTest(container=type(container).__name__):
|
||||
container.description = "model weights"
|
||||
model = SimpleNamespace(layers=container)
|
||||
state = get_state_dict(model)
|
||||
self.assertEqual(list(state), [f"layers.{key}"])
|
||||
self.assertIs(state[f"layers.{key}"], weight)
|
||||
params = get_parameters(model)
|
||||
self.assertEqual(len(params), 1)
|
||||
self.assertIs(params[0], weight)
|
||||
|
||||
def test_namedtuple_and_ordered_dict(self):
|
||||
first, second = Tensor([1.]), Tensor([2.])
|
||||
pair = namedtuple("Pair", ["first", "second"])(first, second)
|
||||
state = get_state_dict(OrderedDict(pair=pair))
|
||||
self.assertEqual(list(state), ["pair.first", "pair.second"])
|
||||
self.assertIs(state["pair.first"], first)
|
||||
self.assertIs(state["pair.second"], second)
|
||||
|
||||
def test_load_container_subclass(self):
|
||||
class TensorDict(dict): pass
|
||||
weight = Tensor([1., 2.])
|
||||
model = TensorDict(weight=weight)
|
||||
loaded = load_state_dict(model, {"weight": Tensor([3., 4.])}, verbose=False)
|
||||
self.assertEqual(len(loaded), 1)
|
||||
self.assertIs(loaded[0], weight)
|
||||
self.assertEqual(weight.tolist(), [3., 4.])
|
||||
|
||||
def test_container_tensor_attributes(self):
|
||||
class TensorDict(dict): pass
|
||||
class TensorList(list): pass
|
||||
class TensorTuple(tuple): pass
|
||||
for container_type in (TensorDict, TensorList, TensorTuple):
|
||||
with self.subTest(container=container_type.__name__):
|
||||
model = container_type()
|
||||
model.weight = Tensor([1., 2.])
|
||||
state = get_state_dict(model)
|
||||
self.assertEqual(list(state), ["weight"])
|
||||
self.assertIs(state["weight"], model.weight)
|
||||
params = get_parameters(model)
|
||||
self.assertEqual(len(params), 1)
|
||||
self.assertIs(params[0], model.weight)
|
||||
loaded = load_state_dict(model, {"weight": Tensor([3., 4.])}, verbose=False)
|
||||
self.assertEqual(len(loaded), 1)
|
||||
self.assertIs(loaded[0], model.weight)
|
||||
self.assertEqual(model.weight.tolist(), [3., 4.])
|
||||
|
||||
def test_container_contents_and_attributes(self):
|
||||
class TensorDict(dict): pass
|
||||
class TensorList(list): pass
|
||||
class TensorTuple(tuple): pass
|
||||
item, weight = Tensor([1.]), Tensor([2.])
|
||||
for model, key in ((TensorDict(item=item), "item"), (TensorList([item]), "0"), (TensorTuple([item]), "0")):
|
||||
with self.subTest(container=type(model).__name__):
|
||||
model.weight = weight
|
||||
state = get_state_dict(model, prefix="model.")
|
||||
self.assertEqual(list(state), [f"model.{key}", "model.weight"])
|
||||
self.assertIs(state[f"model.{key}"], item)
|
||||
self.assertIs(state["model.weight"], weight)
|
||||
|
||||
def test_container_attribute_precedence(self):
|
||||
class TensorDict(dict): pass
|
||||
model = TensorDict(weight=Tensor([1.]))
|
||||
model.weight = Tensor([2.])
|
||||
self.assertIs(get_state_dict(model)["weight"], model.weight)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -647,9 +647,10 @@ class ElementwiseMixin(CreationMixin):
|
||||
```
|
||||
"""
|
||||
other = self.ufix(other)
|
||||
error = (self - other).abs()
|
||||
is_finite_close = error.isfinite() & (error <= atol + rtol * other.abs())
|
||||
return self.eq(other) | is_finite_close | (self.isnan() & other.isnan() & equal_nan)
|
||||
is_finite_close = self.isfinite() & other.isfinite() & ((self - other).abs() <= atol + rtol * other.abs())
|
||||
is_infinite_close = (self.isinf() | other.isinf()) & self.eq(other)
|
||||
is_nan_close = (self.isnan() & other.isnan()) & equal_nan
|
||||
return is_finite_close | is_infinite_close | is_nan_close
|
||||
|
||||
def ceil(self) -> Self:
|
||||
"""
|
||||
@@ -1086,7 +1087,7 @@ class ElementwiseMixin(CreationMixin):
|
||||
print(Tensor([1., 2., 3.]).lerp(Tensor([4., 5., 6.]), 0.5).numpy())
|
||||
```
|
||||
"""
|
||||
if self.dtype == dtypes.uint8 and not end.is_floating_point() and not isinstance(weight, ConstType):
|
||||
weight_int = (weight * 128 + 0.5).cast(dtypes.int32) # 7 fractional bits
|
||||
return ((self * (128 - weight_int) + end.cast(dtypes.int32) * weight_int + 64) >> 7).cast(dtypes.uint8)
|
||||
if self.dtype == dtypes.uint8 and not isinstance(weight, ConstType):
|
||||
w_i = (weight * (1<<(W_PREC:=7)) + 0.5).cast(dtypes.int16)
|
||||
return (self+(((end - self).cast(dtypes.int8) * w_i + (1<<W_PREC-1)).cast(dtypes.uint16) >> W_PREC)).cast(dtypes.uint8)
|
||||
return self + (end - self) * weight
|
||||
|
||||
@@ -3,6 +3,7 @@ import math, dataclasses
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
|
||||
from tinygrad.helpers import argsort
|
||||
from tinygrad.dtype import sum_acc_dtype
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.function import renumber_invalid_outputs
|
||||
|
||||
def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
|
||||
@@ -64,6 +65,25 @@ 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 after_gradient(ctx:UOp, ret:UOp):
|
||||
value, *deps = ret.src
|
||||
if len(deps) == 1:
|
||||
dep = deps[0]
|
||||
if dep.op is Ops.STORE and len(dep.src) == 2 and value is dep.src[0]: return (None, ctx)
|
||||
if dep.op is Ops.CALL and (value.unsharded_base.is_unbound or value in dep.call_access()[1]):
|
||||
if dep.src[1:].count(value) != 1: raise RuntimeError("ambiguous CALL output gradient")
|
||||
return (None, UOp.sink(*(ctx if a is value else UOp(Ops.NOOP) for a in dep.src[1:])))
|
||||
for dep in deps:
|
||||
if dep.op is Ops.STORE: writes = dep.src[:1]
|
||||
elif dep.op is Ops.CALL: _, writes = dep.call_access()
|
||||
else: raise RuntimeError(f"gradient through {dep.op} ordering is unsupported")
|
||||
for w in writes:
|
||||
a, b = (u.storage_base.arg.buffer if u.storage_base.op is Ops.BUFFER else None for u in (value, w))
|
||||
if not isinstance(a, Buffer) or not isinstance(b, Buffer) or a.base is b.base or \
|
||||
any(buf.base.options is not None and buf.base.options.external_ptr is not None for buf in (a, b)):
|
||||
raise RuntimeError("gradient through an aliased write is unsupported")
|
||||
return (ctx,) + (None,)*len(deps)
|
||||
|
||||
# ctx is grad_output
|
||||
pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="ret"), lambda ctx, ret: (ctx.cast(ret.src[0].dtype),)),
|
||||
@@ -94,10 +114,7 @@ pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.COPY, name="ret"), lambda ctx, ret: (ctx.copy_to_device(ret.src[0].device),)),
|
||||
(UPat(Ops.UNSHARD, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
|
||||
(UPat(Ops.SINK), lambda ctx: ctx.src),
|
||||
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
|
||||
(ctx, UOp.sink(*([ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1)])))),
|
||||
# clone/assign gradient passes through to val
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
|
||||
(UPat(Ops.AFTER, name="ret"), after_gradient),
|
||||
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
|
||||
# there's no gradient for bitcast
|
||||
(UPat(Ops.BITCAST), lambda: (None,)),
|
||||
|
||||
@@ -493,8 +493,9 @@ class MovementMixin:
|
||||
```
|
||||
"""
|
||||
if indexing not in ("ij", "xy"): raise RuntimeError(f'indexing must be in ("ij", "xy"), got {indexing}')
|
||||
basis = tuple(range(len(args)+1)) if indexing == "ij" or not args else (1, 0) + tuple(range(2, len(args)+1))
|
||||
tensors = tuple(t.reshape((-1,) + (1,)*(len(args) - i)) for i,t in zip(basis, (self, *args)))
|
||||
if len(tensors:=(self, *args)) == 1: return tensors
|
||||
basis = tuple(range(len(tensors))) if indexing == "ij" else (1, 0) + tuple(range(2, len(tensors)))
|
||||
tensors = tuple(t.reshape((-1,) + (1,)*(len(args) - i)) for i,t in zip(basis, tensors))
|
||||
output_shape = _broadcast_shape(*(t.shape for t in tensors))
|
||||
return tuple(t._broadcast_to(output_shape) for t in tensors)
|
||||
|
||||
@@ -527,8 +528,8 @@ class MovementMixin:
|
||||
"""
|
||||
if (dim1:=self._resolve_dim(dim1)) == (dim2:=self._resolve_dim(dim2)): raise RuntimeError("dim1 and dim2 cannot be the same dimension")
|
||||
x = self.permute(*[i for i in range(self.ndim) if i != dim1 and i != dim2], dim1, dim2)
|
||||
if offset >= 0: x = x.shrink((None,)*(x.ndim-1) + ((min(offset, x.shape[-1]), x.shape[-1]),))
|
||||
else: x = x.shrink((None,)*(x.ndim-2) + ((min(-offset, x.shape[-2]), x.shape[-2]), None))
|
||||
if offset >= 0: x = x.shrink(tuple(None for _ in x.shape[:-1]) + ((offset, x.shape[-1]),))
|
||||
else: x = x.shrink(tuple(None for _ in x.shape[:-2]) + ((-offset, x.shape[-2]), None))
|
||||
if (d := min(int(x.shape[-2]), int(x.shape[-1]))) <= 0: return x.reshape(*x.shape[:-2], 0)
|
||||
nones, x = tuple(None for _ in x.shape[:-2]), x.shrink_to(tuple(None for _ in x.shape[:-2]) + (d, d))
|
||||
return x.flatten(-2).pad_to(nones+(d*(d+1),)).unflatten(-1, (d, d+1)).shrink_to(nones+(None, 1)).squeeze(-1)
|
||||
|
||||
+19
-15
@@ -206,7 +206,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
if steps < 0: raise ValueError("number of steps must be non-negative")
|
||||
if (dtype := to_dtype(dtype or dtypes.default_float)) == dtypes.bool: raise ValueError("linspace with bool dtype is not supported")
|
||||
if steps == 1: return cls.full((1,), start, dtype=dtype, buffer=False)
|
||||
return (start + cls.arange(steps, dtype=least_upper_dtype(dtype, dtypes.default_float)) * ((stop - start) / (steps - 1))).cast(dtype)
|
||||
return (start + cls.arange(steps, dtype=dtypes.default_float) * ((stop - start) / (steps - 1))).cast(dtype)
|
||||
|
||||
@classmethod
|
||||
def eye(cls, n:int, m:int|None=None, dtype:DTypeLike|None=None) -> Self:
|
||||
@@ -542,10 +542,11 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
print(t.var(axis=1).numpy())
|
||||
```
|
||||
"""
|
||||
output_dtype = self.dtype if dtypes.is_float(self.dtype) else dtypes.float32
|
||||
squares = (self - self.mean(axis=axis, keepdim=True)).square()
|
||||
n = prod([si for si, so in zip(self.shape, squares.sum(axis=axis, keepdim=True).shape) if resolve(si != so)])
|
||||
numerator = squares.sum(axis=axis, keepdim=keepdim, dtype=sum_acc_dtype(squares.commit_dtype()))
|
||||
return numerator.div(smax(n - correction, 0)).cast(squares.dtype)
|
||||
numerator = squares.cast(sum_acc_dtype(self.commit_dtype())).sum(axis=axis, keepdim=keepdim)
|
||||
return numerator.div(smax(n - correction, 0)).cast(output_dtype)
|
||||
|
||||
def var_mean(self, axis:int|Sequence[int]|None=None, keepdim=False, correction=1) -> tuple[Self, Self]:
|
||||
"""
|
||||
@@ -806,10 +807,11 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
```
|
||||
"""
|
||||
if self.ndim == 0: return self._split_cumalu(axis, Ops.MAX), type(self).zeros(self.shape, dtype=dtypes.int32, buffer=False)
|
||||
values = self._split_cumalu(axis, Ops.MAX)
|
||||
# Record the latest index matching the running maximum, then carry it forward.
|
||||
idx = self.eq(values).transpose(axis, -1) * type(self).arange(self.shape[axis], dtype=dtypes.int32)
|
||||
return values, idx._split_cumalu(-1, Ops.MAX).transpose(-1, axis)
|
||||
values, n = self._split_cumalu(axis, Ops.MAX), int(self.shape[axis])
|
||||
x, values_t = self.transpose(axis, -1), values.transpose(axis, -1)
|
||||
match = x.unsqueeze(-1).eq(values_t.unsqueeze(-2)) * self._tri(n, n)
|
||||
idx = (-(match * type(self).arange(n, 0, -1).reshape(n, 1)).max(-2) + n).cast(dtypes.int32)
|
||||
return values, idx.transpose(-1, axis)
|
||||
|
||||
def cummin(self, axis:int=0) -> tuple[Self, Self]:
|
||||
"""
|
||||
@@ -849,12 +851,14 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
print(t.logcumsumexp(axis=1).numpy())
|
||||
```
|
||||
"""
|
||||
axis = self._resolve_dim(axis)
|
||||
if self.ndim == 0: return self
|
||||
x = self.transpose(axis, -1)
|
||||
mask = self._tri(x.shape[-1], x.shape[-1], 1)
|
||||
prefixes = mask.where(-math.inf, x.unsqueeze(-2))
|
||||
return prefixes.logsumexp(-1).transpose(-1, axis)
|
||||
last_dim_size = x.shape[-1]
|
||||
x_unsqueezed = x.unsqueeze(-2)
|
||||
x_cummax = (mx:=x.cummax(-1)[0].detach()).isfinite().where(mx, 0)
|
||||
mask = self._tri(last_dim_size, last_dim_size, 1).logical_not()
|
||||
ret = mask.where(x_unsqueezed - x_cummax.unsqueeze(-1), self.dtype.min).exp().sum(-1).log() + x_cummax
|
||||
return ret.transpose(-1, axis)
|
||||
|
||||
def argmax(self, axis=None, keepdim=False) -> Self:
|
||||
"""
|
||||
@@ -1734,10 +1738,10 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
if Y.device is not None and self.device is not None and Y.device != self.device:
|
||||
raise RuntimeError(f"expected Y and self on the same device, {Y.device=}, {self.device=}")
|
||||
log_probs = self.log_softmax()
|
||||
loss_mask = Y.ne(ignore_index)
|
||||
y = Y.unsqueeze(-1)._one_hot_along_dim(self.shape[-1], dim=-1)
|
||||
smoothing = label_smoothing * log_probs.mean(-1)
|
||||
unreduced = ((1 - label_smoothing) * (log_probs * y).sum(-1) + smoothing) * loss_mask
|
||||
loss_mask = Y.ne(ignore_index) if ignore_index != -1 else Y.const_like(True, dtypes.bool)
|
||||
y = Y.unsqueeze(-1)._one_hot_along_dim(self.shape[-1], dim=-1) * loss_mask.unsqueeze(-1)
|
||||
smoothing = label_smoothing * (log_probs.mean(-1) * loss_mask)
|
||||
unreduced = ((1 - label_smoothing) * (log_probs * y).sum(-1) + smoothing)
|
||||
return -unreduced.sum() / loss_mask.sum() if reduction == "mean" else -unreduced._do_reduction(reduction)
|
||||
|
||||
def cross_entropy(self, Y:Self, reduction:ReductionStr="mean", label_smoothing:float=0.0) -> Self:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json, pathlib, struct, functools, io, zlib
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Callable, BinaryIO, Iterable, cast
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
@@ -101,12 +102,13 @@ def get_state_dict(obj, prefix:str='', tensor_type=Tensor) -> dict[str, Tensor]:
|
||||
"""
|
||||
if isinstance(obj, tensor_type): return {prefix.strip('.'):obj}
|
||||
if hasattr(obj, '_asdict'): return get_state_dict(obj._asdict(), prefix, tensor_type) # namedtuple
|
||||
if isinstance(obj, OrderedDict): return get_state_dict(dict(obj), prefix, tensor_type)
|
||||
if hasattr(obj, '__dict__'): return get_state_dict(obj.__dict__, prefix, tensor_type)
|
||||
state_dict = {}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
for i,x in enumerate(obj): state_dict.update(get_state_dict(x, f"{prefix}{str(i)}.", tensor_type))
|
||||
elif isinstance(obj, dict):
|
||||
for k,v in obj.items(): state_dict.update(get_state_dict(v, f"{prefix}{str(k)}.", tensor_type))
|
||||
if hasattr(obj, '__dict__'): state_dict.update(get_state_dict(obj.__dict__, prefix, tensor_type))
|
||||
return state_dict
|
||||
|
||||
def get_parameters(obj) -> list[Tensor]:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import time, inspect
|
||||
import time, inspect, dataclasses
|
||||
from collections import deque
|
||||
from tinygrad.uop.ops import UOp, Ops, UOpMetaClass, rewrite_group, graph_rewrite, gate_kernel_sink, KernelInfo
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, partition, dedup
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, dedup
|
||||
|
||||
# **** schedule linearizer
|
||||
|
||||
@@ -11,67 +11,60 @@ def _unwrap_src(s: UOp) -> UOp:
|
||||
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK}: s = s.src[0]
|
||||
return s
|
||||
|
||||
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states
|
||||
# unwrap per-device buffer arguments without dropping their ordering dependencies
|
||||
def _states(s: UOp) -> list[UOp]:
|
||||
s = _unwrap_src(s)
|
||||
if s.op in {Ops.MSELECT, Ops.MSTACK}: return [st for ss in s.src for st in _states(ss)]
|
||||
assert s.op in {Ops.AFTER, Ops.BUFFER, Ops.PARAM}, f"input to kernel must resolve to a buffer state, not {s.op}"
|
||||
return [s]
|
||||
|
||||
def _split_after(after: UOp) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]:
|
||||
kernels, remaining = partition(after.src[1:], lambda s: s.op in {Ops.CALL, Ops.END})
|
||||
deps, remaining = partition(remaining, lambda s: s.op is Ops.AFTER)
|
||||
if invalid := [s for s in remaining if s.op is not Ops.STORE]:
|
||||
raise AssertionError(f"AFTER source should be CALL, END, STORE, or AFTER, not {invalid[0].op}")
|
||||
return tuple(kernels), tuple(deps)
|
||||
|
||||
def create_schedule(sched_sink:UOp) -> UOp:
|
||||
with cpu_profile(TracingKey("toposort sched_sink")):
|
||||
# build kernel dependency graph: edges from producer kernel to consumer kernels
|
||||
afters = [u for u in sched_sink.toposort(gate_kernel_sink) if u.op is Ops.AFTER]
|
||||
kernels = dict.fromkeys(k for u in afters for k in u.src[1:] if k.op in {Ops.CALL, Ops.END})
|
||||
dependencies: dict[UOp, set[UOp]] = {}
|
||||
writes: dict[UOp, set[UOp]] = {}
|
||||
reads: list[tuple[UOp, UOp]] = []
|
||||
ancestors: dict[UOp, set[UOp]] = {}
|
||||
for k in kernels:
|
||||
call = k.src[0] if k.op is Ops.END else k
|
||||
states = [st for s in call.src[1:] for st in _states(s)]
|
||||
for st in states:
|
||||
if st not in ancestors: ancestors[st] = kernels.keys() & st.toposort(enter_calls=False).keys()
|
||||
# AFTER supplies ordering dependencies, not evidence that its returned buffer was written.
|
||||
dependencies[k] = set().union(*(ancestors[st] for st in states))
|
||||
read_args, write_args = call.call_access()
|
||||
reads += [(k, st) for s in read_args for st in _states(s)]
|
||||
for s in write_args:
|
||||
for st in _states(s): writes.setdefault(st.buf_uop, set()).add(k)
|
||||
for u in afters:
|
||||
for dep in (s for s in u.src[1:] if s.op is Ops.AFTER):
|
||||
for k in (s for s in u.src[1:] if s in kernels):
|
||||
dependencies[k].update(kernels.keys() & dep.toposort(enter_calls=False).keys() - {k})
|
||||
# Tensor reads require the contents preceding writes absent from their argument ancestry (not an AFTER property).
|
||||
for k, st in reads:
|
||||
for writer in writes.get(st.buf_uop, set()):
|
||||
if writer is not k and writer not in ancestors[st]: dependencies[writer].add(k)
|
||||
children: dict[UOp, list[UOp]] = {}
|
||||
in_degree: dict[UOp, int] = {}
|
||||
writes: dict[UOp, list[tuple[UOp, tuple[UOp, ...]]]] = {} # superseded state -> (AFTER, new kernels)
|
||||
reads: list[tuple[UOp, UOp, UOp]] = [] # (reader AFTER, reader kernel, buffer state read)
|
||||
for u in sched_sink.toposort(gate_kernel_sink):
|
||||
if u.op is not Ops.AFTER: continue
|
||||
kernels, after_deps = _split_after(u)
|
||||
prev_state = _unwrap_src(u.src[0])
|
||||
prev_kernels = set(_split_after(prev_state)[0]) if prev_state.op is Ops.AFTER else set()
|
||||
writes.setdefault(prev_state, []).append((u, tuple(k for k in kernels if k not in prev_kernels)))
|
||||
for k in kernels:
|
||||
in_degree.setdefault(k, 0)
|
||||
if k.op is Ops.END: assert k.src[0].op is Ops.CALL, f"END src[0] should be KERNEL, not {k.src[0].op}"
|
||||
kernel_deps = k.src[0].src[1:] if k.op is Ops.END else k.src[1:]
|
||||
read_states = [st for s in kernel_deps for st in _states(s)]
|
||||
reads += [(u, k, st) for st in read_states]
|
||||
# RAW deps: a kernel runs after the kernels that produced the states it reads or joins
|
||||
for st in read_states + [st for s in after_deps for st in _states(s)]:
|
||||
if st.op is Ops.AFTER:
|
||||
for t in _split_after(st)[0]:
|
||||
children.setdefault(t, []).append(k)
|
||||
in_degree[k] += 1
|
||||
# WAR deps: a kernel reading buffer state S must run before another write that supersedes S. an AFTER only
|
||||
# supersedes its immediate prior state; join members already present in that prior state are ordering deps, not writes
|
||||
for u, k, s in reads:
|
||||
for a, write_kernels in writes.get(s, []):
|
||||
if a is u: continue
|
||||
for t in write_kernels:
|
||||
if t is not k and t not in k.backward_slice:
|
||||
children.setdefault(k, []).append(t)
|
||||
in_degree[t] += 1
|
||||
in_degree = {k:len(deps) for k,deps in dependencies.items()}
|
||||
for k, deps in dependencies.items():
|
||||
for p in deps: children.setdefault(p, []).append(k)
|
||||
|
||||
with cpu_profile(TracingKey("linearize schedule")):
|
||||
queue: deque[UOp] = deque(k for k,v in in_degree.items() if v == 0)
|
||||
linearized: list[UOp] = []
|
||||
while len(queue):
|
||||
rk = queue.popleft()
|
||||
if rk.op is Ops.LINEAR:
|
||||
linearized.extend(rk.src)
|
||||
else:
|
||||
k = rk.src[0] if rk.op is Ops.END else rk
|
||||
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
|
||||
linearized.append(k.src[0].call(*buf_uops))
|
||||
k = rk.src[0] if rk.op is Ops.END else rk
|
||||
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
|
||||
body = k.src[0]
|
||||
# Storage aliases may share a parameter now that their dependencies are in the schedule.
|
||||
if body.op is Ops.SINK and len(set(buf_uops)) != len(buf_uops):
|
||||
params = {p for p in body.toposort(enter_calls=False) if p.op is Ops.PARAM and p.arg.slot >= 0}
|
||||
body = body.substitute({p:q for p in params
|
||||
if (q:=p.replace(arg=dataclasses.replace(p.arg, slot=buf_uops.index(buf_uops[p.arg.slot])))) in params})
|
||||
linearized.append(body.call(*buf_uops))
|
||||
for x in children.get(rk, []):
|
||||
in_degree[x] -= 1
|
||||
if in_degree[x] == 0: queue.append(x)
|
||||
@@ -188,6 +181,8 @@ def create_linear_with_vars(big_sink:UOp) -> tuple[UOp, dict[str, int]]:
|
||||
|
||||
# this recursively resolves the linear_call and allocates buffers
|
||||
linear = graph_rewrite(linear_call, pm_resolve_linear_call, name="resolve linear call")
|
||||
for call in linear.src:
|
||||
if call.src[0].op is Ops.PROGRAM: call.call_access()
|
||||
|
||||
# create copies
|
||||
linear = graph_rewrite(linear, pm_copy_from_store, name="create COPY kernels for SDMA")
|
||||
|
||||
@@ -301,27 +301,13 @@ def debuf(ctx:LocalAddBufferContext, buf:UOp):
|
||||
ctx.dg += 1
|
||||
return ret
|
||||
|
||||
def handle_after(ctx:LocalAddBufferContext, after:UOp):
|
||||
if after.addrspace == AddrSpace.LOCAL: return None
|
||||
buf = after.buf_uop
|
||||
# NOTE: this is bottom up, so we only add it once
|
||||
if buf not in ctx.map: ctx.map[buf] = after
|
||||
return buf
|
||||
|
||||
def renumber_range(ctx:LocalAddBufferContext, r:UOp):
|
||||
if r.tag != (): return None
|
||||
ret = r.replace(arg=(ctx.range,)+r.arg[1:], tag=None)
|
||||
ctx.range += 1
|
||||
return ret
|
||||
|
||||
def find_bufs(x:UOp):
|
||||
idxs = [s for s in x.toposort(gate=lambda x: x.op is not Ops.AFTER) if s.op is Ops.INDEX]
|
||||
read_from: dict[UOp, Ops] = {}
|
||||
if any((buf:=idx.buf_uop).op in {Ops.BUFFER, Ops.PARAM} and read_from.setdefault(buf, op:=idx.src[0].op) is not op for idx in idxs):
|
||||
raise RuntimeError(f"cycle detected while indexing {buf}")
|
||||
|
||||
to_define_global = PatternMatcher([
|
||||
(UPat(Ops.STORE, name="x"), find_bufs),
|
||||
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
|
||||
(UPat(Ops.PARAM, name="v"), lambda v:
|
||||
v.replace(arg=replace(v.arg, slot=-1)) if v.arg.name is not None and v.arg.vmin_vmax is not None and v.arg.slot != -1 else None),
|
||||
@@ -335,7 +321,7 @@ to_define_global = PatternMatcher([
|
||||
|
||||
# bound Variables are stores into Variable buffers: strip the store, the buffer becomes an ALU param via debuf
|
||||
(UPat(Ops.AFTER, name="b"), lambda b: b.src[0] if b.is_bound_var else None),
|
||||
(UPat(Ops.AFTER, name="after"), handle_after),
|
||||
(UPat(Ops.AFTER, name="buf"), lambda ctx,buf: debuf(ctx, buf) if buf.addrspace != AddrSpace.LOCAL else None),
|
||||
|
||||
# remove device from local BUFFERIZE
|
||||
(UPat(Ops.STAGE, name="b"), lambda b: b.replace(arg=replace(b.arg, device=None))),
|
||||
|
||||
+3
-6
@@ -43,9 +43,6 @@ def creation_copy_is_realized(u:UOp):
|
||||
# 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 are assigned via STORE+AFTER — merge COPY tag into AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(), 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.CONTIGUOUS, Ops.AFTER), name="x"), tag_uop),
|
||||
(UPat(GroupOp.All, name="x"), lambda ctx,x: tag_uop(x) if x in ctx.bases else None),
|
||||
])
|
||||
@@ -62,8 +59,7 @@ def replace_contig_with_store_after(u:UOp):
|
||||
|
||||
def wrap_tagged_in_contig(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
|
||||
# An empty tag suppresses retagging without requesting materialization.
|
||||
if not x.tag: return x.rtag(None)
|
||||
return x.rtag(None).contiguous(tag=x.tag) # the tag moves onto the wrapping CONTIGUOUS
|
||||
|
||||
@@ -398,8 +394,9 @@ class Tensor(RandMixin):
|
||||
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 = transform_to_call(UOp.sink(*[x.uop for x in (self,)+lst]))
|
||||
linear, var_vals = create_linear_with_vars(big_sink)
|
||||
_apply_map_to_tensors(becomes_map, name="buffers")
|
||||
return create_linear_with_vars(big_sink)
|
||||
return linear, var_vals
|
||||
|
||||
def schedule_linear(self, *lst:Tensor) -> UOp:
|
||||
"""Creates the schedule needed to realize these Tensor(s)."""
|
||||
|
||||
+23
-3
@@ -1250,6 +1250,27 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
kernel = fxn(*placeholders).call(*srcs, grad_fxn=grad_fxn)
|
||||
return [s.after(kernel) for s in srcs]
|
||||
|
||||
def call_access(self) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]:
|
||||
body = self.src[0]
|
||||
if body.op is Ops.SINK and not body.op_in_backward_slice_with_self(Ops.CALL, Ops.CUSTOM, Ops.CUSTOMI, Ops.INS):
|
||||
from tinygrad.codegen import pm_add_loads
|
||||
info = ProgramInfo.from_sink(graph_rewrite(body, pm_add_loads))
|
||||
ins, outs = info.ins, info.outs
|
||||
elif body.op is Ops.PROGRAM and isinstance(body.arg, ProgramInfo): ins, outs = body.arg.ins, body.arg.outs
|
||||
elif body.op is Ops.COPY: ins, outs = (1,), (0,)
|
||||
elif body.op is Ops.LINEAR:
|
||||
ins, outs = (tuple(sorted({p.arg.slot for args in group for a in args for p in a.buf_uop.toposort() if p.op is Ops.PARAM}))
|
||||
for group in zip(*(c.call_access() for c in body.src))) if body.src else ((), ())
|
||||
else: raise RuntimeError(f"cannot compute accesses for opaque {body.op}")
|
||||
if any(i < 0 or i >= len(self.src)-1 or (body.op is Ops.PROGRAM and i not in body.arg.globals) for i in (*ins, *outs)):
|
||||
raise RuntimeError("invalid CALL access slot")
|
||||
if body.op is Ops.PROGRAM:
|
||||
bufs = [s.buf_uop for s in self.src[1:]]
|
||||
keys = [b.arg.buffer.base if b.op is Ops.BUFFER and isinstance(b.arg.buffer, Buffer) else b for b in bufs]
|
||||
if any(i != j and keys[i] is keys[j] for i in outs for j in set(ins+outs)):
|
||||
raise RuntimeError("aliased opaque kernel arguments are unsupported")
|
||||
return tuple(self.src[i+1] for i in ins), tuple(self.src[i+1] for i in outs)
|
||||
|
||||
def to_elf(self) -> TinyELF:
|
||||
assert self.op is Ops.PROGRAM and isinstance(self.arg, ProgramInfo), "to_elf should only be called on a PROGRAM ast"
|
||||
params = tuple(u for u in self.src[1].src if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU)
|
||||
@@ -1304,9 +1325,8 @@ class ProgramInfo:
|
||||
for u in sink.toposort():
|
||||
if u.op is Ops.PARAM and u.addrspace == AddrSpace.ALU: _vars.append(u)
|
||||
if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU: _globals.append(u.arg.slot)
|
||||
if u.op in (Ops.STORE, Ops.LOAD):
|
||||
if (idx:=u.src[0]).op in (Ops.INDEX, Ops.SHRINK) or (u.src[0].op is Ops.CAST and (idx:=u.src[0].src[0]).op is Ops.INDEX):
|
||||
if (buf:=idx.src[0].buf_uop).op is Ops.PARAM: (outs if u.op is Ops.STORE else ins).append(buf.arg.slot)
|
||||
if u.op in (Ops.STORE, Ops.LOAD) and (buf:=u.src[0].buf_uop).op is Ops.PARAM and buf.addrspace is AddrSpace.GLOBAL:
|
||||
(outs if u.op is Ops.STORE else ins).append(buf.arg.slot)
|
||||
if u.op is Ops.SPECIAL: (local_size if u.arg[0] == 'l' else global_size)[int(u.arg[-1])] = cast(int, u.src[0].ssimplify())
|
||||
return ProgramInfo(sink.arg.name if isinstance(sink.arg, KernelInfo) else "test", tuple(global_size), tuple(local_size),
|
||||
tuple(sorted(dedup(_vars), key=lambda v: v.arg.slot)), tuple(sorted(dedup(_globals))), tuple(sorted(dedup(outs))),
|
||||
|
||||
Reference in New Issue
Block a user