forked from tinygrad/tinygrad
105 lines
4.4 KiB
Python
105 lines
4.4 KiB
Python
import heapq
|
|
from typing import cast
|
|
from collections import defaultdict
|
|
from tinygrad.dtype import dtypes
|
|
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat
|
|
from tinygrad.helpers import panic
|
|
|
|
# only needed if device doesn't support gated stores
|
|
pm_linearize_cleanups = PatternMatcher([
|
|
# if statements are not allowed in the graph
|
|
(UPat((Ops.IF, Ops.ENDIF)), lambda: panic(RuntimeError("if not allowed in graph"))),
|
|
# gated INDEX becomes IF-STORE-ENDIF. this is the only use of IF-ENDIF
|
|
(UPat(Ops.STORE, name="u", src=(UPat(Ops.INDEX, src=(UPat(), UPat(), UPat(name="gate", dtype=dtypes.bool))).or_casted(), UPat()),
|
|
allow_any_len=True), lambda u, gate: (u, [mif:=UOp(Ops.IF, src=(gate, u.src[0])), u, UOp(Ops.ENDIF, src=(mif,))]))
|
|
])
|
|
|
|
# requires lst be toposorted. like graph rewrite, but for lines
|
|
def line_rewrite(lst:list[UOp], pm:PatternMatcher) -> list[UOp]:
|
|
newlst = []
|
|
replaced: dict[UOp, UOp] = {}
|
|
for u in lst:
|
|
nu = u.replace(src=tuple([replaced[x] for x in u.src]))
|
|
ret: tuple[UOp, list[UOp]] = cast(tuple[UOp, list[UOp]]|None, pm.rewrite(nu)) or (nu, [nu])
|
|
replaced[u] = ret[0]
|
|
newlst.extend(ret[1])
|
|
return newlst
|
|
|
|
def linearize(u:UOp) -> list[UOp]:
|
|
lst = list(u.toposort())
|
|
consumers: defaultdict[UOp, list[UOp]] = defaultdict(list)
|
|
in_degree:dict[UOp, int] = {}
|
|
priorities:dict[UOp, int] = {}
|
|
|
|
# get consumers and assign priorities
|
|
# NOTE: this requires the lst be locally toposorted
|
|
for u in reversed(lst):
|
|
for s in u.src: consumers[s].append(u)
|
|
in_degree[u] = len(u.src)
|
|
# put loads in the beginning of the block and prevent priority inversion. hack for BARRIER grouping too
|
|
priority = [0] + [priorities[x] for x in consumers[u]]
|
|
if u.op is Ops.LOAD: priority.append(-1000)
|
|
if u.op is Ops.BARRIER: priority.append(-1500)
|
|
# ranges are scheduled as late as possible so anything that can be outside is
|
|
# if u.op is Ops.RANGE: priority = [2000]
|
|
if u.op is Ops.END: priority = [-1000]
|
|
# move defines and consts to the top
|
|
if u.op in {Ops.DEFINE_GLOBAL, Ops.DEFINE_LOCAL, Ops.DEFINE_REG, Ops.DEFINE_VAR, Ops.SPECIAL, Ops.CONST}: priority.append(-2000)
|
|
priorities[u] = min(priority)
|
|
|
|
# number the uops in "ideal" order
|
|
nkey = {u:i for i,u in enumerate(sorted(lst, key=lambda x: (priorities[x],)+x.tuplize))}
|
|
|
|
# then force then to be toposorted in as close to the ideal order as possible
|
|
heapq.heapify(heap:=[(nkey[u],u) for u in lst if in_degree[u] == 0])
|
|
newlst = []
|
|
while heap:
|
|
newlst.append(u:=heapq.heappop(heap)[1])
|
|
for v in consumers[u]:
|
|
in_degree[v] -= 1
|
|
if in_degree[v] == 0: heapq.heappush(heap, (nkey[v],v))
|
|
|
|
assert len(newlst) == len(lst), f"len mismatch {len(newlst)} != {len(lst)}"
|
|
return line_rewrite(newlst, pm_linearize_cleanups)
|
|
|
|
class CFGContext:
|
|
def __init__(self, sink:UOp):
|
|
# there are 3 relationships between ranges:
|
|
# nested, meaning endrange y is a dependency of endrange x and range x is a dependency of endrange y
|
|
# dependent, meaning endrange y is a dependency of endrange x and range x is not a dependency of endrange y
|
|
# independent, endrange y is not a dependency of endrange x
|
|
# everything is nested inside the sink
|
|
deps: dict[UOp, dict[UOp, None]] = {}
|
|
nesting: dict[UOp, UOp] = {}
|
|
for u in sink.toposort():
|
|
# get the deps from the src
|
|
deps[u] = {}
|
|
for s in u.src: deps[u] |= deps[s]
|
|
|
|
if u.op in (Ops.END, Ops.SINK):
|
|
nesting |= {x:u for x in deps[u] if x.op is Ops.END and (u.op is Ops.SINK or u.src[1] in deps[x]) and x not in nesting}
|
|
if u.op in (Ops.RANGE, Ops.END): deps[u][u] = None
|
|
|
|
self.edges: dict[UOp, UOp] = {}
|
|
siblings: dict[UOp, list[UOp]] = {}
|
|
for k,vv in nesting.items(): siblings.setdefault(vv, []).append(k)
|
|
for k,v in siblings.items():
|
|
# ranges that have dependencies on other siblings need to be scheduled after them
|
|
order = sorted(v, key=lambda x: len([u for u in v if u in deps[x]]))
|
|
zipped = zip(order, order[1:]) if k.op is Ops.SINK else zip([k.src[1]] + order, order)
|
|
for x,y in zipped: self.edges[y.src[1]] = x
|
|
|
|
pm_add_control_flow = PatternMatcher([
|
|
(UPat(Ops.RANGE, name="x"), lambda ctx,x: x.replace(src=x.src+(y,)) if (y:=ctx.edges.get(x)) is not None else None),
|
|
])
|
|
|
|
def do_split_ends(e:UOp):
|
|
ret = e.src[0]
|
|
for r in list(UOp.sink(*e.src[1:]).ranges)[::-1]: ret = ret.end(r)
|
|
return ret
|
|
|
|
pm_split_ends = PatternMatcher([
|
|
# split the ends
|
|
(UPat(Ops.END, name="e"), do_split_ends),
|
|
])
|