Files
2026-10-20 03:55:45 +00:00

139 lines
6.1 KiB
Python

import unittest
from tinygrad import Tensor, UOp, function, Device
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.codegen import to_program
from tinygrad.helpers import Target
from tinygrad.renderer.cstyle import ClangRenderer
from tinygrad.uop.ops import KernelInfo, Ops
from tinygrad.schedule import transform_to_call, resolve_linear_call
def sched_key(t:Tensor): return transform_to_call(UOp.sink(t.uop)).body.key
class TestCall(unittest.TestCase):
def test_call_scalar_param_shape_mismatch(self):
scalar_fxn = UOp.param(0, dtypes.float, ()) * 2
with self.assertRaisesRegex(TypeError, "shape mismatch: expected scalar"):
Tensor.call(Tensor.ones(2), fxn=scalar_fxn).realize()
class TestCallShape(unittest.TestCase):
def test_call_shape_int(self):
# fixed-shape function: shape passes through unchanged
@function
def f(x:Tensor) -> Tensor: return x * 2
self.assertEqual(f(Tensor.empty(4, 8)).shape, (4, 8))
def test_call_shape_param_substitution(self):
# symbolic shape dimension is substituted: inner PARAM replaced with the BIND arg
@function
def f(x:Tensor) -> Tensor: return x * 2
sz = UOp.variable("sz", 1, 8)
shape = f(Tensor.empty(8)[:sz.bind(5)]).shape
# the inner param should be gone, replaced with the bound Variable from the call arg
self.assertIsInstance(shape[0], UOp)
self.assertTrue(shape[0].is_bound_var)
self.assertEqual(shape[0], sz.bind(5))
def test_call_shape_expr_substitution(self):
# expression containing PARAMs in shape gets fully substituted
@function
def f(x:Tensor) -> Tensor: return x + 1
sz = UOp.variable("sz", 1, 10)
shape = f(Tensor.empty(10, 4)[:sz.bind(3)]).shape
self.assertIsInstance(shape[0], UOp)
self.assertTrue(shape[0].is_bound_var)
self.assertEqual(shape[1], 4)
def test_call_shape_no_param_passthrough(self):
# a non-PARAM UOp shape element passes through unchanged
@function
def f(x:Tensor) -> Tensor: return x * 3
sz = UOp.variable("sz", 1, 8)
shape = f(Tensor.empty(8)[:sz.bind(5)]).shape
self.assertEqual(shape[0], sz.bind(5))
class TestCallSchedule(unittest.TestCase):
def test_precompile_schedule_cache_hit(self):
"""two instances of the same @function should produce identical scheduled function keys without aliasing their outputs"""
@function(precompile=True)
def f(x:Tensor) -> Tensor: return x + Tensor.full(x.shape, -1.0)
a = Tensor.empty(4, 8)
b = Tensor.empty(4, 8)
r0, r1 = f(a), f(b)
c0 = next(u for u in r0.uop.toposort() if u.op is Ops.CALL and u.arg.precompile)
c1 = next(u for u in r1.uop.toposort() if u.op is Ops.CALL and u.arg.precompile)
self.assertTrue(c0.has_unbound_outputs)
self.assertTrue(c1.has_unbound_outputs)
# output identities stay unique per call; they canonicalize only when combined into a scheduling scope
self.assertIsNot(c0.src[-1], c1.src[-1])
self.assertEqual(sched_key(r0), sched_key(r1))
def test_scalar_names_and_values_do_not_affect_cache_key(self):
@function(precompile=True)
def f(x:Tensor) -> Tensor: return x * 2
x = Tensor.empty(8)
left = UOp.variable("left", 1, 8).bind(2)
right = UOp.variable("right", 1, 8).bind(3)
a, b = f(x[:left]), f(x[:right])
self.assertEqual(a.shape, (left,))
self.assertEqual(b.shape, (right,))
self.assertEqual(sched_key(a), sched_key(b))
for v in a.uop.src[1].body.variables():
self.assertIsNone(v.arg.name)
self.assertGreaterEqual(v.arg.slot, 0)
class TestArgOrder(unittest.TestCase):
def _dev(self, x): return x.device if isinstance(x.device, str) else (x.device or (Device.DEFAULT,))[0]
def test_output_pos_symbolic_shape(self):
# symbolic output shapes resolve against the final arg slots, not the input order (PARAM(2) in the shape, output at 0)
x = Tensor.empty(8).realize()
sz = UOp.variable('sz', 1, 8)
dev = self._dev(x)
p1, p2 = UOp.param(1, x.dtype, x.shape, dev), sz.param_like(2)
value = p1.reshape(x.shape).shrink_to((p2,))
bound = sz.bind(5)
outs = UOp.call_with_outputs((value,), x.uop, bound, output_pos=(0,))
# the minted output's shape substituted PARAM(2) with the bound Variable from position 2 in the arg list
shp = outs[0].shape[0]
self.assertIsInstance(shp, UOp)
self.assertTrue(shp.is_bound_var)
self.assertEqual(shp, bound)
def test_output_pos_free_variable_shape(self):
x = Tensor.empty(8).realize()
sz = UOp.variable('sz', 1, 8)
p1 = UOp.param(1, x.dtype, x.shape, self._dev(x))
for dim in (sz, sz.bind(5)):
with self.subTest(bound=dim.is_bound_var):
# Free Variables have slot -1, not a positional slot into the call args.
out, = UOp.call_with_outputs((p1.shrink_to((dim,)),), x.uop, output_pos=(0,))
self.assertEqual(out.shape, (dim,))
def test_output_pos_must_be_ascending(self):
x = Tensor.arange(3, dtype=dtypes.int).realize()
p1 = UOp.param(1, x.dtype, x.shape, self._dev(x))
with self.assertRaises(AssertionError):
UOp.call_with_outputs((p1.reshape(x.shape) * 2, p1.reshape(x.shape) + 1), x.uop, output_pos=(1, 0))
class TestCallCodegen(unittest.TestCase):
def test_compiled_scalar_slots_are_not_call_slots(self):
out = UOp.placeholder((1,), dtypes.int)
p = out.param_like(0)
v = UOp.variable("external", 1, 8, dtype=dtypes.int)
prg = to_program(p.index(0).store(v).sink(arg=KernelInfo("scalar")),
ClangRenderer(Target("CPU", arch="x86_64,x86-64")))
self.assertEqual(prg.arg.vars[0].arg.slot, 1)
linear = UOp(Ops.LINEAR, src=(prg.call(p, v.bind(2)),))
# Slot 1 in the enclosing call must not rewrite the already-compiled program's ABI slot 1.
resolved = resolve_linear_call(linear.call(out, UOp.variable("other", 1, 8, dtype=dtypes.int).bind(3)))
self.assertIs(resolved.src[0].body, prg)
def test_call_stack_pointer(self):
slot = UOp.placeholder((1,), dtypes.uint32, addrspace=AddrSpace.REG)
call = UOp.custom_function("callback").call(slot[0])
prg = to_program(call.sink(arg=KernelInfo("call_stack")), ClangRenderer(Target("CPU", arch="x86_64,x86-64")))
self.assertIn("(unsigned int*)((buf", prg.src[2].arg)
if __name__ == '__main__':
unittest.main()