import unittest, pytest from tinygrad import dtypes, Variable, Device from tinygrad.dtype import AddrSpace from tinygrad.helpers import DEBUG, Context from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType, broadcast_axes, KernelInfo from tinygrad.uop.symbolic import sym from test.helpers import to_uops_list from tinygrad.codegen import full_rewrite_to_sink simple_pm = PatternMatcher([ (UPat.cvar('x', dtypes.weakint), lambda x: UOp.const(1.0) + UOp.const(2.0)), (UPat.cvar('x') + UPat.cvar('y'), lambda x,y: UOp.const(x.val+y.val)), (UPat.cvar('x') * UPat.cvar('y') * UPat.cvar('z'), lambda x,y,z: UOp.const(x.val*y.val*z.val)), ((UPat.var('x') + UPat.cvar('c1')) + UPat.cvar('c2'), lambda x,c1,c2: x + (c1.val+c2.val)), ]) def const_values(u:UOp): if u.op is Ops.CONST: return (u.val,) if u.op is Ops.STACK: return tuple(x.val for x in u.src) raise AssertionError(f"expected const-like UOp, got {u.op}") class TestGraphRewriteConst(unittest.TestCase): def test_gep_const(self): v1 = UOp.const((0,1,2), dtypes.int) v2 = v1.index(1) ret = graph_rewrite(v2, sym) self.assertEqual(ret.dtype, dtypes.int) self.assertEqual(ret.val, 1) def test_add_const(self): v1 = UOp.const((0,1,2)) v2 = UOp.const((5,6,7)) ret = graph_rewrite(v1+v2, sym) self.assertEqual(ret.op, Ops.STACK) self.assertEqual(const_values(ret), (5,7,9)) def test_add_const_lose_v(self): v1 = UOp.const((0,1,2)) v2 = UOp.const((2,1,0)) ret = graph_rewrite(v1+v2, sym) self.assertEqual(ret.op, Ops.STACK) self.assertEqual(const_values(ret), (2,2,2)) def xfail_broken_const_wraparound(fn): fn = pytest.mark.xfail(reason="const folding does not properly implement modular arithmetic")(fn) return unittest.expectedFailure(fn) class TestModularWraparound(unittest.TestCase): def _test(self, uop:UOp, expected:int): results = to_uops_list([uop]) self.assertEqual(len(results), 2) # +1 for SINK self.assertEqual(results[0].op, Ops.CONST) self.assertEqual(results[0].dtype, uop.dtype) self.assertEqual(results[0].val, expected) @xfail_broken_const_wraparound def test_cast(self): t = self._test t(UOp.const(0xABCD17D6, dtypes.uint).cast(dtypes.uint8), 0xD6) t(UOp.const(0xABCD17D6, dtypes.uint).cast(dtypes.uint8).cast(dtypes.uint), 0xD6) @xfail_broken_const_wraparound def test_mul(self): t = self._test t(UOp.const(0xABCD17D6, dtypes.uint) * 0xAABBCCDD, 1147018174) t(UOp.const(0xABCD17D6, dtypes.int) * 10, -1241321892) @xfail_broken_const_wraparound def test_div(self): t = self._test t(UOp.const(0xABCD17D6, dtypes.uint) * 0xAABBCCDD // 11, 104274379) t(UOp.const(0xABCD17D6, dtypes.int) * 10 // 11, -112847444) @xfail_broken_const_wraparound def test_neg(self): t = self._test t(-UOp.const(1, dtypes.uint8), 0xFF) t(-UOp.const(1, dtypes.uint16), 0xFFFF) t(-UOp.const(1, dtypes.uint32), 0xFFFFFFFF) t(-UOp.const(1, dtypes.uint64), 0xFFFFFFFFFFFFFFFF) @xfail_broken_const_wraparound def test_neg_min_int(self): t = self._test t(-UOp.const(-2**7, dtypes.int8), -2**7) t(-UOp.const(-2**15, dtypes.int16), -2**15) t(-UOp.const(-2**31, dtypes.int32), -2**31) t(-UOp.const(-2**63, dtypes.int64), -2**63) @xfail_broken_const_wraparound def test_payne_hanek_reduction_bug(self): t = self._test a = (UOp.const(43748177600, dtypes.uint).cast(dtypes.uint) | 36).cast(dtypes.ulong) b = 2536655455 * a + 4294967296 * UOp.const(25366554550, dtypes.ulong) c = (b + 2261737165) // 4611686018427387904 t(c, 0) class TestGraphRewrite(unittest.TestCase): def test_dedup(self): v1 = UOp.variable("v", 0, 1, dtypes.float) v2 = UOp.variable("v", 0, 1, dtypes.float) nout = graph_rewrite(v1+v2, PatternMatcher([])) self.assertIs(nout.src[0], nout.src[1]) # NOTE: this shows why we can't have a UOp in arg @unittest.expectedFailure def test_no_dedup_args(self): a1 = UOp.variable("a1", UOp.const(0), UOp.const(11), dtypes.int) a2 = UOp.variable("a2", UOp.const(0), UOp.const(11), dtypes.int) sink = a1.sink(a2) variables = [x for x in graph_rewrite(sink, PatternMatcher([])).toposort() if x.op is Ops.PARAM and x.addrspace is AddrSpace.ALU] self.assertEqual(len(variables), 1) def test_simple(self): c1 = UOp.const(1.0) c2 = UOp.const(2.0) nout = graph_rewrite(c1+c2, simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 3.0) def test_depth_2_late(self): c1 = UOp.const(1.0) c2 = UOp.const(2.0) c3 = UOp.const(3.0) nout = graph_rewrite(c1*c2*(c3+c3), simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 12.0) def test_double(self): c1 = UOp.const(1.0) c2 = UOp.const(2.0) c3 = UOp.const(3.0) nout = graph_rewrite(c1+c2+c3, simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 6.0) def test_triple(self): c1 = UOp.const(1.0) c2 = UOp.const(2.0) c3 = UOp.const(3.0) c4 = UOp.const(4.0) nout = graph_rewrite(c1+c2+c3+c4, simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 10.0) def test_diamond(self): c1 = UOp.const(1.0) c2 = UOp.const(2.0) c3 = UOp.const(3.0) nout = graph_rewrite((c1+c2)+(c1+c3), simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 7.0) def test_magic_4(self): c1 = UOp.const(4) nout = graph_rewrite(c1, simple_pm) self.assertEqual(nout.op, Ops.CONST) self.assertEqual(nout.val, 3.0) def test_depth_2_fold(self): v = UOp.variable("v", 0, 1, dtypes.float) c1 = UOp.const(1.0) c2 = UOp.const(2.0) nout = graph_rewrite(v+c1+c2, simple_pm) self.assertEqual(nout.op, Ops.ADD) self.assertEqual(nout.src[0].op, Ops.PARAM) self.assertEqual(nout.src[1].op, Ops.CONST) self.assertEqual(nout.src[1].val, 3.0) def test_commutative_work(self): a = UOp.variable('a', 0, 1) b = UOp.variable('b', 0, 1) self.assertIs((a+b).simplify(), (b+a).simplify()) def test_consts_go_last_right_away(self): a = UOp.variable('a', 0, 1) tst = (2+a).simplify() self.assertIs(tst.src[0], a) self.assertIs(tst.src[1], UOp.const(2)) def test_consts_go_last(self): a = UOp.variable('a', 0, 1) b = UOp.variable('b', 0, 1) c = UOp.variable('c', 0, 1) d = UOp.variable('d', 0, 1) outs = [2+a, 2+a+d+3+b+c+4, UOp.const(2)+a, (4+d)+c+(2+a)+b] for out in outs: sink = graph_rewrite(out, sym) print(sink.render()) self.assertEqual(sink.op, Ops.ADD) self.assertEqual(sink.src[1].op, Ops.CONST) self.assertEqual(len([x for x in sink.toposort() if x.op is Ops.CONST]), 1) class TestUOpGraph(unittest.TestCase): def test_add_constant_fold(self): c1 = UOp.const(1.0, dtypes.float) c2 = UOp.const(2.0, dtypes.float) out = c1+c2 uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK out = uops[-2] self.assertEqual(out.op, Ops.CONST) self.assertEqual(out.val, 3.0) def test_where_same_fold(self): v = UOp.variable('tmp', 0, 1) c0 = UOp.const(0) vc = v != c0 c1 = UOp.const(1.0, dtypes.float) out = vc.where(c1, c1) uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK out = uops[-2] self.assertEqual(out.op, Ops.CONST) self.assertEqual(out.val, 1.0) def test_where_const_fold(self): bf = UOp.const(False) c1 = UOp.const(1.0, dtypes.float) c2 = UOp.const(2.0, dtypes.float) out = bf.where(c1, c2) uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK out = uops[-2] self.assertEqual(out.op, Ops.CONST) self.assertEqual(out.val, 2.0) def test_const_cast(self): bf = UOp.const(False) out = bf.cast(dtypes.int) uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK out = uops[-2] self.assertEqual(out.op, Ops.CONST) self.assertEqual(out.val, 0) def test_const_bitcast(self): bf = UOp.const(1.0, dtypes.float) out = bf.bitcast(dtypes.uint32) uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK out = uops[-2] self.assertEqual(out.op, Ops.CONST) self.assertEqual(out.val, 0x3F800000) @unittest.expectedFailure def test_const_shape_change_bitcast(self): bf = UOp.const(0x3F).cast(dtypes.uint8) out = bf.bitcast(dtypes.half) uops = to_uops_list([out]) self.assertEqual(len(uops), 2) # +1 for SINK def test_devectorize_derives_lane_dtype(self): from tinygrad.codegen import do_devectorize # an Invalid lane derives bool while the value lane derives float: the lane rebuild must derive, not inherit lhs = UOp.stack(UOp.invalid(), UOp.const(1.0).cast(dtypes.float)) out = do_devectorize(lhs * lhs) invalid_lane_mul = next(u for u in out.src[0].toposort() if u.op is Ops.MUL) self.assertIs(invalid_lane_mul.dtype, dtypes.bool) @unittest.skip("this test isn't valid uops") def test_noop_vectorize_fold(self): d0 = UOp.param(0, dtypes.float, (1,)) idx = UOp.const(0) ld = d0.load(idx, dtype=dtypes.float) vec = UOp(Ops.STACK, dtypes.float, (ld,)) x = vec.index(0) alu = UOp(Ops.SQRT, src=(x, )) out = UOp(Ops.STORE, src=(d0, idx, alu)) uops = to_uops_list([out]) self.assertEqual(len([x for x in uops if x.op is Ops.STACK]), 0) @unittest.skip("this test isn't valid uops") def test_gep_vec_fold(self): d0 = UOp.param(0, dtypes.float, (1,)) d1 = UOp.param(1, dtypes.float, (1,)) d2 = UOp.param(2, dtypes.float, (1,)) idx = UOp.const(0) def _test_vec(geps, count=4): vec = UOp(Ops.STACK, dtypes.float, geps) out = d0.index(idx).store(vec) uops = to_uops_list([out]) if DEBUG >= 4: from tinygrad import Device print(Device[Device.DEFAULT].renderer.render(uops)) return uops[-2].src[-1] # -2 to skip SINK # possible val = d1.index(idx).load(dtype=dtypes.float) xyzw = tuple(val.index(i) for i in range(4)) self.assertIs(_test_vec(xyzw).op, Ops.LOAD) # unaligned val = d1.index(idx).load(dtype=dtypes.float) wzyx = tuple(val.index(i) for i in reversed(range(4))) self.assertIs(_test_vec(wzyx).op, Ops.STACK) # different_size val = d1.index(idx).load(dtype=dtypes.float) xy = tuple(val.index(i) for i in range(2)) self.assertIs(_test_vec(xy+xy).op, Ops.STACK) val = d1.index(idx).load(dtype=dtypes.float) xy = tuple(val.index(i) for i in range(2)) self.assertIs(_test_vec(xy, count=2).op, Ops.STACK) # different vals val1 = d1.index(idx).load(dtype=dtypes.float) val2 = d2.index(idx).load(dtype=dtypes.float) xy1 = tuple(val1.index(i) for i in range(2)) xy2 = tuple(val2.index(i) for i in range(2)) self.assertIs(_test_vec(xy1+xy2).op, Ops.STACK) def test_gep_vec_const_fold(self): for vec_size in [2, 4, 8]: consts = [UOp.const(float(i), dtypes.float) for i in range(vec_size)] vec = UOp(Ops.STACK, src=tuple(consts)) with Context(SPEC=0): uops = to_uops_list([vec.index(i) for i in range(vec_size)]) for uop, const in zip(uops, consts): self.assertEqual(uop, const) def test_cast_alu_fold(self): d0 = UOp.param(0, dtypes.bool, (1,)) d1 = UOp.param(1, dtypes.int, (1,)) idx = UOp.const(0) ld = d1.index(idx) alu = (ld<1).cast(dtypes.bool) out = d0.index(idx).store(alu) uops = to_uops_list([out]) self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 0) def test_double_cast_fold(self): d0 = UOp.param(0, dtypes.float, (1,)) d1 = UOp.param(1, dtypes.int, (1,)) idx = UOp.const(0, dtypes.int) ld = d1.index(idx) alu = ld.cast(dtypes.float).cast(dtypes.float) out = d0.index(idx).store(alu) uops = to_uops_list([out]) self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 1) def test_depth_2_const_fold(self): v = UOp.variable("tmp", 0, 1, dtypes.int) c2 = UOp.const(2, dtypes.int) c4 = UOp.const(4, dtypes.int) vc = v+c2 out = vc+c4 uops = to_uops_list([out]) self.assertEqual(len(uops), 5) # +1 for SINK, +1 for the PARAM shape STACK out = uops[-2] # -2 to skip SINK self.assertEqual(out.op, Ops.ADD) self.assertEqual(out.src[1].op, Ops.CONST) self.assertEqual(out.src[1].val, 6) def test_bitcast_to_same_dtype_fold(self): for dt in dtypes.ints + dtypes.floats + (dtypes.bool,): d0 = UOp.param(0, dt, (1,)) v = d0.index(UOp.const(0)) uops = to_uops_list([v.bitcast(dt)]) self.assertEqual(len([x for x in uops if x.op is Ops.BITCAST and x.dtype is dt]), 0, f"dtype = {dt}") def test_sub_with_cast_folds(self): a = Variable("a", 0, 5) uops = to_uops_list([a.cast(dtypes.int)+(-a).cast(dtypes.int)]) assert uops[0] == UOp.const(0, dtypes.int) assert uops[-1].op == Ops.SINK def test_where_on_gated_load_fold(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.long, (100,)) ld = d0.index(ridx0.valid(ridx0<50)) w = (ridx0<50).where(ld, 5) out = UOp.param(1, dtypes.long, (100,)) uops = to_uops_list([out.index(ridx0).store(w)]) for u in uops: assert u.op is not Ops.WHERE if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].val==5 def test_where_on_gated_load_folds_swapped_branches(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.long, (100,)) ld = d0.index(ridx0.valid((ridx0<50).logical_not())) w = (ridx0<50).where(5, ld) uops = to_uops_list([w]) for u in uops: assert u.op is not Ops.WHERE if u.op is Ops.LOAD: assert u.src[1].val==5 def test_where_on_gated_load_with_cast(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.int, (100,)) gate_idx = ridx0.valid((ridx0<50)) ld = d0.index(gate_idx).cast(dtypes.float) w = (ridx0<50).where(ld, 5.0) out = UOp.param(1, dtypes.float, (100,)) uops = to_uops_list([out.index(ridx0).store(w)]) for u in uops: assert u.op is not Ops.WHERE if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].val == 5 def test_where_on_casted_gated_load_extra_cond(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.float, (100,)) ld = d0.index(ridx0.valid(ridx0<50)) w = ((ridx0<50) & (ridx0>30)).where(ld, UOp.const(0.0)).cast(dtypes.half) out = UOp.param(1, dtypes.half, (100,)) uops = to_uops_list([out.index(ridx0).store(w)]) for u in uops: assert u.op is not Ops.WHERE def test_where_on_casted_gated_load_extra_cond_swapped(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.float, (100,)) ld = d0.index(ridx0.valid(ridx0<50)) w = ((ridx0<50) & (ridx0>30)).where(UOp.const(0.0), ld).cast(dtypes.half) out = UOp.param(1, dtypes.half, (100,)) uops = to_uops_list([out.index(ridx0).store(w)]) for u in uops: assert u.op is not Ops.WHERE def test_where_in_store_becomes_gate(self): ridx0 = UOp.range(100, 0) d0 = UOp.param(0, dtypes.long, (100,)) idx = d0.index(ridx0) ld = idx.load() val = (ridx0<50).where(5, ld) st = idx.store(val).end(ridx0) uops = to_uops_list([st]) for u in uops: assert u.op is not Ops.WHERE if u.op is Ops.STORE: assert u.src[1].val==5 def test_load_idx_becomes_int(self): # mnist indexing with split reduceop # Make sure we are not doign math on the loaded index, which would promote it to long c0 = UOp.param(0, dtypes.uchar, (128000,)) c1 = UOp.range(UOp.const(512), 1, AxisType.WEAK) c2 = UOp.range(UOp.const(250), 2, AxisType.WEAK) c3 = UOp.param(1, dtypes.int, (512,)) c4 = c3.index(c1) c5 = UOp.range(UOp.const(240), 0, AxisType.REDUCE) c6 = ((c2*UOp.const(240))+c5) c7 = UOp.param(2, dtypes.uchar, (60000,)) c8 = c7.index(c6) c9 = ((c4<0).where((c4+60000), c4)!=c6.cast(dtypes.int)).where(0, c8.cast(dtypes.uint).cast(dtypes.uchar)).reduce(c5, arg=Ops.ADD) c10 = c0.index(((c1*UOp.const(250))+c2)).store(c9).end(c1, c2) uops = to_uops_list([c10]) for u in uops: self.assertNotEqual(u.dtype, dtypes.long) def test_load_idx_no_math_on_loaded(self): # test the (x+y)