mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-14 23:58:28 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8665d104ae | ||
|
|
75bbc2ef10 | ||
|
|
51aef3e495 | ||
|
|
257f7d6d03 | ||
|
|
5953a33853 | ||
|
|
82504bc5ea | ||
|
|
df660ce904 | ||
|
|
bc326d6fc8 | ||
|
|
018f9a81fa | ||
|
|
8e1ce85283 | ||
|
|
9f18dc700d | ||
|
|
e863b2ea6f |
+1
-2
@@ -134,6 +134,7 @@ backend_test.exclude('test_simple_rnn_*')
|
||||
|
||||
# no control flow
|
||||
# control flow uses AttributeProto.GRAPH
|
||||
backend_test.exclude('test_if_*')
|
||||
backend_test.exclude('test_loop*')
|
||||
backend_test.exclude('test_range_float_type_positive_delta_expanded_cpu') # requires loop
|
||||
backend_test.exclude('test_affine_grid_2d_align_corners_expanded_cpu')
|
||||
@@ -182,8 +183,6 @@ backend_test.exclude('test_resize_downsample_scales_cubic_antialias_cpu') # anti
|
||||
backend_test.exclude('test_resize_downsample_sizes_cubic_antialias_cpu') # antialias not implemented
|
||||
backend_test.exclude('test_ai_onnx_ml_label_encoder_tensor_value_only_mapping_cpu') # bad data type string
|
||||
backend_test.exclude('test_ai_onnx_ml_label_encoder_tensor_mapping_cpu') # bad data type string
|
||||
backend_test.exclude('test_if_opt_cpu') # ValueError: 13 is not a valid AttributeType
|
||||
backend_test.exclude('test_if_seq_cpu') # NotImplementedError: op='SequenceConstruct' is not supported
|
||||
|
||||
backend_test.exclude('test_scatternd_min_cpu') # min not yet supported
|
||||
backend_test.exclude('test_scatternd_max_cpu') # max not yet supported
|
||||
|
||||
-19
@@ -100,25 +100,6 @@ class TestMainOnnxOps(TestOnnxOps):
|
||||
self._test_resize_scales([0.01, 0.25, 0.5, 0.51, 0.6, 1.0, 1.5, 2.0, 3.5, 20.0], mode="cubic", exclude_outside=1)
|
||||
self._test_resize_scales([0.01, 0.25, 0.5, 0.51, 0.6, 1.0, 1.5, 2.0, 3.5, 20.0], mode="cubic", exclude_outside=0)
|
||||
|
||||
def _test_if(self, then_value, else_value):
|
||||
then_out = onnx.helper.make_tensor_value_info("res", onnx.TensorProto.FLOAT, then_value.shape)
|
||||
else_out = onnx.helper.make_tensor_value_info("res", onnx.TensorProto.FLOAT, else_value.shape)
|
||||
|
||||
then_const_node = onnx.helper.make_node("Constant", inputs=[], outputs=["res"], value=onnx.numpy_helper.from_array(then_value))
|
||||
else_const_node = onnx.helper.make_node("Constant", inputs=[], outputs=["res"], value=onnx.numpy_helper.from_array(else_value))
|
||||
|
||||
then_body = onnx.helper.make_graph([then_const_node], "then_body", [], [then_out])
|
||||
else_body = onnx.helper.make_graph([else_const_node], "else_body", [], [else_out])
|
||||
|
||||
self.helper_test_single_op("If", {"cond": np.array(False).astype(bool)}, {"then_branch": then_body, "else_branch": else_body}, ["res"])
|
||||
self.helper_test_single_op("If", {"cond": np.array(True).astype(bool)}, {"then_branch": then_body, "else_branch": else_body}, ["res"])
|
||||
|
||||
def test_if_different_shapes_broadcastable(self):
|
||||
self._test_if(np.array([[1], [2]]).astype(np.float32), np.array([[6, 5, 4, 3, 2, 1]]).astype(np.float32))
|
||||
|
||||
def test_if_different_shapes_not_broadcastable(self):
|
||||
self._test_if(np.array([[1, 2, 3], [4, 5, 6]]).astype(np.float32), np.array([[6, 5, 4, 3, 2, 1]]).astype(np.float32))
|
||||
|
||||
def test_resize_downsample_scales_linear_align_corners(self):
|
||||
# https://github.com/onnx/onnx/blob/main/docs/Operators.md#examples-131
|
||||
X = np.array([[[[1, 2, 3, 4], [5, 6, 7, 8]]]], dtype=np.float32)
|
||||
|
||||
+2
-22
@@ -11,26 +11,6 @@ class TestRangeify(unittest.TestCase):
|
||||
ba = A.expand(N, N)
|
||||
((ba+1).sum(axis=1) + (ba+2).sum(axis=0)).realize()
|
||||
|
||||
def test_partial_contig(self):
|
||||
A = Tensor.empty(64, 64, 64)
|
||||
ret = A.sum(axis=2).contiguous(arg=(1,)).sum(axis=1)
|
||||
ret.realize()
|
||||
|
||||
def test_double_gemm_real(self):
|
||||
def go():
|
||||
with Context(DEBUG=0):
|
||||
Tensor.manual_seed(1337)
|
||||
A,B,C = [Tensor.randn(N, N) for _ in range(3)]
|
||||
Tensor.realize(A, B, C)
|
||||
GlobalCounters.reset()
|
||||
return (A@B@C).realize()
|
||||
rng = go()
|
||||
with Context(RANGEIFY=0, DEBUG=2):
|
||||
ref = go()
|
||||
mse = ((rng-ref)**2).sum().item()
|
||||
print(f"mse: {mse}")
|
||||
self.assertLessEqual(mse, 1e-2)
|
||||
|
||||
def test_double_gemm(self):
|
||||
A = Tensor.empty(N, N)
|
||||
B = Tensor.empty(N, N)
|
||||
@@ -116,10 +96,10 @@ class TestRangeify(unittest.TestCase):
|
||||
out.realize()
|
||||
|
||||
def test_flash_attention(self):
|
||||
#BS, HEADS, SEQLEN, EMB = 4, 2, 16, 8
|
||||
BS, HEADS, SEQLEN, EMB = 4, 2, 16, 8
|
||||
|
||||
# bigger
|
||||
BS, HEADS, SEQLEN, EMB = 4, 32, 1024, 64
|
||||
#BS, HEADS, SEQLEN, EMB = 4, 16, 128, 64
|
||||
|
||||
# llama 8B
|
||||
#BS, HEADS, SEQLEN, EMB = 4, 32, 2048, 128
|
||||
|
||||
@@ -9,18 +9,17 @@ from tinygrad.renderer import Renderer
|
||||
# import all pattern matchers here
|
||||
from tinygrad.codegen.lowerer import pm_lowerer, get_index
|
||||
from tinygrad.codegen.quantize import pm_quant
|
||||
from tinygrad.codegen.gpudims import pm_add_gpudims, pm_tensor_cores, pm_group_for_reduce, pm_fix_locals, pm_bufferize_loop
|
||||
from tinygrad.codegen.gpudims import pm_add_gpudims
|
||||
from tinygrad.uop.symbolic import sym, symbolic_simple, gep_pushing
|
||||
from tinygrad.uop.decompositions import get_late_rewrite_patterns
|
||||
from tinygrad.codegen.late.expander import migrate_indexing, expander, pm_pre_expander
|
||||
from tinygrad.codegen.late.devectorizer import load_store_folding, load_store_indexing, devectorize, pm_reduce, \
|
||||
ReduceContext, correct_load_store, pm_render
|
||||
from tinygrad.codegen.late.linearize import block_create, pm_blockend_merge, block_merge, pm_finalize, BlockContext
|
||||
from tinygrad.codegen.opt import pm_get_optimization, pm_do_optimize, pm_postrange_opt
|
||||
from tinygrad.codegen.opt import pm_get_optimization, pm_do_optimize
|
||||
from tinygrad.codegen.opt.swizzler import view_left, view_right, fix_kernel_ops
|
||||
from tinygrad.codegen.opt.postrange import pm_postrange_opt
|
||||
from tinygrad.codegen.opt.postrange import pm_postrange_opt_early, pm_postrange_opt, pm_postrange_opt_merge
|
||||
from tinygrad.schedule.rangeify import pm_add_buffers_local, rangeify_codegen
|
||||
from tinygrad.codegen.opt.postrange import pm_flatten_range
|
||||
|
||||
@dataclass
|
||||
class RewriteStep:
|
||||
@@ -58,33 +57,28 @@ def _get_rewrites_for_renderer(opts:Renderer, linearizer:bool, _QUANTIZE, _DEVEC
|
||||
ret.extend(rewrites_for_views)
|
||||
|
||||
# this is kernel.py
|
||||
if not _RANGEIFY: ret.append(RewriteStep(pm_get_optimization, ctx=lambda _: opts, name="get optimization"))
|
||||
ret.append(RewriteStep(pm_get_optimization, ctx=lambda _: opts, name="get optimization"))
|
||||
|
||||
if not _POSTOPT and not _RANGEIFY: ret.append(RewriteStep(pm_do_optimize, ctx=lambda _: opts, name="optimize ast"))
|
||||
|
||||
if _QUANTIZE and opts.device in {"CPU", "DSP"}: ret.append(RewriteStep(pm_quant, name="quantize"))
|
||||
ret.append(RewriteStep(pm_lowerer, get_index, name="lowerer", bottom_up=True))
|
||||
|
||||
# add tensor cores
|
||||
if _RANGEIFY:
|
||||
ret.append(RewriteStep(pm_bufferize_loop, name="bufferize loop"))
|
||||
ret.append(RewriteStep(pm_tensor_cores, lambda _: ({}, opts), name="tensor cores", bottom_up=True))
|
||||
|
||||
if _POSTOPT or _RANGEIFY: ret.append(RewriteStep(pm_postrange_opt, ctx=lambda _: opts, name="post optimize ast"))
|
||||
|
||||
# ** expander (expand_rewrite) **
|
||||
# symbolic before post opt
|
||||
ret.append(RewriteStep(sym+migrate_indexing, name="initial symbolic"))
|
||||
|
||||
ret.append(RewriteStep(pm_fix_locals, name="fix locals"))
|
||||
if _POSTOPT or _RANGEIFY:
|
||||
ret.append(RewriteStep(pm_postrange_opt_merge, ctx=lambda _: ({}, opts), name="early range merge"))
|
||||
ret.append(RewriteStep(sym, name="mid symbolic"))
|
||||
ret.append(RewriteStep(pm_postrange_opt_early, ctx=lambda _: ({}, opts), name="early post opt ast"))
|
||||
ret.append(RewriteStep(sym, name="mid symbolic"))
|
||||
ret.append(RewriteStep(pm_postrange_opt, ctx=lambda _: opts, name="post optimize ast"))
|
||||
|
||||
# ** expander (expand_rewrite) **
|
||||
ret.append(RewriteStep(sym+pm_pre_expander+expander, name="expander"))
|
||||
|
||||
# add locals
|
||||
ret.append(RewriteStep(pm_flatten_range+pm_add_buffers_local+rangeify_codegen+pm_group_for_reduce, name="add local buffers"))
|
||||
|
||||
# add gpu dims (late). this also handles UNROLL range
|
||||
ret.append(RewriteStep(pm_add_gpudims, lambda _: opts, name="add gpudims"))
|
||||
|
||||
# expand
|
||||
ret.append(RewriteStep(sym+pm_pre_expander+expander, name="expander"))
|
||||
ret.append(RewriteStep(pm_add_buffers_local+rangeify_codegen, name="add local buffers"))
|
||||
|
||||
# ** devectorizer (full_graph_rewrite) **
|
||||
# remove reduce
|
||||
|
||||
+4
-179
@@ -1,10 +1,9 @@
|
||||
import math, functools, operator
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, PatternMatcher, UPat, KernelInfo, ssimplify, AxisType, graph_rewrite
|
||||
from tinygrad.helpers import all_int, partition, flatten, prod, dedup, USE_TC, DEBUG
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
import math
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, PatternMatcher, UPat, KernelInfo, ssimplify, AxisType
|
||||
from tinygrad.helpers import all_int, dedup
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.shape.view import get_contraction
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.codegen.opt.tc import TensorCore
|
||||
|
||||
def _group_dims(dims:tuple[sint, ...], max_sizes:tuple[int, ...]):
|
||||
# TODO: symbolic shape
|
||||
@@ -89,181 +88,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
except ValueError: continue
|
||||
return s.substitute(subs)
|
||||
|
||||
def fix_reduce_unroll(x:UOp):
|
||||
reduce_range, reduce_expand = partition(x.src[1:], lambda y: y.op is Ops.RANGE)
|
||||
if len(reduce_expand) == 0: return None
|
||||
reduce_expand = [x for x in reduce_expand if x.op is not Ops.CONST]
|
||||
assert all(x.op is Ops.UNROLL for x in reduce_expand), f"not all UNROLLS in {reduce_expand}"
|
||||
ret = x.src[0]
|
||||
if len(contract_axis:=flatten(x.arg for x in reduce_expand)):
|
||||
ret = UOp(Ops.CONTRACT, x.dtype.vec(prod(x[1] for x in contract_axis)), (ret,), tuple(contract_axis), tag=1)
|
||||
# REDUCE supports both "horizontal" reduction and range reduction. the horizontal elements are taken in the nearest group
|
||||
return x.replace(src=(ret,)+tuple(reduce_range))
|
||||
|
||||
def fix_store_unroll(x:UOp):
|
||||
store_expand, store_range = partition(x.src[2:], lambda y: y.op is Ops.UNROLL)
|
||||
if len(store_expand) == 0: return None
|
||||
return UOp(Ops.CONTRACT, dtypes.void, (x.replace(src=x.src[:2]+tuple(store_range)),), tuple(flatten(x.arg for x in store_expand)), tag=1)
|
||||
|
||||
def fix_group_for_reduce(x:UOp):
|
||||
reduce_gfr, reduce_r = partition(x.src[1:], lambda u: u.op is Ops.RANGE and u.arg[1] == AxisType.GROUP_REDUCE)
|
||||
if len(reduce_gfr) == 0: return None
|
||||
|
||||
# NOTE: if there's other locals here, we need them in the buffer too
|
||||
upstream_locals = [u for u in x.toposort() if u.op is Ops.RANGE and u.arg[1] == AxisType.LOCAL]
|
||||
|
||||
# do only the non grouped reduces early
|
||||
ret = x.replace(src=(x.src[0],)+tuple(reduce_r))
|
||||
reduce_loop = [x.replace(arg=(x.arg[0]+100, AxisType.REDUCE)) for x in reduce_gfr]
|
||||
buf = ret.bufferize(*upstream_locals, *reduce_gfr, arg=AddrSpace.LOCAL).index(*upstream_locals, *reduce_loop)
|
||||
|
||||
# gate with an if on the store + do the final reduce
|
||||
buf = UOp(Ops.IF, dtype=buf.dtype, src=(functools.reduce(operator.and_, [x.eq(0) for x in reduce_gfr]), buf))
|
||||
return buf.reduce(*reduce_loop, arg=x.arg)
|
||||
|
||||
pm_group_for_reduce = PatternMatcher([
|
||||
# fix group for reduce
|
||||
(UPat(Ops.REDUCE, name="x"), fix_group_for_reduce),
|
||||
])
|
||||
|
||||
pm_add_gpudims = PatternMatcher([
|
||||
# add gpudims must be last
|
||||
(UPat(Ops.SINK, name="s"), add_gpudims),
|
||||
# rewrite UPCAST/UNROLL range to something to be expanded
|
||||
(UPat(Ops.RANGE, name="r"),
|
||||
lambda r: UOp(Ops.UNROLL, dtypes.int, (UOp.const(dtypes.int.vec(s:=r.vmax+1), tuple(range(s))),), ((r.arg[0],s),)) \
|
||||
if r.arg[1] in {AxisType.UNROLL, AxisType.UPCAST} else None),
|
||||
# fix REDUCEs with UNROLLs
|
||||
(UPat(Ops.REDUCE, name="x"), fix_reduce_unroll),
|
||||
(UPat(Ops.STORE, name="x"), fix_store_unroll),
|
||||
])
|
||||
|
||||
def apply_tensor_cores(ctx:tuple[dict, Renderer], in0:UOp, in1:UOp, r_range:UOp, reduceop:UOp):
|
||||
if not USE_TC: return None
|
||||
# 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])
|
||||
#print(len(in0_ranges), len(in1_ranges))
|
||||
if not len(in0_ranges) or not len(in1_ranges): return None
|
||||
in0_range, in1_range = in0_ranges[0], in1_ranges[0]
|
||||
if DEBUG >= 2: print('TC', in0_range.arg, in1_range.arg, r_range.arg)
|
||||
|
||||
# confirm the dtype and size is good
|
||||
tc_opts: list[TensorCore] = []
|
||||
for tc in ctx[1].tensor_cores:
|
||||
if reduceop.dtype == tc.dtype_out and in0.dtype == tc.dtype_in and in1.dtype == tc.dtype_in:
|
||||
if all(i <= j for i,j in zip(tc.dims, [in0_range.vmax+1, in1_range.vmax+1, r_range.vmax+1])):
|
||||
tc_opts.append(tc)
|
||||
if len(tc_opts) == 0: return None
|
||||
tc = tc_opts[0]
|
||||
|
||||
# create the new ranges as speced by the tensor core
|
||||
old_range = [in0_range, in1_range, r_range]
|
||||
new_range = [r.replace(src=(r.src[0]//tc.dims[i],)) for i,r in enumerate(old_range)]
|
||||
new_reduce_range = new_range[2]
|
||||
tc_range = 9050 #+ r_range.arg[0]*100
|
||||
red_ranges = []
|
||||
|
||||
ne: list[UOp] = []
|
||||
for o in tc.opts:
|
||||
lrange = UOp.range(dtypes.int, 2, tc_range, AxisType.UPCAST if o[0] == "u" else AxisType.LOCAL)
|
||||
ne.append(lrange)
|
||||
tc_range += 1
|
||||
new_range[1-int(o[1])] = (2 * new_range[1-int(o[1])]) + lrange
|
||||
for _, amt in tc.get_reduce_axes():
|
||||
lrange = UOp.range(dtypes.int, amt, tc_range, AxisType.UNROLL)
|
||||
ne.append(lrange)
|
||||
red_ranges.append(lrange)
|
||||
tc_range += 1
|
||||
new_range[2] = (amt * new_range[2]) + lrange
|
||||
tne = [x.replace(tag=1) for x in ne]
|
||||
|
||||
# replace ranges in other parts of the graph
|
||||
for x,y in zip(old_range, new_range): ctx[0][x] = y
|
||||
|
||||
# apply the swizzled ranges to the srcs
|
||||
srcs = [s.substitute(dict(zip(old_range, new_range))).substitute(dict(zip(ne, tne))) for s in (in0, in1)]
|
||||
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()))]
|
||||
|
||||
ned = dict(zip(tc.base_shape_str(), ne))
|
||||
tc_reduce_axes = tuple([ned[f"r{i}"].arg[0] for i in range(len(tc.get_reduce_axes()))])
|
||||
base_upcast_axes = tuple([(ned[s].arg[0], 2) for s in tc.base_upcast_axes()])
|
||||
tc_upcast_axes = tuple([base_upcast_axes[:int(math.log2(tc.elements_per_thread[i]))] for i in range(3)])
|
||||
|
||||
# construct the op
|
||||
# TODO: remove tc_upcast_axes from the arg
|
||||
wmma_arg = (str(tc), tc.dims, tc.dtype_in, tc.dtype_out, ctx[1].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]),
|
||||
UOp(Ops.CONTRACT, dtype=srcs[1].dtype.vec(tc.elements_per_thread[1]), src=(srcs[1],), arg=tc_upcast_axes[1]),
|
||||
UOp.const(tc.dtype_out.vec(tc.elements_per_thread[2]), 0.0)), arg=wmma_arg)
|
||||
tc_uop = UOp(Ops.UNROLL, tc.dtype_out, (wmma,), arg=tc_upcast_axes[2])
|
||||
ret = tc_uop.reduce(new_reduce_range, arg=Ops.ADD)
|
||||
# confirm the UNROLLs aren't actually used, these need to be broadcast MUL
|
||||
assert all(u not in red_ranges for u in ret.toposort()), "UNROLLs in TC"
|
||||
return ret
|
||||
|
||||
from tinygrad.codegen.opt.postrange import pm_flatten_range
|
||||
|
||||
pm_tensor_cores = PatternMatcher([
|
||||
((UPat.var("in0")*UPat.var("in1")).reduce(UPat(Ops.RANGE, name="r_range"), name="reduceop", arg=Ops.ADD), apply_tensor_cores),
|
||||
|
||||
# replace range
|
||||
#(UPat(Ops.RANGE, name="r"), lambda ctx,r: ctx[0].get(r, None)),
|
||||
(UPat(Ops.SINK, name="s"), lambda ctx,s: graph_rewrite(s.substitute(ctx[0]), pm_flatten_range, name="flatten")),
|
||||
])
|
||||
|
||||
def fix_bufferize(x:UOp):
|
||||
if x.arg != AddrSpace.LOCAL: return None
|
||||
locals_left = [r for r in x.ranges if r.arg[1] == AxisType.LOCAL]
|
||||
if not len(locals_left): return None
|
||||
acc = x.size
|
||||
st = []
|
||||
for l in locals_left:
|
||||
st.append(l*acc)
|
||||
acc *= l.vmax+1
|
||||
return x.replace(src=(x.src[0],) + tuple(locals_left[::-1]) + x.src[1:]).index(sum(st))
|
||||
|
||||
|
||||
pm_double_index = PatternMatcher([
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, src=(UPat.var("b"), UPat.var("x"))), UPat.var("y"))), lambda b,x,y: b.index(x+y)),
|
||||
])
|
||||
|
||||
pm_fix_locals = pm_double_index+PatternMatcher([
|
||||
(UPat(Ops.BUFFERIZE, name="x"), fix_bufferize),
|
||||
])
|
||||
|
||||
B = 8
|
||||
|
||||
def loop_store(x:UOp):
|
||||
r_maybe = [r for r in x.src[0].ranges if r.arg[0] == 2 and r.tag is None]
|
||||
ur_maybe = [r for r in x.ranges if r.arg[0] < 0]
|
||||
#print("store", len(r_maybe), len(ur_maybe))
|
||||
if not len(r_maybe) or not len(ur_maybe): return None
|
||||
|
||||
r = r_maybe[0]
|
||||
ur = ur_maybe[0]
|
||||
rr = r.replace(src=(r.src[0]//B,), tag=1)
|
||||
return x.substitute({r:rr*B+ur})
|
||||
|
||||
def loop_bufferize(x:UOp):
|
||||
if x.arg != AddrSpace.LOCAL: return None
|
||||
r_maybe = [r for r in x.ranges if r.arg[0] == 2 and r.tag is None]
|
||||
ur_maybe = [r for r in x.ranges if r.arg[0] < 0]
|
||||
|
||||
if len(ur_maybe):
|
||||
ur = ur_maybe[0]
|
||||
else:
|
||||
if len(r_maybe) == 0: return None
|
||||
r = r_maybe[0]
|
||||
ur = UOp.range(dtypes.int, B, -2010)
|
||||
rr = r.replace(src=(r.src[0]//B,), tag=1)
|
||||
x = x.substitute({r:rr*B + ur})
|
||||
|
||||
ur1 = UOp.range(dtypes.int, B, ur.arg[0]+1)
|
||||
return x.replace(src=(x.src[0],ur)+x.src[1:]).index(x.size*ur1)
|
||||
|
||||
pm_bufferize_loop = pm_double_index+PatternMatcher([
|
||||
(UPat(Ops.BUFFERIZE, name="x"), loop_bufferize),
|
||||
(UPat(Ops.STORE, name="x"), loop_store),
|
||||
])
|
||||
@@ -134,15 +134,15 @@ def fix_store_unroll(x:UOp):
|
||||
return UOp(Ops.CONTRACT, dtypes.void, (x.replace(src=x.src[:2]+tuple(store_range)),), tuple(flatten(x.arg for x in store_expand)), tag=1)
|
||||
|
||||
def fix_group_for_reduce(x:UOp):
|
||||
reduce_gfr, reduce_r = partition(x.src[1:], lambda u: u.op is Ops.RANGE and u.arg[1] == AxisType.GROUP_REDUCE)
|
||||
reduce_gfr, reduce_r = partition(x.src[1:], lambda u: u.op is Ops.RANGE and u.arg[-1] == AxisType.GROUP_REDUCE)
|
||||
if len(reduce_gfr) == 0: return None
|
||||
|
||||
# NOTE: if there's other locals here, we need them in the buffer too
|
||||
upstream_locals = [u for u in x.toposort() if u.op is Ops.RANGE and u.arg[1] == AxisType.LOCAL]
|
||||
upstream_locals = [u for u in x.toposort() if u.op is Ops.RANGE and u.arg[-1] == AxisType.LOCAL]
|
||||
|
||||
# do only the non grouped reduces early
|
||||
ret = x.replace(src=(x.src[0],)+tuple(reduce_r))
|
||||
reduce_loop = [x.replace(arg=(x.arg[0]+100, AxisType.REDUCE)) for x in reduce_gfr]
|
||||
reduce_loop = [x.replace(arg=(*x.arg[0:-1], 0, AxisType.REDUCE)) for x in reduce_gfr]
|
||||
buf = ret.bufferize(*upstream_locals, *reduce_gfr, arg=(AddrSpace.LOCAL, reduce_gfr[0].arg[0])).index(*upstream_locals, *reduce_loop)
|
||||
|
||||
# gate with an if on the store + do the final reduce
|
||||
@@ -152,8 +152,8 @@ def fix_group_for_reduce(x:UOp):
|
||||
pm_pre_expander = PatternMatcher([
|
||||
# rewrite UPCAST/UNROLL range to something to be expanded
|
||||
(UPat(Ops.RANGE, name="r"),
|
||||
lambda r: UOp(Ops.UNROLL, dtypes.int, (UOp.const(dtypes.int.vec(s:=r.vmax+1), tuple(range(s))),), ((r.arg[0],s),)) \
|
||||
if r.arg[1] in {AxisType.UNROLL, AxisType.UPCAST} else None),
|
||||
lambda r: UOp(Ops.UNROLL, dtypes.int, (UOp.const(dtypes.int.vec(s:=r.vmax+1), tuple(range(s))),), ((r.arg[0:-1],s),)) \
|
||||
if r.arg[-1] in {AxisType.UNROLL, AxisType.UPCAST} else None),
|
||||
# fix REDUCEs with UNROLLs
|
||||
(UPat(Ops.REDUCE, name="x"), fix_reduce_unroll),
|
||||
(UPat(Ops.STORE, name="x"), fix_store_unroll),
|
||||
|
||||
@@ -83,5 +83,5 @@ pm_lowerer = PatternMatcher([
|
||||
|
||||
# axis fixups for WMMA
|
||||
(UPat((Ops.CONTRACT, Ops.UNROLL), name="x"),
|
||||
lambda ctx,x: x.replace(tag=1, arg=tuple([(ctx.idxs[a].arg[0], sz) for a,sz in x.arg])) if x.tag is None else None),
|
||||
lambda ctx,x: x.replace(tag=1, arg=tuple([(ctx.idxs[a].arg[0:-1], sz) for a,sz in x.arg])) if x.tag is None else None),
|
||||
])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# opt opinionatedly transforms an ast into an optimized ast using either heuristics or beam search
|
||||
|
||||
from tinygrad.codegen.opt.kernel import Kernel
|
||||
from tinygrad.codegen.opt.postrange import RKernel, pm_flatten_range
|
||||
from tinygrad.codegen.opt.heuristic import hand_coded_optimizations
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, KernelInfo
|
||||
from tinygrad.helpers import NOOPT, BEAM, USE_TC, getenv
|
||||
@@ -29,7 +28,8 @@ def get_optimized_ast(ast:UOp, renderer:Renderer) -> UOp:
|
||||
kb = Kernel(ast, opts=renderer)
|
||||
rawbufs = bufs_from_lin(kb, allocate=False)
|
||||
k = beam_search(kb, rawbufs, BEAM.value, bool(getenv("BEAM_ESTIMATE", 1)))
|
||||
return ast.replace(arg=KernelInfo(opts_to_apply=tuple(k.applied_opts)))
|
||||
# NOTE: this does simplify_ones/simplify_merge_adjacent for you
|
||||
return Kernel(ast, opts=renderer).get_optimized_ast().replace(arg=KernelInfo(opts_to_apply=tuple(k.applied_opts)))
|
||||
|
||||
pm_get_optimization = PatternMatcher([
|
||||
(UPat(Ops.SINK, name="ast"), lambda ctx,ast: get_optimized_ast(ast, ctx) if ast.arg is None and ast.src[0].st is not None else None),
|
||||
@@ -45,15 +45,3 @@ def apply_opt(ast:UOp, renderer:Renderer):
|
||||
pm_do_optimize = PatternMatcher([
|
||||
(UPat(Ops.SINK, name="ast"), lambda ctx,ast: apply_opt(ast, ctx) if ast.arg is not None and ast.arg.opts_to_apply is not None else None),
|
||||
])
|
||||
|
||||
# ** postrange **
|
||||
|
||||
def apply_ropt(ast:UOp, renderer:Renderer):
|
||||
k = RKernel(ast, opts=renderer)
|
||||
if 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),
|
||||
])
|
||||
|
||||
@@ -92,6 +92,10 @@ class Kernel:
|
||||
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)]
|
||||
|
||||
# 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):
|
||||
ret = type(self).__new__(type(self))
|
||||
|
||||
|
||||
+142
-141
@@ -1,12 +1,16 @@
|
||||
import math
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, ssimplify, AxisType, KernelInfo, PatternMatcher, UPat, graph_rewrite, _substitute
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.codegen.opt.kernel import Kernel, Opt, OptOps
|
||||
from tinygrad.renderer import Renderer
|
||||
from dataclasses import replace
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, KernelInfo, graph_rewrite, _substitute
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.helpers import colored, USE_TC, DEBUG
|
||||
from tinygrad.codegen.opt.kernel import axis_colors, AxisType
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.codegen.opt.tc import TensorCore
|
||||
|
||||
def flatten_range(r:UOp):
|
||||
def count_divmod(x:UOp): return len([u for u in x.toposort() if u.op in {Ops.IDIV, Ops.MOD}])
|
||||
|
||||
def flatten_range_in_terminators(r:UOp):
|
||||
off = 2 if r.op is Ops.STORE else 1
|
||||
rngs = r.src[off:]
|
||||
if not len(rngs): return None
|
||||
@@ -14,159 +18,151 @@ def flatten_range(r:UOp):
|
||||
return r.replace(src=r.src[:off]+tuple(new_rngs))
|
||||
|
||||
pm_flatten_range = PatternMatcher([
|
||||
# real ranges only
|
||||
(UPat((Ops.REDUCE, Ops.STORE), name="r"), flatten_range),
|
||||
# flatten ranges
|
||||
(UPat((Ops.REDUCE, Ops.STORE), name="r"), flatten_range_in_terminators),
|
||||
])
|
||||
|
||||
def count_divmod(x:UOp): return len([u for u in x.toposort() if u.op in {Ops.IDIV, Ops.MOD}])
|
||||
# NOTE: this one is better than the one in kernel.py
|
||||
def simplify_merge_adjacent(ast:UOp):
|
||||
# get all ranges (sorted)
|
||||
rng = sorted([u for u in ast.parents if u.op is Ops.RANGE], key=lambda x: x.arg[0:-1])
|
||||
terminators = [u for u in ast.parents if u.op in {Ops.REDUCE, Ops.STORE}]
|
||||
termination = {}
|
||||
for t in terminators:
|
||||
for u in t.src[1 if t.op is Ops.REDUCE else 2:]: termination[u] = t
|
||||
|
||||
class RKernel(Kernel):
|
||||
def __init__(self, ast:UOp, opts:Renderer|None=None):
|
||||
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()
|
||||
replaces = {}
|
||||
i = 0
|
||||
while i < len(rng)-1:
|
||||
r0, r1 = rng[i], rng[i+1]
|
||||
# same axistype and same termination
|
||||
if r0.arg[1] == r1.arg[1] and termination[r0] == termination[r1]:
|
||||
s0, s1 = r0.src[0], r1.src[0]
|
||||
new_range = r0.replace(src=(s0*s1,)).simplify()
|
||||
# this checks the legality of a merge
|
||||
oidx = ast.simplify()
|
||||
nidx = graph_rewrite(oidx, _substitute+symbolic+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1}, name=f"check_merge_{i}_{i+1}")
|
||||
# it simplifies
|
||||
if count_divmod(nidx) <= count_divmod(oidx):
|
||||
# it is correct
|
||||
midx = graph_rewrite(nidx, _substitute+symbolic+pm_flatten_range, ctx={new_range:r0*s1+r1}, name=f"correct_merge_{i}_{i+1}")
|
||||
if oidx is midx:
|
||||
termination[new_range] = termination[r0]
|
||||
replaces[r0] = new_range//s1
|
||||
replaces[r1] = new_range%s1
|
||||
rng[i] = new_range
|
||||
del rng[i+1]
|
||||
continue
|
||||
i += 1
|
||||
return ast.substitute(replaces, name="simplify_merge_adjacent")
|
||||
|
||||
# convert LOOP to GLOBAL
|
||||
self.replaces = {}
|
||||
if self.opts.has_local:
|
||||
store_rngs = self.ast.src[0].src[2:]
|
||||
pm_postrange_opt_merge = pm_flatten_range+PatternMatcher([
|
||||
(UPat(Ops.SINK, name="ast"), simplify_merge_adjacent),
|
||||
])
|
||||
|
||||
# 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 = [x for x in store_rngs if x in ls]
|
||||
def apply_tensor_cores(ctx:tuple[dict, Renderer], in0:UOp, in1:UOp, r_range:UOp, reduceop:UOp):
|
||||
if not USE_TC: return None
|
||||
# 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])
|
||||
if not len(in0_ranges) or not len(in1_ranges): return None
|
||||
in0_range, in1_range = in0_ranges[0], in1_ranges[0]
|
||||
if DEBUG >= 2: print('TC', in0_range.arg, in1_range.arg, r_range.arg)
|
||||
|
||||
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
|
||||
# confirm the dtype and size is good
|
||||
tc_opts: list[TensorCore] = []
|
||||
for tc in ctx[1].tensor_cores:
|
||||
if reduceop.dtype == tc.dtype_out and in0.dtype == tc.dtype_in and in1.dtype == tc.dtype_in:
|
||||
if all(i <= j for i,j in zip(tc.dims, [in0_range.vmax+1, in1_range.vmax+1, r_range.vmax+1])):
|
||||
tc_opts.append(tc)
|
||||
if len(tc_opts) == 0: return None
|
||||
tc = tc_opts[0]
|
||||
|
||||
def simplify_merge_adjacent(self):
|
||||
return
|
||||
# NOTE: this one is better than the one in kernel.py, which is kind of a problem
|
||||
terminators = [u for u in self.ast.toposort() if u.op in {Ops.REDUCE, Ops.STORE}]
|
||||
termination = {}
|
||||
for t in terminators:
|
||||
for u in t.src[1 if t.op is Ops.REDUCE else 2:]: termination[u] = t
|
||||
# create the new ranges as speced by the tensor core
|
||||
old_range = [in0_range, in1_range, r_range]
|
||||
new_range = [r.replace(src=(r.src[0]//tc.dims[i],), arg=r.arg[0:-1]+(0, r.arg[-1])) for i,r in enumerate(old_range)]
|
||||
new_range_args = [list(x.arg[0:-1]) for x in new_range]
|
||||
new_reduce_range = new_range[2]
|
||||
red_ranges = []
|
||||
|
||||
replaces = {}
|
||||
i = 0
|
||||
while i < len(self.rng)-1:
|
||||
r0, r1 = self.rng[i], self.rng[i+1]
|
||||
# same axistype and same termination
|
||||
if r0.arg[1] == r1.arg[1] and termination[r0] == termination[r1]:
|
||||
s0, s1 = r0.src[0], r1.src[0]
|
||||
new_range = r0.replace(src=(s0*s1,)).simplify()
|
||||
# this checks the legality of a merge
|
||||
oidx = self.ast.simplify()
|
||||
nidx = graph_rewrite(oidx, _substitute+symbolic+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1}, name=f"check_merge_{i}_{i+1}")
|
||||
# it simplifies
|
||||
if count_divmod(nidx) <= count_divmod(oidx):
|
||||
# it is correct
|
||||
midx = graph_rewrite(nidx, _substitute+symbolic+pm_flatten_range, ctx={new_range:r0*s1+r1}, name=f"correct_merge_{i}_{i+1}")
|
||||
if oidx is midx:
|
||||
termination[new_range] = termination[r0]
|
||||
replaces[r0] = new_range//s1
|
||||
replaces[r1] = new_range%s1
|
||||
self.rng[i] = new_range
|
||||
del self.rng[i+1]
|
||||
continue
|
||||
i += 1
|
||||
self.ast = self.ast.substitute(replaces, name="simplify_merge_adjacent")
|
||||
# place the warp at -99
|
||||
warp_range = -99
|
||||
|
||||
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]}"
|
||||
|
||||
maxarg = max([x.arg[0] for x in self.rng])
|
||||
new_rng = UOp.range(dtypes.int, amount, maxarg+1, 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]
|
||||
ne: list[UOp] = []
|
||||
for o in tc.opts:
|
||||
axis = 1-int(o[1])
|
||||
if o[0] == "u":
|
||||
new_range_args[axis][-1] += 1
|
||||
lrange = UOp.range(dtypes.int, 2, *new_range_args[axis], AxisType.UPCAST)
|
||||
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
|
||||
lrange = UOp.range(dtypes.int, 2, warp_range, AxisType.LOCAL)
|
||||
warp_range += 1
|
||||
ne.append(lrange)
|
||||
new_range[axis] = (2 * new_range[axis]) + lrange
|
||||
for _, amt in tc.get_reduce_axes():
|
||||
new_range_args[2][-1] += 1
|
||||
lrange = UOp.range(dtypes.int, amt, *new_range_args[2], AxisType.UNROLL)
|
||||
ne.append(lrange)
|
||||
red_ranges.append(lrange)
|
||||
new_range[2] = (amt * new_range[2]) + lrange
|
||||
tne = [x.replace(tag=1) for x in ne]
|
||||
|
||||
@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)
|
||||
# replace ranges in other parts of the graph
|
||||
for x,y in zip(old_range, new_range): ctx[0][x] = y
|
||||
|
||||
@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, ...]: return tuple([ssimplify(x.src[0]) for x in self.ast.src[0].src[2:]])
|
||||
# apply the swizzled ranges to the srcs
|
||||
srcs = [s.substitute(dict(zip(old_range, new_range))).substitute(dict(zip(ne, tne))) for s in (in0, in1)]
|
||||
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()))]
|
||||
|
||||
def get_optimized_ast(self, name_override:str|None=None) -> UOp:
|
||||
ret = self.ast
|
||||
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.substitute(self.replaces).replace(arg=rarg)
|
||||
ned = dict(zip(tc.base_shape_str(), ne))
|
||||
tc_reduce_axes = tuple([ned[f"r{i}"].arg[0:-1] for i in range(len(tc.get_reduce_axes()))])
|
||||
base_upcast_axes = tuple([(ned[s].arg[0:-1], 2) for s in tc.base_upcast_axes()])
|
||||
tc_upcast_axes = tuple([base_upcast_axes[:int(math.log2(tc.elements_per_thread[i]))] for i in range(3)])
|
||||
|
||||
# does nothing
|
||||
@axis_types.setter
|
||||
def axis_types(self, value): pass
|
||||
# construct the op
|
||||
# TODO: remove tc_upcast_axes from the arg
|
||||
wmma_arg = (str(tc), tc.dims, tc.dtype_in, tc.dtype_out, ctx[1].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]),
|
||||
UOp(Ops.CONTRACT, dtype=srcs[1].dtype.vec(tc.elements_per_thread[1]), src=(srcs[1],), arg=tc_upcast_axes[1]),
|
||||
UOp.const(tc.dtype_out.vec(tc.elements_per_thread[2]), 0.0)), arg=wmma_arg)
|
||||
tc_uop = UOp(Ops.UNROLL, tc.dtype_out, (wmma,), arg=tc_upcast_axes[2])
|
||||
ret = tc_uop.reduce(new_reduce_range, arg=Ops.ADD)
|
||||
# confirm the UNROLLs aren't actually used, these need to be broadcast MUL
|
||||
assert all(u not in red_ranges for u in ret.toposort()), "UNROLLs in TC"
|
||||
return ret
|
||||
|
||||
def _apply_tc_opt(self, use_tensor_cores:int, axis:int, tc_select:int, opt_level:int) -> bool:
|
||||
reduceop = [x for x in self.ast.toposort() if x.op is Ops.REDUCE][0]
|
||||
if use_tensor_cores and reduceop is not None and reduceop.arg is Ops.ADD:
|
||||
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 == dtypes.float and tc.dtype_out == dtypes.float:
|
||||
axes = [1,0]
|
||||
def early_sink(ctx:tuple[dict, Renderer], s:UOp):
|
||||
s = s.substitute(ctx[0])
|
||||
# global_stores_are_global
|
||||
if ctx[1].has_local:
|
||||
rngs = UOp.sink(*s.src[0].src[2:]).parents
|
||||
s = s.substitute({u:u.replace(arg=u.arg[0:-1]+(AxisType.GLOBAL,)) for u in rngs if u.op is Ops.RANGE and u.arg[-1] is AxisType.LOOP})
|
||||
return s
|
||||
|
||||
# do optimizations and save the ranges
|
||||
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])], 2), append_opt=False))
|
||||
for _, amt in tc.get_reduce_axes():
|
||||
ne.append(self.apply_opt(Opt(OptOps.UNROLL, 0, amt), append_opt=False)) # TODO: this should be the reduce, not 0
|
||||
pm_postrange_opt_early = PatternMatcher([
|
||||
# TODO: this is optional (and can have internal options) and we need a way to express that
|
||||
((UPat.var("in0")*UPat.var("in1")).reduce(UPat(Ops.RANGE, name="r_range"), name="reduceop", arg=Ops.ADD), apply_tensor_cores),
|
||||
(UPat(Ops.SINK, name="s"), early_sink),
|
||||
])
|
||||
|
||||
# early realize for TC
|
||||
self.ast = self.ast.substitute(self.replaces)
|
||||
self.replaces = {}
|
||||
# *** late (BEAM goes here) ***
|
||||
|
||||
# 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()))]
|
||||
axis_typemap = { # (is_reduce, is_local)
|
||||
(False,False): AxisType.UPCAST, (False,True): AxisType.LOCAL,
|
||||
(True, False): AxisType.UNROLL, (True, True): AxisType.GROUP_REDUCE}
|
||||
|
||||
# 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]),
|
||||
UOp(Ops.CONTRACT, dtype=srcs[1].dtype.vec(tc.elements_per_thread[1]), src=(srcs[1],), arg=tc_upcast_axes[1]),
|
||||
UOp.const(tc.dtype_out.vec(tc.elements_per_thread[2]), 0.0)), arg=wmma_arg)
|
||||
tc_uop = UOp(Ops.UNROLL, tc.dtype_out, (wmma,), arg=tc_upcast_axes[2])
|
||||
|
||||
# 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
|
||||
|
||||
from dataclasses import replace
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, KernelInfo
|
||||
from tinygrad.helpers import colored
|
||||
from tinygrad.codegen.opt.kernel import axis_colors
|
||||
def split_range(r:UOp):
|
||||
if r.arg[-1] not in {AxisType.LOOP, AxisType.GLOBAL, AxisType.REDUCE}: return None
|
||||
if r.tag is not None: return None
|
||||
# any divisor is an option
|
||||
is_local = False if r.arg[-1] is AxisType.REDUCE else False
|
||||
N = 4
|
||||
rd = r.src[0].divides(N)
|
||||
if rd is None: return None
|
||||
sr = r.replace(src=(rd,), arg=r.arg[0:-1]+(0, r.arg[-1]), tag=1)
|
||||
er = UOp(Ops.RANGE, dtypes.int, src=(UOp.const(dtypes.int, N),), arg=r.arg[0:-1]+(1, axis_typemap[(r.arg[-1] is AxisType.REDUCE, is_local)]))
|
||||
return sr*N+er
|
||||
|
||||
def rename_sink(s:UOp):
|
||||
if s.arg is not None and s.arg.name != "test": return None
|
||||
@@ -178,6 +174,11 @@ def rename_sink(s:UOp):
|
||||
name = "k" + colored('_', 'BLACK').join(['']+[colored(x.src[0].render(), axis_colors[x.arg[-1]]) for x in rngs])
|
||||
return s.replace(arg=KernelInfo(name=name) if s.arg is None else replace(s.arg, name=name))
|
||||
|
||||
pm_postrange_opt = PatternMatcher([
|
||||
pm_postrange_opt = pm_flatten_range+PatternMatcher([
|
||||
# TODO: this is optional (and can have internal options) and we need a way to express that
|
||||
(UPat(Ops.RANGE, name="r"), split_range),
|
||||
# remove axes with 1
|
||||
(UPat(Ops.RANGE, name="r"), lambda r: r.const_like(0) if r.vmax == 0 else None),
|
||||
# run this last
|
||||
(UPat(Ops.SINK, name="s"), rename_sink),
|
||||
])
|
||||
|
||||
@@ -21,9 +21,9 @@ class AttributeType(enum.IntEnum):
|
||||
ONNX attribute type identifiers.
|
||||
Reference: https://github.com/onnx/onnx/blob/rel-1.18.0/onnx/onnx.proto3#L128-L145
|
||||
"""
|
||||
FLOAT = 1; INT = 2; STRING = 3; TENSOR = 4; GRAPH = 5; FLOATS = 6; INTS = 7; STRINGS = 8 # noqa: E702
|
||||
FLOAT = 1; INT = 2; STRING = 3; TENSOR = 4; FLOATS = 6; INTS = 7; STRINGS = 8 # noqa: E702
|
||||
|
||||
def to_field_name(self) -> str: return {1: "f", 2: "i", 3: "s", 4: "t", 5: "g", 6: "floats", 7: "ints", 8: "strings"}[self.value]
|
||||
def to_field_name(self) -> str: return {1: "f", 2: "i", 3: "s", 4: "t", 6: "floats", 7: "ints", 8: "strings"}[self.value]
|
||||
|
||||
class OnnxDataType(enum.IntEnum):
|
||||
"""
|
||||
@@ -266,7 +266,6 @@ class OnnxPBParser:
|
||||
case 3: obj["i"] = self.reader.read_int64()
|
||||
case 4: obj["s"] = self.reader.read_bytes().data().tobytes().decode("utf8")
|
||||
case 5: obj["t"] = self._parse_TensorProto()['parsed_tensor']
|
||||
case 6: obj["g"] = OnnxRunner._from_subgraph(self._parse_GraphProto())
|
||||
case 7: obj["floats"].append(self.reader.read_float())
|
||||
case 8: obj["ints"].append(self.reader.read_int64())
|
||||
case 9: obj["strings"].append(self.reader.read_bytes().data().tobytes().decode("utf8"))
|
||||
@@ -402,11 +401,8 @@ class OnnxRunner:
|
||||
"""
|
||||
def __init__(self, model_path: Tensor | str | pathlib.Path):
|
||||
model = OnnxPBParser(model_path, load_external_data=True).parse()
|
||||
self._init_from_graph(model["graph"])
|
||||
|
||||
def _init_from_graph(self, graph: dict, is_subgraph: bool = False):
|
||||
graph = model["graph"]
|
||||
self.is_training = any(n['parsed_node'].opset_id.domain in {Domain.AI_ONNX_TRAINING, Domain.AI_ONNX_PREVIEW_TRAINING} for n in graph["node"])
|
||||
self.graph_name = graph["name"] if is_subgraph else ""
|
||||
self.graph_values = {"": None, **{i["name"]: i["parsed_tensor"] for i in graph["initializer"]}}
|
||||
self.graph_inputs = {i["name"]: i["parsed_type"] for i in graph["input"] if i["name"] not in self.graph_values}
|
||||
self.graph_outputs = tuple(o["name"] for o in graph["output"])
|
||||
@@ -418,12 +414,6 @@ class OnnxRunner:
|
||||
self.variable_dims: dict[str, int] = {}
|
||||
self.onnx_ops = onnx_ops
|
||||
|
||||
@classmethod
|
||||
def _from_subgraph(cls, graph: dict) -> "OnnxRunner":
|
||||
subgraph = cls.__new__(cls)
|
||||
subgraph._init_from_graph(graph, is_subgraph=True)
|
||||
return subgraph
|
||||
|
||||
def _parse_input(self, name: str, value: Any, spec: OnnxValue):
|
||||
if spec.is_optional and value is None: return None
|
||||
if spec.is_sequence:
|
||||
@@ -455,10 +445,9 @@ class OnnxRunner:
|
||||
return {name:Tensor.empty(*spec.shape, device=device, dtype=dtype or spec.dtype) for name, spec in self.graph_inputs.items()}
|
||||
|
||||
def to(self, device:str|None):
|
||||
self.graph_values = {k: (v.to(device) if isinstance(v, Tensor) else v) for k,v in self.graph_values.items()}
|
||||
self.graph_values = {k:v.to(device) if isinstance(v, Tensor) else v for k,v in self.graph_values.items()}
|
||||
self.graph_nodes = tuple(OnnxNode(n.op, n.opset_id, tuple(n.inputs), tuple(n.outputs),
|
||||
{k: (v.to(device) if isinstance(v, (Tensor, OnnxRunner)) else v) for k,v in n.opts.items()})
|
||||
for n in self.graph_nodes)
|
||||
{k:v.to(device) if isinstance(v, Tensor) else v for k,v in n.opts.items()}) for n in self.graph_nodes)
|
||||
return self
|
||||
|
||||
def __call__(self, inputs:dict[str, Any], debug=debug):
|
||||
@@ -472,9 +461,9 @@ class OnnxRunner:
|
||||
|
||||
# provide additional opts
|
||||
if node.op == "Split" and 'num_outputs' not in opts: opts['num_outputs'] = len(node.outputs)
|
||||
if node.op in {"Gradient", "If"}: opts['intermediate_tensors'] = self.graph_values
|
||||
if node.op == "Gradient": opts['intermediate_tensors'] = self.graph_values
|
||||
|
||||
if debug >= 1: print((f"[{self.graph_name}] " if self.graph_name else "") + f"{num}: op '{node.op}' opt {opts}")
|
||||
if debug >= 1: print(f"{num}: op '{node.op}' opt {opts}")
|
||||
if debug >= 2 and node.inputs: print("\tinputs:\n" + "\n".join(f"\t\t{x} - {i!r}" for x,i in zip(node.inputs, inps)))
|
||||
ret = self._select_op(node.op, node.opset_id)(*inps, **opts)
|
||||
ret = ret if isinstance(ret, tuple) else (ret,)
|
||||
@@ -554,23 +543,6 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
|
||||
return __decorator
|
||||
|
||||
# ***** Property/Graph Ops *****
|
||||
def If(condition:Tensor, else_branch:OnnxRunner, then_branch:OnnxRunner, intermediate_tensors:dict[str, Tensor]):
|
||||
def run_branch(branch:OnnxRunner):
|
||||
branch.graph_values.update(intermediate_tensors)
|
||||
out = branch({k:intermediate_tensors[k] for k in branch.graph_inputs.keys()})
|
||||
# dereference intermediate tensors so Buffer can be deallocated
|
||||
for k in intermediate_tensors: del branch.graph_values[k]
|
||||
return out
|
||||
# both branch must be ran before the condition can be evaluated
|
||||
else_out, then_out = run_branch(else_branch), run_branch(then_branch)
|
||||
assert len(else_out) == len(then_out), f"else_out and then_out must have the same number of outputs: {len(else_out)} != {len(then_out)}"
|
||||
# can use where op when output shape is the same
|
||||
if all(t.shape == e.shape for t,e in zip(then_out.values(), else_out.values())):
|
||||
return tuple(condition.where(t,e) for t,e in zip(then_out.values(), else_out.values()))
|
||||
# otherwise, use condition to select the output in python
|
||||
cond = _resolve_const(_cached_to_python_const(condition))
|
||||
return tuple(t if cond else e for t,e in zip(then_out.values(), else_out.values()))
|
||||
|
||||
def Identity(x:Tensor): return x
|
||||
def Constant(sparse_value:Tensor|None=None, value:Tensor|None=None, value_float:float|None=None, value_floats:list[float]|None=None,
|
||||
value_int:int|None=None, value_ints:list[int]|None=None, value_string:str|None=None, value_strings:list[str]|None=None):
|
||||
|
||||
@@ -316,9 +316,7 @@ class MetalRenderer(CStyleLanguage):
|
||||
|
||||
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None):
|
||||
prefix = ["#include <metal_stdlib>","using namespace metal;"]
|
||||
wargs = wmma_args(uops)
|
||||
if len(wargs) > 0: wargs = wargs[0:1]
|
||||
for name, _, dtype_in, dtype_out, _, _, _, _ in wargs: prefix.append(
|
||||
for name, _, dtype_in, dtype_out, _, _, _, _ in wmma_args(uops): prefix.append(
|
||||
f"""{(dstr_out:=self.render_dtype(dtype_out.vec(2)))} __{name}({(dstr_in:=self.render_dtype(dtype_in.vec(2)))} a, {dstr_in} b, {dstr_out} c){{
|
||||
simdgroup_{self.render_dtype(dtype_in)}8x8 mat_a, mat_b; simdgroup_{self.render_dtype(dtype_out)}8x8 mat_c;
|
||||
mat_a.thread_elements()[0] = a[0]; mat_b.thread_elements()[0] = b[0]; mat_c.thread_elements()[0] = c[0];
|
||||
|
||||
@@ -189,7 +189,6 @@ def map_partial_contiguous(ctx:RangeifyContext, x:UOp, idx:UOp):
|
||||
ranges.append(ctx.new_range(s) if resolve(s!=1) else UOp.const(dtypes.int, 0))
|
||||
new_ranges.append(ranges[-1])
|
||||
ret = x.src[0].index(*ranges).bufferize(*[x for x in new_ranges if x.op is not Ops.CONST], arg=x.device)
|
||||
if len(ret.ranges): ret = ret.replace(arg=AddrSpace.LOCAL) # if some ranges are still open, this has to be LOCAL
|
||||
return ret.index(*passthrough_idx)
|
||||
|
||||
def map_contiguous(ctx:RangeifyContext, x:UOp):
|
||||
@@ -239,10 +238,7 @@ def index_child(ctx:RangeifyContext, c:UOp, x:UOp, idx:UOp):
|
||||
# index based on the shared ranges
|
||||
ret = c.index(*out_rngs)
|
||||
# if all ranges aren't the same between children, we have to bufferize
|
||||
if len(idx_ranges) > 0:
|
||||
ret = ret.bufferize(*end_ranges, arg=x.device)
|
||||
if len(ret.ranges): ret = ret.replace(arg=AddrSpace.LOCAL) # if some ranges are still open, this has to be LOCAL
|
||||
ret = ret.index(*[idx.src[1+i] for i in idx_ranges])
|
||||
if len(idx_ranges) > 0: ret = ret.bufferize(*end_ranges, arg=x.device).index(*[idx.src[1+i] for i in idx_ranges])
|
||||
return ret
|
||||
|
||||
def children_gate(ctx:RangeifyContext, idx:UOp, c:UOp):
|
||||
@@ -338,7 +334,7 @@ def bufferize_to_store(x:UOp, locals_allowed=False):
|
||||
shape = tuple([int(r.vmax+1) for r in rngs])
|
||||
size = prod(shape)
|
||||
assert size > 0, f"no zero sized buffers {shape}"
|
||||
sdtype = x.dtype.ptr(size=size, addrspace=AddrSpace.GLOBAL if not isinstance(x.arg, AddrSpace) else x.arg)
|
||||
sdtype = x.dtype.ptr(size=size, addrspace=AddrSpace.GLOBAL if not isinstance(x.arg, tuple) else x.arg[0])
|
||||
if x.src[0].op is Ops.ASSIGN:
|
||||
assign_target, assign_src = x.src[0].src
|
||||
assert assign_target.op is Ops.INDEX
|
||||
@@ -348,7 +344,7 @@ def bufferize_to_store(x:UOp, locals_allowed=False):
|
||||
buf = UOp.new_buffer(x.arg, size, x.dtype)
|
||||
else:
|
||||
if not locals_allowed: return None
|
||||
buf = UOp(Ops.DEFINE_LOCAL, sdtype, arg=0) #UOp.unique().arg)
|
||||
buf = UOp(Ops.DEFINE_LOCAL, sdtype, arg=x.arg[1])
|
||||
return buf.reshape(shape).index(*rngs, dtype=sdtype).store(x.src[0], *rngs, dtype=sdtype).forced_reshape(shape, dtype=x.dtype)
|
||||
|
||||
pm_add_buffers_local = pm_mops+PatternMatcher([
|
||||
|
||||
+2
-5
@@ -142,7 +142,6 @@ class UOp(MathTrait, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.INDEX and self.src[0].op in {Ops.DEFINE_GLOBAL, Ops.DEFINE_LOCAL, Ops.DEFINE_REG,
|
||||
Ops.BUFFER, Ops.BUFFERIZE, Ops.VECTORIZE, Ops.STORE}:
|
||||
return None
|
||||
if self.op is Ops.BARRIER: return None
|
||||
if self.op in GroupOp.Block: return None
|
||||
from tinygrad.shape.shapetracker import ShapeTracker
|
||||
# VIEW and MovementOps define a new ShapeTracker from the arg
|
||||
@@ -207,10 +206,8 @@ class UOp(MathTrait, metaclass=UOpMetaClass):
|
||||
ret: dict[UOp, None] = {}
|
||||
if self.op in range_start.keys():
|
||||
for s in self.src[:range_start[self.op]]: ret.update(s.ranges)
|
||||
delete_ranges = self.src[range_start[self.op]:]
|
||||
if len(delete_ranges):
|
||||
for s in UOp.sink(*delete_ranges).ranges:
|
||||
if s in ret: del ret[s]
|
||||
for s in self.src[range_start[self.op]:]:
|
||||
if s in ret: del ret[s]
|
||||
else:
|
||||
for s in self.src: ret.update(s.ranges)
|
||||
return ret
|
||||
|
||||
Reference in New Issue
Block a user