Compare commits

..
Author SHA1 Message Date
Chen-Yu Yang cf3703cd2b const are weak 3 [pr] 2026-08-24 18:55:52 -04:00
16 changed files with 186 additions and 137 deletions
+14 -16
View File
@@ -23,6 +23,7 @@ from tinygrad.codegen.decomp.dtype import f2f
VarVal = UOp | tuple[str, list[str], str]
def _const(dt, v): return UOp.const(v, dt)
def _single_value(v: UOp): return v.vmin if v.vmin == v.vmax else None
def _u32(v): return _const(dtypes.uint32, v)
def _u64(v): return _const(dtypes.uint64, v)
def _to_u32(v): return v if v.dtype == dtypes.uint32 else v.bitcast(dtypes.uint32) if v.dtype.itemsize == 4 else v.cast(dtypes.uint32)
@@ -70,8 +71,8 @@ def _expr_bits(v: UOp) -> int:
if v.op in (Ops.AND, Ops.XOR):
widths: list[int] = []
for src in v.src:
if src.op == Ops.CONST and isinstance(src.val, int) and src.val > 0 and (src.val & (src.val + 1)) == 0:
widths.append(src.val.bit_length())
if isinstance(sv:=_single_value(src), int) and sv > 0 and (sv & (sv + 1)) == 0:
widths.append(sv.bit_length())
if widths: return max(widths)
return v.dtype.bitsize
@@ -159,9 +160,9 @@ def _minmax_reduce(is_max: bool, dt, *args: UOp) -> UOp:
def _find_two_pi_mul(x):
if x.op != Ops.MUL or len(x.src) != 2: return None
for i, s in enumerate(x.src):
if s.op == Ops.CONST and abs(s.val - 6.283185307179586) < 1e-5: return (x.src[1-i], 6.283185307179586)
if (sv:=_single_value(s)) is not None and abs(sv - 6.283185307179586) < 1e-5: return (x.src[1-i], 6.283185307179586)
if s.op == Ops.MUL and len(s.src) == 2:
vals = [ss.val for ss in s.src if ss.op == Ops.CONST] + [ss.src[0].val for ss in s.src if ss.op == Ops.CAST and ss.src[0].op == Ops.CONST]
vals = [sv for ss in s.src if (sv:=_single_value(ss)) is not None]
if len(vals) == 2 and abs(vals[0] * vals[1] - 6.283185307179586) < 1e-5: return (x.src[1-i], vals[0] * vals[1])
return None
@@ -178,7 +179,7 @@ def _trig_reduce(x, phase=0.0):
def _signext(val: UOp) -> UOp:
for bits, mask, ext in [(4, 0xF, 0xFFFFFFF0), (8, 0xFF, 0xFFFFFF00), (16, 0xFFFF, 0xFFFF0000)]:
if (val.op == Ops.AND and len(val.src) == 2 and val.src[1].op == Ops.CONST and val.src[1].val == mask) or val.dtype.itemsize == bits // 8:
if (val.op == Ops.AND and len(val.src) == 2 and _single_value(val.src[1]) == mask) or val.dtype.itemsize == bits // 8:
v32 = val.cast(dtypes.uint32) if val.dtype != dtypes.uint32 else val
sb = (v32 >> _u32(bits - 1)) & _u32(1)
return sb.ne(_u32(0)).where(v32 | _u32(ext), v32).cast(dtypes.int)
@@ -549,7 +550,7 @@ class Parser:
if not dtypes.is_int(right.dtype): right = right.cast(dtypes.uint32)
return (left >> right) if op == '>>' else (left << right)
case '+' | '-':
if op == '-' and left.op == Ops.CONST and right.op == Ops.CONST: return _const(left.dtype, left.val - right.val)
if op == '-' and (lv:=_single_value(left)) is not None and (rv:=_single_value(right)) is not None: return _const(left.dtype, lv - rv)
return (left + right) if op == '+' else (left - right)
case '*' | '/':
# Integer promotion: promote 16-bit integers to 32-bit before multiply to avoid overflow
@@ -559,7 +560,7 @@ class Parser:
left, right = left.cast(pdt), right.cast(pdt)
if op == '*': return left * right
return (left // right) if dtypes.is_int(left.dtype) else (left / right)
case '**': return UOp(Ops.EXP2, src=(right.cast(left.dtype),)) if left.op == Ops.CONST and left.val == 2.0 else left
case '**': return UOp(Ops.EXP2, src=(right.cast(left.dtype),)) if _single_value(left) == 2.0 else left
_PREC = [('||',), ('&&',), ('|',), ('^',), ('&',), ('==', '!=', '<>'), ('>=', '<=', '>', '<'), ('>>', '<<'), ('+', '-'), ('*', '/'), ('**',)]
@@ -581,8 +582,8 @@ class Parser:
return inner.eq(_const(inner.dtype, 0))
if self.try_eat_val('-', 'OP'):
inner = self.unary()
if inner.op == Ops.CONST:
return _const(dtypes.int if inner.dtype == dtypes.uint32 else inner.dtype, -inner.val)
if (v:=_single_value(inner)) is not None:
return _const(dtypes.int if inner.dtype == dtypes.uint32 else inner.dtype, -v)
return inner.neg()
if self.try_eat_val('+', 'OP'): return self.unary()
return self.postfix()
@@ -721,15 +722,13 @@ class Parser:
self.eat('OP')
width = self.parse()
self.eat('RBRACKET')
if width.op == Ops.CONST:
w = int(width.val)
if isinstance(w:=_single_value(width), int):
return (base >> _to_u32(first)) & _const(base.dtype, (1 << w) - 1)
return base
if self.try_eat('COLON'):
second = self.parse()
self.eat('RBRACKET')
if first.op == Ops.CONST and second.op == Ops.CONST:
a, b = int(first.val), int(second.val)
if isinstance(a:=_single_value(first), int) and isinstance(b:=_single_value(second), int):
if a < b: return _bitreverse(base, b - a + 1)
hi, lo = a, b
if lo >= base.dtype.itemsize * 8:
@@ -750,8 +749,7 @@ class Parser:
dt_suffix = DTYPES.get(self.eat('IDENT').val, dtypes.uint32)
if var_name is None:
var_name = self._find_var_name(base)
if first.op == Ops.CONST:
idx = int(first.val)
if isinstance(idx:=_single_value(first), int):
# Check for array element (var@idx)
if var_name and f'{var_name}@{idx}' in self.vars:
v = self.vars[f'{var_name}@{idx}']
@@ -924,7 +922,7 @@ class Parser:
def _coerce_cmp(self, l: UOp, r: UOp) -> tuple[UOp, UOp]:
if l.dtype != r.dtype:
if r.dtype == dtypes.int and r.op == Ops.CONST and r.val < 0: l = l.cast(dtypes.int)
if r.dtype == dtypes.int and isinstance(rv:=_single_value(r), int) and rv < 0: l = l.cast(dtypes.int)
else: r = r.cast(l.dtype)
return l, r
+1 -1
View File
@@ -1023,7 +1023,7 @@ class TestSymbolic(unittest.TestCase):
cond = Variable("s", 0, 3, dtypes.int) < 2
a = Variable("a", 0, 3, dtypes.int)
self.assertIs(graph_rewrite(cond.where(a, a+1).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), (a+1).cast(dtypes.half)))
self.assertIs(graph_rewrite(cond.where(a, uconst(2)).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.const(2, dtypes.half)))
self.assertIs(graph_rewrite(cond.where(a, uconst(2)).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), uconst(2.0)))
self.assertIs(graph_rewrite(cond.where(a, UOp.invalid()).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.invalid()))
def test_where_const_gate_keeps_stated_width(self):
+7 -5
View File
@@ -6,7 +6,7 @@ from tinygrad.helpers import Timing, Context, cdiv
from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
from tinygrad.device import Device
from tinygrad.uop.ops import Ops, AxisType, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
from tinygrad.uop.weak import pm_lower_index_dtype
from tinygrad.uop.weak import pm_lower_weak
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
from tinygrad.uop.symbolic import sym, pm_remove_invalid
from test.helpers import eval_uop, to_uops_list
@@ -76,16 +76,18 @@ class TestLowerIndexDtype(unittest.TestCase):
buf = UOp.param(0, dtypes.float, (2**31+64,))
i = UOp.variable("i", 0, 2**28)
shrink = UOp(Ops.SHRINK, src=(buf, (i*24).valid(i < 2**28), UOp.const(4)))
lowered = graph_rewrite(shrink.sink(), pm_lower_index_dtype)
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
lowered = graph_rewrite(shrink.sink(), pm_lower_weak)
self.assertTrue(all(u.op is Ops.CONST for u in lowered.backward_slice_with_self if u.dtype in dtypes.weaks),
"lowering must resolve every weak width, except a typed literal's value half")
sh = next(u for u in lowered.backward_slice_with_self if u.op is Ops.SHRINK)
self.assertEqual(sh.src[1].dtype, dtypes.long)
def test_reg_buffer_size_lowers(self):
reg = UOp.placeholder((4,), dtypes.float, 0, addrspace=AddrSpace.REG)
self.assertEqual(reg.src[0].dtype, dtypes.weakint)
lowered = graph_rewrite(reg.sink(), pm_lower_index_dtype)
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
lowered = graph_rewrite(reg.sink(), pm_lower_weak)
self.assertTrue(all(u.op is Ops.CONST for u in lowered.backward_slice_with_self if u.dtype in dtypes.weaks),
"lowering must resolve every weak width, except a typed literal's value half")
self.assertEqual(next(u for u in lowered.backward_slice_with_self if u.op is Ops.BUFFER).src[0].dtype, dtypes.int)
class TestSafeCast(unittest.TestCase):
+20 -5
View File
@@ -4,7 +4,7 @@ from tinygrad import Tensor, dtypes, TinyJit
from tinygrad.helpers import Context
from tinygrad.dtype import least_upper_float
from tinygrad.uop.ops import UOp, Ops, GroupOp, dtype_from_uop, graph_rewrite
from tinygrad.uop.weak import pm_lower_index_dtype, pm_commit_weak
from tinygrad.uop.weak import pm_commit_weak
from tinygrad.uop.symbolic import symbolic_simple
from tinygrad.uop.spec import spec_shared, type_verify
from tinygrad.engine.jit import JitError
@@ -74,7 +74,7 @@ class TestWeakPromotion(unittest.TestCase):
recips = [u for u in (x / y)._uop.toposort() if u.op is Ops.RECIPROCAL]
self.assertEqual([(u.dtype, u.src[0].dtype) for u in recips], [(dtypes.float32, dtypes.float32)])
with Context(DEFAULT_FLOAT=dtypes.float16):
committed = graph_rewrite((UOp.const(1).cast(dtypes.int32) + UOp.const(1.0)).cast(dtypes.float32), pm_lower_index_dtype, ctx={})
committed = graph_rewrite((UOp.const(1).cast(dtypes.int32) + UOp.const(1.0)).cast(dtypes.float32), pm_commit_weak)
self.assertEqual([u.dtype for u in committed.toposort() if u.op is Ops.ADD], [dtypes.float32])
def test_div_sub_operand_kept_weak(self):
@@ -85,7 +85,7 @@ class TestWeakPromotion(unittest.TestCase):
def test_cast_weak_expression_commits_at_cast_floor(self):
# the floor never narrows: a cast BELOW the default does not pull the compute width down with it
with Context(DEFAULT_FLOAT=dtypes.float32):
narrowed = graph_rewrite((UOp.const(1.0) + UOp.const(2.0)).cast(dtypes.float16), pm_lower_index_dtype, ctx={})
narrowed = graph_rewrite((UOp.const(1.0) + UOp.const(2.0)).cast(dtypes.float16), pm_commit_weak)
self.assertEqual((narrowed.dtype, narrowed.src[0].dtype), (dtypes.float16, dtypes.float32))
def test_cast_weak_expression_value_uses_cast_floor(self):
@@ -126,8 +126,23 @@ class TestWeakPromotion(unittest.TestCase):
weak_lub = UOp(Ops.ADD, src=(UOp.const(1), UOp.const(1.0)))
self.assertIs(graph_rewrite(weak_lub, pm_commit_weak), weak_lub)
concrete = UOp.const(2.0).cast(dtypes.float16)
where = graph_rewrite(UOp(Ops.WHERE, src=(UOp.const(True), concrete, UOp.const(1.0))), pm_lower_index_dtype, ctx={})
self.assertEqual(tuple(x.dtype for x in where.src), (dtypes.bool, dtypes.float16, dtypes.float16))
# the weak arm stays bare: its sibling states the width, so the WHERE already derives float16 for it
where = graph_rewrite(UOp(Ops.WHERE, src=(UOp.const(True), concrete, UOp.const(1.0))), pm_commit_weak)
self.assertEqual((where.dtype, tuple(x.dtype for x in where.src)), (dtypes.float16, (dtypes.bool, dtypes.float16, dtypes.weakfloat)))
def test_derivable_const_rounds_at_the_derived_width(self):
# re-rounds a derivable const in place (still bare) so value-keyed folds (x*1 -> x, x*-1 -> NEG) still fire
x = UOp.param(0, dtypes.float32, (1,)).index(UOp.const(0).cast(dtypes.int32)).load()
mul = graph_rewrite(x * UOp.const(-0.9999999893980771), symbolic_simple+pm_commit_weak)
self.assertIs(mul.src[1], UOp.const(-1.0))
self.assertIs(graph_rewrite(x * UOp.const(1.0000000106), symbolic_simple+pm_commit_weak), x)
def test_committed_const_conversion_folds_for_native_format(self):
folded = graph_rewrite(UOp.const(16256, dtypes.ushort).cast(dtypes.uint), symbolic_simple)
self.assertIs(folded, UOp.const(16256, dtypes.uint))
# fmt-less targets are lowered by renderer rewrites, where collapsing this pair would cycle with float-intermediate insertion.
emulated = UOp.const(1.0, dtypes.float).cast(dtypes.bfloat16)
self.assertIs(graph_rewrite(emulated, symbolic_simple), emulated)
def test_weak_shift_lhs_commits_the_node(self):
# a shift derives its lhs's dtype, so committing the lhs restates the root (WGSL's packed store writes `mask << shift_am`)
+9 -12
View File
@@ -3,7 +3,7 @@ import itertools, functools
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT, TracingKey, Context, panic
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, Ops, UPat, rewrite_group, KernelInfo, ProgramInfo, GroupOp, AxisType
from tinygrad.uop.weak import pm_lower_index_dtype, pm_commit_weak, pm_cast_weak
from tinygrad.uop.weak import pm_lower_weak, pm_commit_weak, pm_cast_const
from tinygrad.uop.render import pyrender
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
from tinygrad.renderer import Renderer, Estimates
@@ -282,10 +282,6 @@ pm_implicit_barriers = PatternMatcher([
(UPat(Ops.END, name="end"), add_war_barrier),
])
pm_casted_consts = PatternMatcher([
(UPat(Ops.CONST, dtypes.all, name="c"), lambda c: UOp.cconst(c.val, c.dtype)),
])
def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
if VIZ: graph_rewrite(ast, PatternMatcher([]), name="View Base AST")
if DEBUG >= 5: print(pyrender(ast))
@@ -347,11 +343,13 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# extra symbolic before decomp. crashes without this?
# NOTE: also run indexing_simplify here, while the index is still weakint and (x+y)*c -> x*c+y*c applies
sink = graph_rewrite(sink, sym+indexing_simplify, name="extra symbolic")
# commit widths minted in this fixpoint before lowering inspects INDEX shapes
sink = graph_rewrite(sink, sym+indexing_simplify+pm_commit_weak, name="extra symbolic")
# lower index dtype
# the boundary: required compute dtypes settle here; derivable const edges may stay bare
# NOTE: we need indexing_simplify to remove the cast to long using the Invalid
sink = graph_rewrite(sink, symbolic_simple+pm_lower_index_dtype+indexing_simplify, ctx={}, name="lower all index dtypes")
# NOTE: symbolic must NOT be composed here -- pm_data_invalid pushes the weak result CAST into a gated WHERE, remaking the weak node, and it cycles
sink = graph_rewrite(sink, pm_lower_weak+indexing_simplify, name="lower all index dtypes")
# final symbolic before decomp
sink = graph_rewrite(sink, symbolic, name="final symbolic")
@@ -375,12 +373,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# final rules for the renderer (without sym)
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else PatternMatcher([])
pm_final_rewrite = pm_commit_weak+pm_cast_weak+pm_decomp+extra_matcher+pm_split_ends
pm_final_rewrite = pm_commit_weak+pm_decomp+extra_matcher+pm_split_ends
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
# spell every literal as a casted const CAST(dt, CONST(value))
# TODO: remove once consts are always weak
sink = graph_rewrite(sink, pm_casted_consts, name="casted consts", walk=True)
# commit every const still bare so no renderer reads one
sink = graph_rewrite(sink, pm_cast_const, name="cast consts")
# add implicit barriers (stores/loads through LOCAL memory ordered by AFTER or across loop iterations need workgroup barriers)
sink = graph_rewrite(sink, pm_implicit_barriers, name="add implicit barriers")
+7 -5
View File
@@ -3,6 +3,7 @@ from tinygrad.dtype import dtypes, DType, truncate
from tinygrad.helpers import flatten, DEBUG, EMULATED_DTYPES
from tinygrad.uop import GroupOp
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, graph_rewrite
from tinygrad.uop.weak import commit_weak_consts
from tinygrad.renderer import Renderer
from tinygrad.codegen.decomp.transcendental import exponent_bias, shl, shr
@@ -137,6 +138,8 @@ def f2f_store(st, idx, val, fr:DType, to:DType):
# tag is the 32-bit word this node becomes - (0 for the low word, 1 for the high, the dtype the consumer wants)
pm_long_decomp: PatternMatcher = PatternMatcher([
# the decomp's own bottom-up rewrite can mint bare consts mid-flight: word splitting commits them at the long sibling's dtype
(UPat(GroupOp.All, name='x'), lambda x: commit_weak_consts(x, next((s.dtype for s in x.src if s.dtype in l2i_dt), None))),
(UPat(GroupOp.Defines, tuple(l2i_dt.keys()), src=(UPat.var("sz"),), name="x"), lambda x,sz:
UOp(x.op, src=(sz*2,), arg=replace(x.arg, dtype=l2i_dt[x.dtype]), tag=x.tag)),
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
@@ -148,6 +151,9 @@ pm_long_decomp: PatternMatcher = PatternMatcher([
split_l2i(ctx, x.op, dt:=l2i_dt[a.dtype], *flatten((s.rtag((0, dt)), s.rtag((1, dt))) for s in x.src))),
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda ctx,a,x:
split_l2i(ctx, Ops.BITCAST, l2i_dt[x.dtype], a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt)))[x.tag[0]]),
# a const splits by value; the general CAST arm below would drop its high word
(UPat(Ops.CAST, src=(UPat(Ops.CONST, name='c'),), tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'),
lambda x,c: UOp.const(truncate[x.tag[1]](c.val >> (32*x.tag[0])), x.tag[1])),
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a'),), name="x"), lambda ctx,a,x:
split_l2i(ctx, x.op, x.dtype, a)[x.tag[0]] if x.tag is not None else None),
(UPat(Ops.CAST, src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda ctx,a,x:
@@ -161,9 +167,7 @@ pm_long_decomp: PatternMatcher = PatternMatcher([
split_l2i(ctx, x.op, l2i_dt[x.dtype], *flatten((a.rtag((0, l2i_dt[x.dtype])), a.rtag((1, l2i_dt[x.dtype]))) for a in x.src))[x.tag[0]]
if x.tag is not None else None),
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda ctx,x,idx:
reindex(graph_rewrite(idx, pm_long_decomp, ctx=ctx, bottom_up=True), x.tag[0]).replace(tag=None).load() if x.tag is not None else None),
(UPat(Ops.CONST, tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'), lambda x:
UOp.const(truncate[x.tag[1]]((x.val >> 32) if x.tag[0] == 1 else (x.val & 0xFFFFFFFF)), x.tag[1]))
reindex(graph_rewrite(idx, pm_long_decomp, ctx=ctx, bottom_up=True), x.tag[0]).replace(tag=None).load() if x.tag is not None else None)
])
# float decomposition patterns - ctx is (fr, to) tuple
@@ -186,8 +190,6 @@ pm_float_decomp: PatternMatcher = PatternMatcher([
f2f(x.bitcast(f2f_dt[ctx[0]]), ctx[0], ctx[1]) if bc.dtype == ctx[0] else None),
(UPat(Ops.CAST, dtypes.floats, src=(UPat.var("val"),), name="x"), lambda ctx,x,val:
f2f_clamp(val.cast(ctx[1]), ctx[0]) if x.dtype == ctx[0] else None),
# a CONST has no srcs to cast, it restates its value at the emulating dtype
(UPat(Ops.CONST, dtypes.floats, name="x"), lambda ctx,x: UOp.const(x.val, ctx[1]) if x.dtype == ctx[0] else None),
(UPat(GroupOp.All-GroupOp.Defines-{Ops.CAST, Ops.BITCAST, Ops.CONST}, dtypes.floats, name="x"), lambda ctx,x:
UOp(x.op, src=tuple(s.cast(ctx[1]) if s.dtype == ctx[0] else s for s in x.src), arg=x.arg, tag=x.tag) if x.dtype == ctx[0] else None),
(UPat(Ops.STORE, src=(UPat.var("idx"), UPat(Ops.BITCAST, dtypes.floats, name="val")), name='st'), lambda ctx,st,idx,val:
+3 -2
View File
@@ -3,6 +3,7 @@ import math, sys, struct
from collections import defaultdict, Counter
from tinygrad.codegen.opt import tc
from tinygrad.uop.ops import GroupOp, Ops, UOp, PatternMatcher, UPat, range_str, axis_letters
from tinygrad.uop.weak import commit_weak_consts
from tinygrad.helpers import strip_parens, getenv, prod, dedup, Target, NUM_CPU_THREADS, IMAGE, FLOAT16, is_image_shape
from tinygrad.dtype import dtypes, DType, AddrSpace, truncate, float_to_bf16
from tinygrad.renderer import Renderer
@@ -75,6 +76,8 @@ base_rewrite = PatternMatcher([
def create_non_native_float_pats(dts:tuple[DType, ...], casting:bool=True):
patterns = PatternMatcher([
# a weak CONST states no width and cannot be restated: commit it at the emulated dtype a sibling src states
(UPat(GroupOp.ALU, name="x"), lambda x, dts=dts: commit_weak_consts(x, next((s.dtype for s in x.src if s.dtype in dts), None))),
(UPat(Ops.WHERE, dtype=dts, src=(UPat.var("b"), UPat.var("x"), UPat.var("y")), name="w"),
lambda w,b,x,y: b.where(x.cast(dtypes.float), y.cast(dtypes.float)).cast(w.dtype)),
(UPat(GroupOp.ALU-{Ops.WHERE}, dtype=dts, name="x"),
@@ -524,8 +527,6 @@ class HIPRenderer(CStyleLanguage):
(UPat(Ops.WMMA, name="x", dtype=dtypes.float),
lambda x: x.replace(src=(x.src[0].bitcast(dtypes.uint64), x.src[1].bitcast(dtypes.uint64), x.src[2]))
if x.src[0].max_numel() == 8 and x.src[0].dtype in dtypes.fp8_ocp else None),
# bfloat16 constant casting
(UPat.cvar('x', dtypes.bfloat16), lambda x: cast_float_to_bf16(UOp.const(x.val, dtypes.float))),
])
def asm(self, prg:UOp, lin:UOp) -> bytes:
+4 -4
View File
@@ -150,9 +150,9 @@ extra_matcher = PatternMatcher([
# no cmpne for packed ints, y != x => !(y==x)
(UPat(Ops.CMPNE, src=(UPat.var("y", dtypes.ints), UPat.var("x")), name="cmp"),
lambda y,x,cmp: UOp(Ops.CMPEQ, src=(y,x))^True if y.max_numel() > 1 else None),
# float where expects a mask
(UPat.var("m", dtypes.bool).where(UPat.var("a", dtypes.floats), UPat.var("b")),
lambda m,a,b: m.cast(a.dtype).ne(0).where(a, b) if m.src[0].dtype not in dtypes.floats else None),
# float WHERE needs a mask unless its comparison already has a float operand
(UPat.var("m", dtypes.bool).where(UPat.var("a", dtypes.floats+(dtypes.weakfloat,)), UPat.var("b")).named("w"),
lambda m,a,b,w: m.cast(w.dtype).ne(0).where(a, b) if w.dtype in dtypes.floats and not dtypes.is_float(m.src[0].dtype) else None),
# rewrite -x -> 0 - x
(UPat(Ops.NEG, name="x"), lambda x: UOp(Ops.SUB, src=(x.const_like(0),) + x.src)),
# TODO: add support for mod, requires support for accessing the 2nd+ reg of a multi output instruction
@@ -653,7 +653,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
# 0b10 -- signals memory access with 32bit displacement
# 0b11 -- signals no memory access
if disp_uop is not None:
assert disp_uop.op is Ops.CAST, "displacement must be a literal"
assert disp_uop.op is Ops.CAST, "displacement must be a const"
assert disp_uop.dtype in (dtypes.int8, dtypes.int32), "displacement can only be 1 or 4 byte signed int"
# rbp/r13 always require a displacement
if disp_uop.src[0].val != 0 or rm == 0b101: mod = 0b01 if disp_uop.dtype.itemsize == 1 else 0b10
+2 -3
View File
@@ -121,8 +121,6 @@ class NIRRenderer(Renderer):
code_for_op = {**{k:lambda:None for k in u_aop.keys()}, **{k:lambda:None for k in s_aop.keys()}, **{k:lambda:None for k in f_aop.keys()}}
extra_matcher = PatternMatcher([
# handle negative unsigned CONST
(UPat.cvar("x", dtypes.uints), lambda x: UOp.const(x.dtype.max+x.val+1, x.dtype) if x.val < 0 else None),
# from ptx
(UPat.var('x', dtype=dtypes.bool)<UPat.var('y'), lambda x,y: (x^True)&y),
# load/store bool -> uint8
@@ -136,9 +134,10 @@ class NIRRenderer(Renderer):
# ref: https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpConvertFToU
(UPat(Ops.CAST, (dtypes.uchar, dtypes.ushort), src=(UPat.var("x", dtypes.floats),), name="c"), lambda x,c: x.cast(dtypes.int32).cast(c.dtype)),
# load/store use pointer arithmetic, and the cast does nothing. NOTE: this doesn't apply to image indexing cause it's 1-D
# nor to REG/ALU register picks, which keep their own index dtype
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"), UPat.var("off")), allow_any_len=True, name="x"), lambda x,buf,off: x.replace(
src=(buf,UOp.const(off.val, dtypes.long) if off.op is Ops.CONST else off.cast(dtypes.long))+x.src[2:])
if buf.addrspace != AddrSpace.REG and not is_image_shape(buf._shape) else None),
if buf.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL) and not is_image_shape(buf._shape) else None),
# images need index to be int for nir (coordinates only: the INDEX keeps its access dtype)
(UPat.var("buf").index(UPat.var("idx_y"), UPat.var("idx_x"), name="x"),
lambda x,buf,idx_y,idx_x: x.replace(src=(buf, idx_y.cast(dtypes.int), idx_x.cast(dtypes.int)))),
+2 -1
View File
@@ -69,7 +69,8 @@ class WGSLRenderer(CStyleLanguage):
(UPat.cvar("c").cast(dtypes.bool), lambda c: "true" if c.val else "false"),
(UPat.cvar("c").cast((dtypes.uchar, dtypes.ushort, dtypes.uint32)),
lambda c: f"bitcast<u32>({c.val})" if c.val < 0 else f"{c.val&0xFFFFFFFF}u"),
(UPat.cvar("c").cast(dtypes.int32, name="x"), lambda ctx,x,c: f"{truncate[x.dtype](c.val)}"),
# a negative const must state its type: contextual conversion of a bare abstract int rejects it in a u32 position
(UPat.cvar("c").cast(dtypes.int32, name="x"), lambda ctx,x,c: f"i32({v})" if (v:=truncate[x.dtype](c.val)) < 0 else f"{v}"),
(UPat(Ops.BUFFER, name="x"), lambda ctx,x:
f"var{'<workgroup>' if x.addrspace == AddrSpace.LOCAL else ''} {ctx[x]}: array<{ctx.buf_map(x)},{_packed_size(x)}>;"),
(UPat(Ops.BITCAST, dtype=dtypes.half, name="x", src=(UPat(dtype=(dtypes.short, dtypes.ushort, dtypes.uint32),),)),
+6 -4
View File
@@ -71,8 +71,8 @@ def make_binary_patch(buf:UOp, blob:bytes) -> UOp: return buf.store(UOp(Ops.BINA
def make_cmdbuf(lin, devs, buf:UOp|None=None, dep:tuple[UOp, ...]=()):
blob, patches = bytearray(), []
for s in (s for ins in lin.src for s in ins.src):
if s.op is not Ops.CONST: patches.append((len(blob), s))
blob.extend(struct.pack(f'<{s.dtype.fmt}', s.val if s.op is Ops.CONST else 0x0))
if not (is_const:=(s.op is Ops.CAST and s.src[0].op is Ops.CONST)): patches.append((len(blob), s))
blob.extend(struct.pack(f'<{s.dtype.fmt}', s.val if is_const else 0x0))
cmdbuf = buf if buf is not None else UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
return cmdbuf.after(*dep, make_binary_patch(cmdbuf, bytes(blob)), *make_patches(cmdbuf, patches))
@@ -344,7 +344,9 @@ def make_gather_loop(patches:list[UOp], table:UOp, slots:dict[UOp, int], lt_patc
lt_patches.append(make_binary_patch(pairs, struct.pack(f'<{2*len(words)}I', *itertools.chain(*words))))
r = UOp.range(len(words), next(UOp.unique_num), dtype=dtypes.int, src=(pairs, dst))
off, slot = ((pairs.index(2*r+i).load() % bound).cast(dtypes.int) for i, bound in ((0, dst.max_numel()-1), (1, table.max_numel())))
patch = dst.shrink(((off, off+table.dtype.itemsize//dst.dtype.itemsize),)).bitcast(table.dtype).index(0).store(table.index(slot).load()).end(r)
# SHRINK(offset, length): a const length keeps the end bound from becoming an expression the program spec rejects
patch = UOp(Ops.SHRINK, src=(dst, off, off.const_like(table.dtype.itemsize//dst.dtype.itemsize))).bitcast(table.dtype).index(0) \
.store(table.index(slot).load()).end(r)
return {p: UOp(Ops.NOOP) for p in patches} | {patches[0]: patch}
def is_input_addr(g:UOp) -> bool: return all(x.op is Ops.PARAM and x.tag is None for x in unwrap_mstack(g.buf_uop))
@@ -511,7 +513,7 @@ def fold_const_store(view:UOp, off:UOp, val:UOp) -> UOp:
buf, start = unwrap_view(view)
for off,val in zip(off.src, val.src):
for b,v in zip((bs:=mb.bufs if isinstance((mb:=buf.buffer), MultiBuffer) else (mb,)), val.src if val.op is Ops.STACK else (val,)*len(bs)):
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype]((v.src[0] if v.op is Ops.CAST else v).val))
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype](v.val))
bo = start*buf.dtype.itemsize + off.val*val.dtype.itemsize
b.ensure_allocated()._buf.cpu_view().view(fmt='B')[bo:bo+len(data)] = data
return UOp(Ops.NOOP)
+5 -5
View File
@@ -304,15 +304,15 @@ pm_const_buffer_folding = pm_mops+PatternMatcher([
(UPat(Ops.INDEX, name="idx").f(Ops.STAGE, allow_any_len=True, name="b2"), remove_noop_bufferize),
(UPat(Ops.INDEX, src=(UPat(Ops.STAGE),), allow_any_len=True, name="idx").f(Ops.NOOP).f(Ops.STAGE, allow_any_len=True, name="b2"),
remove_noop_bufferize),
# no buffers for const (ranges don't matter for const - it's the same value everywhere)
(UPat(Ops.CONST, name='c').f(Ops.STAGE, allow_any_len=True, name="b"), lambda c,b: b.const_like(c.val)),
# indexing a const is a const
(UPat(Ops.INDEX, src=(UPat(Ops.CONST, name="c"),),), lambda c: c),
# no buffers for a const, in either spelling
(UPat.cvar('c').or_casted().f(Ops.STAGE, allow_any_len=True, name="b"), lambda c,b: b.const_like(c.val)),
# indexing a const is the const
(UPat(Ops.INDEX, src=(UPat.cvar().or_casted("c"),),), lambda c: c),
# indexing an after with all fully invalid stores is invalid
(UPat(Ops.INDEX, src=(UPat(Ops.AFTER, name="after"),), allow_any_len=True, name="idx"),
lambda idx,after: idx.const_like(Invalid) if after_all_invalid(after) else None),
# hack if a noop turned to a const
(UPat(Ops.NOOP, src=(UPat.cvar("c"),)), lambda c: c),
(UPat(Ops.NOOP, src=(UPat.cvar().or_casted("c"),)), lambda c: c),
# a deviceless MSTACK src is the same value on every device, so indexing the stack is just indexing that value
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True, name="idx"),
lambda s,idx: idx.replace(src=(s,)+idx.src[1:]) if s.device is None else None),
+9 -6
View File
@@ -188,9 +188,8 @@ class UOpMetaClass(type):
def __call__(cls, op:Ops, dtype:DType|None=None, src:tuple[UOp,...]=tuple(), arg:Any=None, tag:Any=None,
metadata:tuple[Metadata,...]|None=None, _buffer:Buffer|None=None):
if dtype is None: dtype = dtype_from_uop(op, src, arg) or dtypes.void
# CONST derives its dtype by value only when the constructor omits one
# TODO: delete this once the dtype field is removed, for now it just re-implements spec.py
if SPEC == 2 and op is not Ops.CONST and (expected_dtype:=dtype_from_uop(op, src, arg)) is not None and expected_dtype != dtype:
if SPEC == 2 and (expected_dtype:=dtype_from_uop(op, src, arg)) is not None and expected_dtype != dtype:
raise RuntimeError(f"bad dtype {dtype}, expected {expected_dtype} on {op}")
if (wret:=UOpMetaClass.ucache.get(key:=(op, dtype, src, arg, tag), None)) is not None and (ret:=wret()) is not None: return ret
UOpMetaClass.ucache[key] = weakref.ref(created:=super().__call__(*key))
@@ -256,8 +255,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def rtag(self, tag=True): return self.replace(tag=tag)
@property
def val(self):
assert self.op is Ops.CONST, f"val is only for CONST, got {self.op}"
return self.arg
if self.op is Ops.CONST: return self.arg
# a casted const CAST(dt, CONST(v)) is one const: .val reads the value through the CAST
assert self.op is Ops.CAST and self.src[0].op is Ops.CONST, f"val is only for consts, got {self.op}"
return self.src[0].val
@property
def is_invalid(self) -> bool: return self.op is Ops.CONST and self.val is Invalid
@recursive_property
@@ -611,7 +612,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if isinstance(b, UOp): return b.cast(dtype)
# NOTE: it always has to be STACK now, even if they are all the same
if isinstance(b, tuple): return UOp.stack(*[UOp.const(c, dtype) for c in b])
return UOp(Ops.CONST, dtype, arg=dtype.const(b), src=())
# .cast folds away at exactly the dtypes a CONST derives (bool/weakint/weakfloat): bare there, the pair everywhere else
return UOp(Ops.CONST, arg=dtype.const(b), src=()).cast(dtype)
# weak CONST with width on the CAST. TODO: this is the final const
@staticmethod
def cconst(b:ConstLike, dtype:DType): return UOp(Ops.CAST, src=(UOp.const(b),), arg=dtype)
@@ -987,7 +989,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return unwrap(self.arg.name)
def bind(self, val:int|UOp):
assert self.is_variable, f"op is {self.op}, need Variable"
uval = self.const_like(val) if isinstance(val, int) else val
# the Variable states the width, so the bound value stays bare: is_bound_var tests for a CONST there, unbind reads .val
uval = UOp.const(val) if isinstance(val, int) else val
assert self.vmin <= uval.vmin and uval.vmax <= self.vmax, f"bind {val} not in range [{self.vmin}, {self.vmax}]"
assert uval.divides(self.arg.multiple_of) is not None, f"bind {val} not divisible by {self.arg.multiple_of}"
return self.after(self.store(uval))
+5 -5
View File
@@ -203,10 +203,9 @@ spec_tensor = PatternMatcher([
# these ops can exist in programs but not the tensor spec. example: LOAD
spec_program = PatternMatcher([
# a literal is CAST(dt, CONST(value)), so its inner CONST is the one weak node a program may contain
(UPat(Ops.CONST, dtype=dtypes.weaks, name="x"), lambda x: x.dtype is dtypes.from_py(x.val)),
# index and weak dtypes are not allowed in programs
(UPat(GroupOp.All, (dtypes.weakint, dtypes.weakfloat)), lambda: False),
# every width in a program is stated: a CONST appears only under the CAST stating its width, and is the only weak node
(UPat(GroupOp.All, name="x"), lambda x: False if x.op is not Ops.CAST and any(s.op is Ops.CONST for s in x.src) else None),
(UPat(GroupOp.All-{Ops.CONST}, dtypes.weaks), lambda: False),
# allow special SHRINK
(UPat(Ops.SHRINK, src=(UPat((Ops.PARAM, Ops.BUFFER, Ops.AFTER)), UPat(), UPat(Ops.CONST).or_casted())), lambda: True),
@@ -254,8 +253,9 @@ spec_kernel_graph = PatternMatcher([
(UPat(Ops.SINK, dtypes.void), lambda: True),
# the store of a bound Variable binds it: AFTER(BUFFER, STORE(BUFFER, CONST)) in call args
(UPat(Ops.STORE, dtypes.void, (UPat(Ops.BUFFER, name="b"), UPat(Ops.CONST))), lambda b: b.is_variable),
# const + stack to make vconsts and shape args
# const + stack to make vconsts and shape args. a 0-size/bound reduce keeps its const casted
(UPat(Ops.CONST, src=()), lambda: True),
(UPat(Ops.CAST, src=(UPat(Ops.CONST, src=()),)), lambda: True),
(UPat(Ops.STACK, name="s"), lambda s: all(x.op in (Ops.CONST, Ops.PARAM) or x.is_variable or x.is_bound_var for x in s.src) or None),
# linear for more kernels (TODO: we should enter non sink calls)
#(UPat(Ops.LINEAR), lambda: True),
+29 -22
View File
@@ -1,12 +1,12 @@
# all of symbolic lives here now
import math
from collections import defaultdict
from tinygrad.uop.ops import Ops, PatternMatcher, UPat, UOp, GroupOp, exec_alu
from tinygrad.dtype import PyConst, ConstType, dtypes, can_lossless_cast, Invalid, bitcast, truncate
from tinygrad.uop.ops import Ops, PatternMatcher, UPat, UOp, GroupOp, exec_alu, promo_dtype
from tinygrad.dtype import PyConst, dtypes, can_lossless_cast, Invalid, bitcast, truncate
from tinygrad.helpers import partition, all_same, prod, flatten, unwrap, IMAGE, dedup
from tinygrad.uop.divandmod import div_and_mod_symbolic
from tinygrad.uop.movement import mop_cleanup
from tinygrad.uop.weak import commit_weak
from tinygrad.uop.weak import pm_uncast_const, commit_weak
# TODO: symbolic shouldn't be importing from codegen
from tinygrad.codegen.decomp.transcendental import xpow
@@ -22,20 +22,11 @@ def simplify_pow(x:UOp, c:UOp) -> UOp|None:
def fold_bitcast(root:UOp, c:UOp) -> UOp|None:
if c.dtype.fmt is None or root.dtype.fmt is None or c.dtype.itemsize != root.dtype.itemsize: return None
# the value is mathematical and may not fit: reading it as bits is the emission that pins it to the stated width
return root.const_like(bitcast(truncate[c.dtype](c.val), c.dtype, root.dtype))
# const folding works for CONST and STACK
const_folding_pat = UPat((Ops.CONST, Ops.STACK))
def const_arg(u:UOp) -> ConstType|tuple[ConstType, ...]|None:
if u.op is Ops.CONST: return u.val
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return u.dtype.const(u.src[0].val)
if u.op is Ops.STACK and all(s.op is Ops.CONST for s in u.src): return tuple(s.val for s in u.src)
return None
def fold_const_alu(a:UOp) -> UOp|None:
vals = [const_arg(s) for s in a.src]
return None if any(v is None for v in vals) else a.const_like(exec_alu(a.op, a.dtype, vals, False))
# no truncate: ints stay mathematical past the fold (emission truncates); floats re-round in the mint
def fold_const_alu(a:UOp) -> UOp: return a.const_like(exec_alu(a.op, a.dtype, [const_arg(s) for s in a.src], False))
def _quotient_base(q:UOp, base:UOp, div:int) -> UOp|None:
# the B with q == B//div and B%div == base%div, or None. only such congruence is needed to recombine, and canonicalization
@@ -71,6 +62,12 @@ def fold_add_divmod_recombine(x:UOp) -> UOp|None:
# this needs to be before symbolic so that 0*something_that_might_be_invalid doesnt become 0
invalid_pat = UPat(Ops.CONST, arg=Invalid, name="i")
invalid_gate = UPat.var("cond").where(UPat.var("x"), invalid_pat)
# the two const spellings: Invalid carries no width, so it rides bare inside either
bare_const = UPat.any(UPat(Ops.CONST), UPat(Ops.STACK, src=UPat(Ops.CONST)))
casted_const = UPat.any(p:=UPat(Ops.CAST, src=(UPat(Ops.CONST),)), UPat(Ops.STACK, src=UPat.any(p, UPat(Ops.CONST, arg=Invalid))))
def const_arg(u:UOp):
return tuple(const_arg(s) for s in u.src) if u.op is Ops.STACK else u.val
pm_data_invalid = PatternMatcher([
(invalid_pat.broadcast(), lambda i: i),
(UPat(GroupOp.Unary|{Ops.CAST, Ops.BITCAST}, src=(invalid_pat,)), lambda i: i),
@@ -142,13 +139,19 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) != UPat.var("x"),
lambda x: x.const_like(False, dtypes.bool)), # x != x -> False (only ints)
# ** constant folding **
# a CAST to a concrete dtype over a CONST is a value conversion: evaluate it once, at the width the CAST states
# a CAST to a concrete dtype over a CONST is a value conversion: evaluate it once, at the CAST's dtype
# TODO: delete this once CONST has no dtype
(UPat(Ops.CAST, dtypes.all, name="root", src=(UPat.cvar("c"),)), lambda root, c: root.const_like(c.val)),
(UPat(GroupOp.Unary, src=(const_folding_pat,), name="a"), fold_const_alu),
# collapse committed const conversions when the target has a native constant format. fmt-less targets are emulated and would re-expand this pair.
(UPat(Ops.CAST, dtypes.all, name="root", src=(UPat(Ops.CAST, dtypes.all, src=(UPat(Ops.CONST, name="c"),)),)),
lambda root,c: root.const_like(c.val) if root.dtype.fmt is not None else None),
# one rule per spelling: bare has no width, a pair evaluates at its stated width, mixed commits to the promotion
# NOTE: THREEFRY(const,const) folds via its decomposition
(UPat(GroupOp.Binary-{Ops.THREEFRY}, src=(const_folding_pat,)*2, name="a"), fold_const_alu),
(UPat(GroupOp.Ternary, src=(const_folding_pat,)*3, name="a"), fold_const_alu),
(UPat(GroupOp.ALU-{Ops.THREEFRY}, src=bare_const, name="a"), fold_const_alu),
(UPat(GroupOp.ALU-{Ops.THREEFRY}, src=casted_const, name="a"), fold_const_alu),
(UPat(GroupOp.Binary-{Ops.THREEFRY}, src=[casted_const, bare_const], name="a"), lambda a:
a.replace(dtype=None, src=tuple(commit_weak(s, dt) if s.dtype in dtypes.weaks else s for s in a.src))
if (dt:=promo_dtype(a.src)) not in dtypes.weaks else None),
# bool MUL is AND, ADD/MAX is OR. prevents other rules to rewrite bool ADD/MUL incorrectly
(UPat.var('x', dtype=dtypes.bool) * UPat.var('y', dtype=dtypes.bool), lambda x,y: x&y),
(UPat.var('x', dtype=dtypes.bool) + UPat.var('y', dtype=dtypes.bool), lambda x,y: x|y),
@@ -166,7 +169,9 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
and isinstance(x.val, float) and (math.isnan(x.val) or math.isinf(x.val)) else 0)),
# *** cast/bitcast ***
(UPat((Ops.CAST, Ops.BITCAST), name="root"), lambda root: root.src[0] if root.dtype == root.src[0].dtype else None),
(UPat(Ops.BITCAST, name="root", src=(UPat.cvar("c"),)), fold_bitcast),
# a BITCAST reads its operand at the width it states, so a weak const is nonsense here: the bare arm is bool only
(UPat(Ops.BITCAST, name="root", src=(UPat.any(UPat(Ops.CONST, dtypes.bool, name="c"), UPat(Ops.CAST, src=(UPat(Ops.CONST),), name="c")),)),
fold_bitcast),
# b.cast(a).cast(b) -> b if a preserves all values in b
(UPat.var('x').cast(name="a").cast(name="b"), lambda x,a,b: x if x.dtype == b.dtype and can_lossless_cast(b.dtype, a.dtype) else None),
# bitcast twice
@@ -292,8 +297,9 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
# cast/long folding
# if the intermediate cast doesnt narrow we can do it in one cast
(UPat.var('x').cast(name="a").cast(name="b"), lambda x,a,b: x.cast(b.dtype) if can_lossless_cast(x.dtype, a.dtype) else None),
# commit_weak, not .cast: a weak b.dtype is not a const spelling, and a CAST(weakfloat, CONST) reaches no commit round
(UPat.var('x', dtypes.ints+(dtypes.weakint,)).cast(dtypes.ints+(dtypes.weakint,), name="a").cast(name="b"),
lambda x,a,b: x.cast(b.dtype) if a.dtype.min<=x.vmin and x.vmax<=a.dtype.max else None),
lambda x,a,b: commit_weak(x, b.dtype) if a.dtype.min<=x.vmin and x.vmax<=a.dtype.max else None),
# try to do math in int instead of long, keep weak const weak
(UPat(GroupOp.Binary, src=(UPat.var("x", (dtypes.long, dtypes.weakint)), UPat.var("y", (dtypes.long, dtypes.weakint))), name="u"), lambda u,x,y:
(UOp.const(x.val) if x.op is Ops.CONST else x.cast(dtypes.int)).alu(u.op,
@@ -306,7 +312,8 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
else y.src for y in x.src[1:]]))))),
# after/end with 1 src is just src[0]
(UPat((Ops.AFTER, Ops.END), src=(UPat.var("s"),)), lambda s: s),
])+div_and_mod_symbolic
# the rules above key on bare CONSTs, so a redundantly committed const has to be uncast in the same fixpoint
])+div_and_mod_symbolic+pm_uncast_const
# ******** we take a small aside to "simplify_valid" to rewrite valids ********
+63 -41
View File
@@ -1,19 +1,32 @@
from dataclasses import replace
from tinygrad.dtype import dtypes, DType, AddrSpace, Invalid, least_upper_dtype, strong_dtype, weak_dtype
from tinygrad.helpers import unwrap
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, GroupOp, graph_rewrite, dtype_from_uop
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, GroupOp, dtype_from_uop, promo_dtype
def default_dtype(u:UOp):
if u.dtype is dtypes.weakfloat: return dtypes.default_float
return dtypes.long if u.overflows(dtypes.int32) else dtypes.int
def commit_weak(s:UOp, dt:DType) -> UOp:
# a CONST commits directly at dt (the value stays mathematical, emission truncates), a non-const src takes the cast
# a CONST re-mints, never takes a cast: at bool/weakint/weakfloat a CAST would be a second spelling of one const
return UOp.const(s.val, dt) if s.op is Ops.CONST else s.cast(dt)
def commit_srcs_at(u:UOp, dt:DType) -> UOp:
# the decomps and float emulation commit bare consts at a dtype another src already states
def commit_weak_consts(u:UOp, dt:DType|None) -> UOp|None:
return None if dt is None else u.replace(src=tuple(commit_weak(s, dt) if s.op is Ops.CONST and s.dtype in dtypes.weaks else s for s in u.src))
# the concrete dtypes u commits its srcs at: the operands' meet and u's own derived dtype, None if either is weak
def derived_dtypes(u:UOp, src:tuple[UOp, ...]) -> tuple[DType, DType]|None:
if u.op not in GroupOp.Broadcastable or (meet:=promo_dtype(src)) in dtypes.weaks \
or (result:=unwrap(dtype_from_uop(u.op, src, u.arg))) in dtypes.weaks: return None
return meet, result
def commit_srcs_at(u:UOp, dt:DType) -> UOp|None:
# the root re-derives: a shift's dtype is its lhs's, so committing the lhs commits the node too
return u.replace(dtype=None, src=tuple(commit_weak(s, dt) if s.dtype in dtypes.weaks else s for s in u.src))
dts = derived_dtypes(u, u.src)
ret = u.replace(dtype=None, src=tuple(UOp.const(dt.const(s.val)) if s.op is Ops.CONST and s.dtype in dtypes.weaks and dts is not None else
commit_weak(s, dt) if s.dtype in dtypes.weaks else s for s in u.src))
return None if ret is u else ret
def commit_weak_srcs(u:UOp) -> UOp|None:
if not any(s.dtype in dtypes.weaks for s in u.src) or (dt:=least_upper_dtype(*(s.dtype for s in u.src))) in dtypes.weaks: return None
@@ -21,62 +34,71 @@ def commit_weak_srcs(u:UOp) -> UOp|None:
# a concrete CAST over a weak node states the width the value will live at. that width is a floor, never a narrowing
def cast_weak_srcs(c:UOp, u:UOp) -> UOp|None:
# only within the kind: an int cast of a weakfloat node is a value conversion, not a statement about the node's width
if c.dtype in dtypes.weaks or weak_dtype(c.dtype) is not u.dtype: return None
return commit_srcs_at(u, least_upper_dtype(c.dtype, default_dtype(u))).cast(c.dtype)
return None if (ret:=commit_srcs_at(u, least_upper_dtype(c.dtype, default_dtype(u)))) is None else ret.cast(c.dtype)
pm_cast_weak = PatternMatcher([
(UPat(Ops.CAST, name="c", src=(UPat(GroupOp.ALU, dtype=dtypes.weaks, name="u"),)), cast_weak_srcs),
(UPat(Ops.CAST, name="c", src=(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"),)), lambda c,u: commit_weak(u, c.dtype)),
])
# runs in index lowering and in the decomps: a rule that mints a weak const commits it in the same rewrite, so none reaches the renderer
# rides every round that can mint a weak const, and must reach fixpoint before pm_lower_weak below defaults one
pm_commit_weak = PatternMatcher([
(UPat(GroupOp.Broadcastable, name="u"), commit_weak_srcs),
# demand from the destination: a STORE's weak value commits at the destination's dtype
(UPat(Ops.STORE, src=(UPat(), UPat(dtype=dtypes.weaks)), allow_any_len=True, name="u"),
lambda u: u.replace(src=(u.src[0], commit_weak(u.src[1], u.src[0].dtype), *u.src[2:]))),
# no CONST arm: a concrete CAST over a weak CONST is already committed, minted that way by UOp.const
(UPat(Ops.CAST, name="c", src=(UPat(GroupOp.ALU, dtype=dtypes.weaks, name="u"),)), cast_weak_srcs),
])
# A weakfloat Unary (sin/exp2/...) must resolve here, before the transcendental decomposition.
# consumers absorb the weak CAST off their srcs and default underivable consts; dtype-producing ops settle here.
# a weakfloat Unary (sin/exp2/...) must resolve before the transcendental decomposition.
_lower_weak_ops = GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}
def lower_weak_node(u:UOp) -> UOp|None:
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return None # a committed const, not a consumer
src = tuple(s.src[0] if s.op is Ops.CAST and s.dtype in dtypes.weaks else s for s in u.src)
dts = derived_dtypes(u, src)
src = tuple(commit_weak(s, default_dtype(s)) if s.op is Ops.CONST and s.dtype in dtypes.weaks and dts is None else s
for s in src)
start = 1 if u.op is Ops.WHERE else 0 # WHERE's cond is bool, never part of the width unification
if src == u.src or any(s.dtype in dtypes.weaks for s in src[start:]): return None
# Binary can widen from the bounds, all other nodes derive from the lowered sources.
dt = strong_dtype(least_upper_dtype(default_dtype(u), *(s.dtype for s in src)) if u.op in GroupOp.Binary
else unwrap(dtype_from_uop(u.op, src, u.arg)))
return u.replace(dtype=None, src=src[:start]+tuple(s if s.base.is_invalid else commit_weak(s, dt) for s in src[start:])).cast(u.dtype)
# resolve whole once every weak expression lowered: a Binary widens from its own bounds too, derivable consts wait
if u.op in _lower_weak_ops and src != u.src and not any(s.dtype in dtypes.weaks and s.op is not Ops.CONST for s in src[start:]):
dt = strong_dtype(least_upper_dtype(default_dtype(u), *(s.dtype for s in src)) if u.op in GroupOp.Binary
else unwrap(dtype_from_uop(u.op, src, u.arg)))
return u.replace(dtype=None, src=src[:start]+tuple(s if s.base.is_invalid or s.dtype in dtypes.weaks else commit_weak(s, dt)
for s in src[start:])).cast(u.dtype)
return None if src == u.src else u.replace(dtype=None, src=src)
pm_lower_weak = PatternMatcher([
(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"), lambda u: UOp.const(u.val, default_dtype(u)).cast(u.dtype)),
# a gated long index into a small buffer narrows; its out-of-gate value is discarded
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"), UPat.var("gate").where(UPat.var("idx", dtypes.long), UPat(Ops.CONST, arg=Invalid))),
allow_any_len=True, name="u"),
lambda u,buf,gate,idx: u.replace(src=(buf, idx.cast(dtypes.int).valid(gate))+u.src[2:]) if buf.max_numel()-1 <= dtypes.int32.max else None),
# two stacked weak casts are two kind conversions: each resolves at its own kind's default
# a SINGLE weak cast is never rewritten here, each consumer absorbs it on its own edge (see lower_weak_srcs)
(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat.var("x"),)),), name="u"),
lambda u,x: x.cast(default_dtype(u.src[0])).cast(default_dtype(u)).cast(u.dtype) if x.dtype not in dtypes.weaks else None),
(UPat((Ops.PARAM, Ops.BUFFER), dtype=dtypes.weakint, name="u"),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=default_dtype(u))).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
(UPat(_lower_weak_ops, name="u"), lower_weak_node),
(UPat(GroupOp.All, name="u"), lower_weak_node),
])
def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
if ctx is None: ctx = {}
def lower(s:UOp) -> UOp:
if (r:=ctx.get(s)) is None:
r = graph_rewrite(s, pm_lower_weak)
# the consumer absorbs the cast on its own edge
ctx[s] = r = r.src[0] if r.op is Ops.CAST and r.dtype in dtypes.weaks else r
return r
# a comparison demands a common operand width: lower it whole so the Binary rule unifies its operands
ret = lower(u) if u.op in GroupOp.Comparison else u.replace(src=tuple(lower(s) if s.dtype in dtypes.weaks else s for s in u.src))
return None if ret is u else ret
# drop the CAST off a committed const where the consumer re-derives it anyway, so bare-CONST rules keep matching.
# the drop must change nothing the consumer derives: neither the operands' meet nor the node's own dtype
def uncast_const(u:UOp) -> UOp|None:
# a weak CAST over a const is not a commit, it is still resolving
src = tuple(s.src[0] if s.op is Ops.CAST and s.dtype not in dtypes.weaks and s.src[0].op is Ops.CONST
and s.src[0].dtype in dtypes.weaks else s for s in u.src)
if src == u.src or (dts:=derived_dtypes(u, src)) is None or dts[0] != promo_dtype(u.src) or dts[1] is not u.dtype: return None
return u.replace(src=src)
pm_lower_index_dtype = pm_commit_weak+pm_cast_weak+PatternMatcher([
(UPat(GroupOp.All, name="u"),
lambda ctx,u: lower_weak_srcs(ctx, u) if u.dtype not in dtypes.weaks and any(s.dtype in dtypes.weaks for s in u.src) else None),
# a valid index into an n-element buffer lives in [0,n): a gated long index narrows when n-1 fits int32 (out-of-gate wraps, discarded)
# TODO: more generic
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"), UPat.var("gate").where(UPat.var("idx", dtypes.long), UPat(Ops.CONST, arg=Invalid))),
allow_any_len=True, name="u"),
lambda u,buf,gate,idx: u.replace(src=(buf, idx.cast(dtypes.int).valid(gate))+u.src[2:]) if buf.max_numel()-1 <= dtypes.int32.max else None),
])
pm_uncast_const = PatternMatcher([(UPat(GroupOp.Broadcastable, name="u"), uncast_const)])
def cast_const(u:UOp, s:UOp) -> UOp:
if s.op is not Ops.CONST or s.is_invalid: return s # Invalid never commits
# bool is the one strong bare dtype: cconst, since .cast(bool) would fold at construction
if s.dtype is dtypes.bool: return UOp.cconst(s.val, s.dtype)
# commit at the dtype its consumer derives; where nothing does, commit_weak is the identity and spec_program rejects it
return commit_weak(s, dts[0]) if (dts:=derived_dtypes(u, u.src)) is not None else s
# commit every remaining bare const, keyed on the consumer: "bare" is a property of the edge
def cast_consts(u:UOp) -> UOp|None:
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return None # a committed const's CONST is its value, not an edge
return None if (src:=tuple(cast_const(u, s) for s in u.src)) == u.src else u.replace(src=src)
pm_cast_const = PatternMatcher([(UPat(GroupOp.All, name="u"), cast_consts)])