Compare commits

...
Author SHA1 Message Date
geohot f4e1b93225 fix beam 2025-08-29 19:05:49 -07:00
geohot 4a31c319b3 work 2025-08-29 18:42:50 -07:00
geohot 59081645f7 beam in RKernel 2025-08-29 18:42:50 -07:00
George HotzandGitHub 6e57905c6d Merge branch 'master' into working_postopt 2025-08-29 12:37:01 -07:00
George HotzandGitHub 40606c60b0 Merge branch 'master' into working_postopt 2025-08-29 11:23:17 -07:00
George HotzandGitHub 80986321eb Merge branch 'master' into working_postopt 2025-08-29 09:43:05 -07:00
George HotzandGitHub bd263cbcb0 Merge branch 'master' into working_postopt 2025-08-28 15:13:02 -07:00
geohot 2e41472b02 support tc 2, all are pad 2025-08-28 14:40:50 -07:00
geohot ac641f7b10 support tc padding 2025-08-28 14:34:42 -07:00
geohot 226c59fa5a bugfix 2025-08-28 14:13:34 -07:00
geohot 3bbfcbccde work 2025-08-28 13:55:07 -07:00
geohot 78a56b3461 fix some tests 2025-08-28 12:55:12 -07:00
George HotzandGitHub b5ac4501d4 Merge branch 'master' into working_postopt 2025-08-28 12:35:39 -07:00
geohot 038d3bc295 clean up test 2025-08-28 11:39:02 -07:00
geohot 4b223c820a fix test 2025-08-28 11:29:02 -07:00
geohot 528e285d81 more tests 2025-08-28 11:15:47 -07:00
geohot cd3dc67636 tensor cores need to pad 2025-08-28 11:04:30 -07:00
geohot b19a8963c3 revert 2025-08-28 11:00:20 -07:00
George HotzandGitHub ec10e00cf5 Merge branch 'master' into working_postopt 2025-08-28 10:50:00 -07:00
geohot d3aa38ad4a work 2025-08-28 10:46:18 -07:00
geohot 6e41040e91 get tc ranges 2025-08-28 08:24:33 -07:00
geohot 6b3d9d6663 fix some tests 2025-08-28 08:07:45 -07:00
geohot 0ece218588 fix uop test 2025-08-28 07:34:06 -07:00
geohot feb12685f2 postopt using RKernel 2025-08-28 07:28:52 -07:00
6 changed files with 201 additions and 31 deletions
+3 -3
View File
@@ -445,10 +445,10 @@ class TestUOpGraph(unittest.TestCase):
with Context(IGNORE_OOB=0): with Context(IGNORE_OOB=0):
glbl0 = UOp(Ops.DEFINE_GLOBAL, dtypes.int.ptr(16), src=(), arg=0) glbl0 = UOp(Ops.DEFINE_GLOBAL, dtypes.int.ptr(16), src=(), arg=0)
v = Variable("v", 0, 20) v = Variable("v", 0, 20)
st0 = UOp(Ops.STORE, dtypes.void, src=(glbl0.index(v), UOp.const(dtypes.int, 0), UOp(Ops.IF, src=(v<16,)))) st0 = UOp(Ops.STORE, dtypes.void, src=(glbl0.index(v, v<16), UOp.const(dtypes.int, 0)))
to_uops_list([st0]) to_uops_list([st0])
st1 = UOp(Ops.STORE, dtypes.void, (glbl0.index(v), v, v<20)) st1 = UOp(Ops.STORE, dtypes.void, (glbl0.index(v, v<20), v))
with self.assertRaises(RuntimeError): to_uops_list([st1]) with self.assertRaises(RuntimeError): to_uops_list([st1])
def test_in_bounds_access_gated_local(self): def test_in_bounds_access_gated_local(self):
@@ -463,7 +463,7 @@ class TestUOpGraph(unittest.TestCase):
gate = (gidx<400) & (lidx<8) gate = (gidx<400) & (lidx<8)
local_store = UOp(Ops.STORE, dtypes.void, (sbuf.index(lidx), UOp.const(dtypes.uint, 1), UOp(Ops.IF, src=(lidx<8,)))) local_store = UOp(Ops.STORE, dtypes.void, (sbuf.index(lidx, lidx<8), UOp.const(dtypes.uint, 1)))
barrier = UOp(Ops.BARRIER, dtypes.void, (local_store,)) barrier = UOp(Ops.BARRIER, dtypes.void, (local_store,))
if_barrier = UOp(Ops.IF, dtypes.void, (gate, barrier)) if_barrier = UOp(Ops.IF, dtypes.void, (gate, barrier))
+1 -1
View File
@@ -15,7 +15,7 @@ def shape_to_idx(s, axis_types, start=0):
def get_index(ast:UOp) -> IndexContext: def get_index(ast:UOp) -> IndexContext:
axis_types = ast.arg.axis_types if isinstance(ast.arg, KernelInfo) else () axis_types = ast.arg.axis_types if isinstance(ast.arg, KernelInfo) else ()
if len(ast.full_shape) != len(axis_types): if len(ast.full_shape) != len(axis_types) and ast.st is not None:
axis_types = tuple([AxisType.REDUCE if resolve(s != fs) else AxisType.LOOP for s,fs in zip(ast.shape, ast.full_shape)]) axis_types = tuple([AxisType.REDUCE if resolve(s != fs) else AxisType.LOOP for s,fs in zip(ast.shape, ast.full_shape)])
return IndexContext(axis_types, [], 0) return IndexContext(axis_types, [], 0)
+3 -9
View File
@@ -22,17 +22,11 @@ def get_optimized_ast(ast:UOp, renderer:Renderer) -> UOp|None:
# no shape, no opt # no shape, no opt
if ast.src[0].st is None: return None if ast.src[0].st is None: return None
new_arg = ast.arg new_arg = ast.arg
if new_arg is None: if new_arg is None and not NOOPT and not BEAM:
k = Kernel(ast, opts=renderer) k = Kernel(ast, opts=renderer)
if not NOOPT: if not k.apply_tensor_cores(USE_TC.value): k.apply_opts(hand_coded_optimizations(k))
if not k.apply_tensor_cores(USE_TC.value): k.apply_opts(hand_coded_optimizations(k))
if BEAM >= 1:
from tinygrad.codegen.opt.search import beam_search, bufs_from_lin
kb = Kernel(ast, opts=renderer)
rawbufs = bufs_from_lin(kb, allocate=False)
k = beam_search(kb, rawbufs, BEAM.value, bool(getenv("BEAM_ESTIMATE", 1)))
new_arg = KernelInfo(opts_to_apply=tuple(k.applied_opts)) new_arg = KernelInfo(opts_to_apply=tuple(k.applied_opts))
elif len(new_arg.applied_opts): return None elif new_arg is not None and len(new_arg.applied_opts): return None
return Kernel(ast.replace(arg=None), opts=renderer).get_optimized_ast().replace(arg=new_arg) return Kernel(ast.replace(arg=None), opts=renderer).get_optimized_ast().replace(arg=new_arg)
pm_get_optimization = PatternMatcher([ pm_get_optimization = PatternMatcher([
+1 -4
View File
@@ -92,10 +92,6 @@ class Kernel:
global_loops = AxisType.GLOBAL if self.opts.has_local else AxisType.LOOP global_loops = AxisType.GLOBAL if self.opts.has_local else AxisType.LOOP
self.axis_types: list[AxisType] = [AxisType.REDUCE if resolve(x!=y) else global_loops for x,y in zip(self.output_shape, self.full_shape)] self.axis_types: list[AxisType] = [AxisType.REDUCE if resolve(x!=y) else global_loops for x,y in zip(self.output_shape, self.full_shape)]
# confirm all reduce axes are at the end
if (final_reduces := [x for x in self.axis_types if x == AxisType.REDUCE]) and final_reduces != self.axis_types[-len(final_reduces):]:
raise RuntimeError(f"reduces are not at the end of the shape {self.full_shape} -> {self.output_shape}")
def copy(self): def copy(self):
ret = type(self).__new__(type(self)) ret = type(self).__new__(type(self))
@@ -449,6 +445,7 @@ class Kernel:
def shape_str_to_axis(self, nms:list[str]) -> tuple[int, ...]: return tuple([self.shape_str().index(x) for x in nms]) def shape_str_to_axis(self, nms:list[str]) -> tuple[int, ...]: return tuple([self.shape_str().index(x) for x in nms])
def get_optimized_ast(self, name_override:str|None=None) -> UOp: def get_optimized_ast(self, name_override:str|None=None) -> UOp:
if self.applied_opts: raise RuntimeError("not supported")
@functools.cache @functools.cache
def fixup_ast(op:UOp) -> UOp: def fixup_ast(op:UOp) -> UOp:
ret = op.replace(src=tuple(fixup_ast(x) for x in op.src)) # noqa: F821 ret = op.replace(src=tuple(fixup_ast(x) for x in op.src)) # noqa: F821
+192 -13
View File
@@ -1,18 +1,197 @@
from dataclasses import replace import math, itertools
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, KernelInfo from tinygrad.dtype import AddrSpace
from tinygrad.helpers import colored from tinygrad.uop.ops import UOp, Ops, sint, ssimplify, AxisType, KernelInfo, PatternMatcher, UPat, graph_rewrite
from tinygrad.codegen.opt.kernel import axis_colors from tinygrad.helpers import DEBUG, BEAM, getenv
from tinygrad.codegen.opt.kernel import Kernel, Opt, OptOps, KernelOptError
from tinygrad.renderer import Renderer
from tinygrad.dtype import dtypes
from tinygrad.device import Buffer
def rename_sink(s:UOp): def flatten_range(r:UOp):
if s.arg is not None and s.arg.name != "test": return None off = 2 if r.op is Ops.STORE else 1
rngs = r.src[off:]
if not len(rngs): return None
new_rngs = [x for x in UOp.sink(*rngs).toposort() if x.op is Ops.RANGE]
return r.replace(src=r.src[:off]+tuple(new_rngs))
# get all ranges (sorted) pm_flatten_range = PatternMatcher([
rngs = sorted([u for u in s.parents if u.op is Ops.RANGE], key=lambda x: x.arg[0:-1]) # real ranges only
(UPat((Ops.REDUCE, Ops.STORE), name="r"), flatten_range),
])
# add name to kernel class RKernel(Kernel):
name = "k" + colored('_', 'BLACK').join(['']+[colored(x.src[0].render(), axis_colors[x.arg[-1]]) for x in rngs]) def __init__(self, ast:UOp, opts:Renderer|None=None):
return s.replace(arg=KernelInfo(name=name) if s.arg is None else replace(s.arg, name=name)) self.rng = sorted([u for u in ast.toposort() if u.op is Ops.RANGE and u.vmax > 0], key=lambda x: x.arg)
super().__init__(ast, opts)
self.sts.clear()
pm_postrange_opt = PatternMatcher([ # convert LOOP to GLOBAL
(UPat(Ops.SINK, name="s"), rename_sink), self.replaces = {}
if self.opts.has_local:
store_rngs = self.ast.src[0].src[2:]
# filter any not in local stores
local_store_rngs = [x.ranges for x in self.ast.toposort() if (x.op is Ops.STORE and x.src[0].dtype.addrspace == AddrSpace.LOCAL) \
or (x.op is Ops.BUFFERIZE and x.arg == AddrSpace.LOCAL)]
for ls in local_store_rngs: store_rngs = tuple([x for x in store_rngs if x in ls])
store_rng = [x for x in UOp.sink(*store_rngs).toposort() if x.op is Ops.RANGE] if store_rngs else []
rng = [x.replace(arg=(x.arg[0], AxisType.GLOBAL)) if x.arg[1] == AxisType.LOOP and x in store_rng else x for x in self.rng]
self.replaces.update(dict(zip(self.rng, rng)))
self.rng = rng
# NOTE: needed for tensor cores
self.substitute()
self.maxarg = max([x.arg[0] for x in self.rng]) if len(self.rng) else 0
def substitute(self) -> UOp:
self.ast = graph_rewrite(self.ast.substitute(self.replaces), pm_flatten_range)
self.replaces = {}
return self.ast
def copy(self):
self.substitute()
return RKernel(self.ast, self.opts)
# must be done earlier
def simplify_merge_adjacent(self): return
def apply_opt(self, opt:Opt, append_opt:bool=True) -> UOp|None:
if opt.op == OptOps.PADTO: raise KernelOptError("PAD is not supported yet. needs INVALID")
if opt.op == OptOps.SWAP: raise KernelOptError("SWAP is not supported yet")
return super().apply_opt(opt, append_opt)
def shift_to(self, axis:int, amount:int, new_type:AxisType, top:bool=False, insert_at:int|None=None):
old_sz = self.rng[axis].src[0].arg // amount
assert old_sz > 0, f"bad old_sz on {axis} {amount} {self.rng[axis]}"
self.maxarg += 1
new_rng = UOp.range(amount, self.maxarg, new_type)
if old_sz == 1:
self.replaces[self.rng[axis]] = new_rng
self.rng.insert(insert_at if insert_at is not None else len(self.rng), new_rng)
del self.rng[axis]
else:
replaced_rng = self.rng[axis].replace(src=(UOp.const(dtypes.int, old_sz),))
self.replaces[self.rng[axis]] = (new_rng * old_sz + replaced_rng) if top else (replaced_rng * amount + new_rng)
self.rng[axis] = replaced_rng
self.rng.insert(insert_at if insert_at is not None else len(self.rng), new_rng)
return new_rng
@property
def axis_types(self) -> list[AxisType]: return [x.arg[-1] for x in self.rng]
@property
def shape_len(self): return len(self.rng)
@property
def full_shape(self) -> tuple[sint, ...]: return tuple([ssimplify(x.src[0]) for x in self.rng])
@property
def output_shape(self) -> tuple[sint, ...]:
if self.ast.src[0].op is not Ops.STORE: return ()
return tuple([ssimplify(x.src[0]) for x in self.ast.src[0].src[2:]])
def get_optimized_ast(self, name_override:str|None=None) -> UOp:
ret = self.substitute()
kernel_name = ret.arg.name if ret.arg is not None and ret.arg.name != "test" else self.name if name_override is None else name_override
rarg = KernelInfo(kernel_name, tuple(self.axis_types), self.dont_use_locals, tuple(self.applied_opts))
return ret.replace(arg=rarg)
# does nothing
@axis_types.setter
def axis_types(self, value): pass
def _apply_tc_opt(self, use_tensor_cores:int, axis:int, tc_select:int, opt_level:int) -> bool:
reduceops = [x for x in self.ast.toposort() if x.op is Ops.REDUCE]
if not len(reduceops): raise KernelOptError("no reduce ops for TensorCore")
reduceop = reduceops[0]
if use_tensor_cores and reduceop is not None and reduceop.arg is Ops.ADD:
mul = reduceop.src[0] if reduceop.src[0].op is not Ops.CAST else reduceop.src[0].src[0]
if mul.op is not Ops.MUL: return False
in0, in1 = mul.src
tensor_cores = self.opts.tensor_cores if tc_select == -1 else [self.opts.tensor_cores[tc_select]]
for tc in tensor_cores:
if tc.dtype_in == in0.dtype.scalar() and tc.dtype_in == in1.dtype.scalar() and tc.dtype_out == reduceop.dtype.scalar():
# early realize for TC
self.substitute()
# tensor cores have three ranges. X, Y, and REDUCE
in0_ranges = sorted([u for u in in0.ranges if u not in in1.ranges], key=lambda x: x.arg[0])
in1_ranges = sorted([u for u in in1.ranges if u not in in0.ranges], key=lambda x: x.arg[0])
red_ranges = sorted(reduceop.src[1:], key=lambda x: x.arg[0])
if DEBUG >= 3:
print(f"TC({axis}): {[(x.arg[0],x.vmax+1) for x in in0_ranges]}",
f"{[(x.arg[0],x.vmax+1) for x in in1_ranges]} {[(x.arg[0],x.vmax+1) for x in red_ranges]}")
if not len(in0_ranges) or not len(in1_ranges) or not len(red_ranges): return None
# pick ranges
# NOTE: why are in1 and in0 switched?
axis_choices = list(itertools.product(in1_ranges, in0_ranges, red_ranges))
if not (axis < len(axis_choices)): return None
axes = axis_choices[axis]
# do optimizations and save the ranges
try:
for i,a in enumerate(axes):
if a.src[0].divides(tc.dims[i]) is None:
self.apply_opt(Opt(OptOps.PADTO, self.rng.index(a), tc.dims[i]), append_opt=False) # PADTO might fail
except KernelOptError: continue
ne: list[UOp] = []
for opt in tc.opts:
ne.append(self.apply_opt(Opt({"u":OptOps.UPCAST, "l":OptOps.LOCAL}[opt[0]], axes[int(opt[1])].arg[0], 2), append_opt=False))
reduce_axis = [self.rng[i] for i in self.axes_of(AxisType.REDUCE)].index(axes[2])
for _, amt in tc.get_reduce_axes():
ne.append(self.apply_opt(Opt(OptOps.UNROLL, reduce_axis, amt), append_opt=False))
if use_tensor_cores != 2:
# fix the srcs
reduceop = [x for x in self.ast.toposort() if x.op is Ops.REDUCE][0]
tne = [x.replace(tag=1) for x in ne]
ret = reduceop.substitute(dict(zip(ne, tne)))
srcs = list((ret.src[0] if ret.src[0].op is not Ops.CAST else ret.src[0].src[0]).src)
srcs = [x.substitute(dict(zip(tne, [ne[i] for i in p]))) for x,p in zip(srcs, tc.permutes_for_shape_str(tc.base_shape_str()))]
# get reduce/upcast axes for the tensor cores
tc_reduce_axes = self.shape_str_to_axis([f"r{i}" for i in range(len(tc.get_reduce_axes()))])
base_upcast_axes = tuple([(s,2) for s in self.shape_str_to_axis(tc.base_upcast_axes())])
tc_upcast_axes = tuple([base_upcast_axes[:int(math.log2(tc.elements_per_thread[i]))] for i in range(3)])
# axes to range number (was done in lowerer)
tc_upcast_axes = tuple([tuple([(self.rng[a].arg[0], sz) for a,sz in v]) for v in tc_upcast_axes])
tc_reduce_axes = tuple([self.rng[a].arg[0] for a in tc_reduce_axes])
# construct the op
# TODO: remove tc_upcast_axes from the arg
wmma_arg = (str(tc), tc.dims, tc.dtype_in, tc.dtype_out, self.opts.device, tc.threads, tc_upcast_axes, tc_reduce_axes)
wmma = UOp(Ops.WMMA, dtype=tc.dtype_out.vec(tc.elements_per_thread[2]), src=(
UOp(Ops.CONTRACT, dtype=srcs[0].dtype.vec(tc.elements_per_thread[0]), src=(srcs[0],), arg=tc_upcast_axes[0], tag=1),
UOp(Ops.CONTRACT, dtype=srcs[1].dtype.vec(tc.elements_per_thread[1]), src=(srcs[1],), arg=tc_upcast_axes[1], tag=1),
UOp.const(tc.dtype_out.vec(tc.elements_per_thread[2]), 0.0)), arg=wmma_arg, tag=1)
tc_uop = UOp(Ops.UNROLL, tc.dtype_out, (wmma,), arg=tc_upcast_axes[2], tag=1)
# preserve extra reduces
reduce_ranges = [x for x in UOp.sink(*reduceop.src[1:]).toposort() if x.op is Ops.RANGE and x.arg[0] not in tc_reduce_axes]
if len(reduce_ranges): tc_uop = UOp(Ops.REDUCE, tc_uop.dtype, (tc_uop,)+tuple(reduce_ranges), Ops.ADD)
self.ast = self.ast.substitute({reduceop: tc_uop})
return True
return False
def bufs_from_ast(ast:UOp, dname:str) -> list[Buffer]:
glbls = sorted([x for x in ast.parents if x.op is Ops.DEFINE_GLOBAL], key=lambda x: x.arg)
return [Buffer(dname, x.dtype.size, x.dtype.base) for x in glbls]
def apply_ropt(ast:UOp, renderer:Renderer):
k = RKernel(ast, opts=renderer)
if BEAM >= 1:
from tinygrad.codegen.opt.search import beam_search
kb = RKernel(ast, opts=renderer)
rawbufs = bufs_from_ast(ast, renderer.device)
k = beam_search(kb, rawbufs, BEAM.value, bool(getenv("BEAM_ESTIMATE", 1)))
elif ast.arg is not None: k.apply_opts(ast.arg.opts_to_apply)
return k.get_optimized_ast()
pm_postrange_opt = pm_flatten_range+PatternMatcher([
(UPat(Ops.SINK, name="ast"), lambda ctx,ast: apply_ropt(ast, ctx) if ast.arg is None or \
(ast.arg is not None and ast.arg.opts_to_apply is not None) else None),
]) ])
+1 -1
View File
@@ -140,7 +140,7 @@ DONT_REALIZE_EXPAND, DONT_GROUP_REDUCES = ContextVar("DONT_REALIZE_EXPAND", 0),
QUANTIZE, VALIDATE_WITH_CPU, DISABLE_FAST_IDIV = ContextVar("QUANTIZE", 0), ContextVar("VALIDATE_WITH_CPU", 0), ContextVar("DISABLE_FAST_IDIV", 0) QUANTIZE, VALIDATE_WITH_CPU, DISABLE_FAST_IDIV = ContextVar("QUANTIZE", 0), ContextVar("VALIDATE_WITH_CPU", 0), ContextVar("DISABLE_FAST_IDIV", 0)
CORRECT_DIVMOD_FOLDING, FUSE_OPTIM = ContextVar("CORRECT_DIVMOD_FOLDING", 0), ContextVar("FUSE_OPTIM", 0) CORRECT_DIVMOD_FOLDING, FUSE_OPTIM = ContextVar("CORRECT_DIVMOD_FOLDING", 0), ContextVar("FUSE_OPTIM", 0)
ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, AMD_LLVM = ContextVar("ALLOW_DEVICE_USAGE", 1), ContextVar("MAX_BUFFER_SIZE", 0), ContextVar("AMD_LLVM", 1) ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, AMD_LLVM = ContextVar("ALLOW_DEVICE_USAGE", 1), ContextVar("MAX_BUFFER_SIZE", 0), ContextVar("AMD_LLVM", 1)
RANGEIFY, POSTOPT, FUSE_ATTENTION = ContextVar("RANGEIFY", 0), ContextVar("POSTOPT", 0), ContextVar("FUSE_ATTENTION", 0) RANGEIFY, POSTOPT, FUSE_ATTENTION = ContextVar("RANGEIFY", 0), ContextVar("POSTOPT", 1), ContextVar("FUSE_ATTENTION", 0)
@dataclass(frozen=True) @dataclass(frozen=True)
class Metadata: class Metadata: