forked from tinygrad/tinygrad
* delete Ops.FUNCTION/GETTUPLE/TUPLE: call outputs are AFTER on RETURNED placeholders value-producing calls: the body is a plain parametric program that stores outputs into output PARAMs (slots after the input PARAMs). the RETURNED placeholders are inputs to the call, bound to the output PARAMs positionally wherever the call is resolved, and callers AFTER on them like normal buffers. gradient flows through the generic AFTER rule; everything is just Ops.CALL. * RETURNED identity is its placement in the call srcs, not a nonce slot=-1 merging collapses duplicate-signature outputs into one uop (t+1,t+2 grads and multi-grad backward calls dedupe wrongly), and skipping the uop cache breaks schedule_cache (stale linear hits since structural keys assume interning). instead the RETURNED's placement (output index among call srcs) is its identity: identical call constructions merge deterministically, positions never collide. * resolve RETURNED afters in the tensor graph like values (master parity with gettuple) - remove the CONTIGUOUS wrap of tagged call-output afters, it forced call outputs (e.g. local shard amax) into their own buffer/kernel instead of inlining - inline RETURNED afters at transform time via returned_after_finalize, dissolving to values for consumers; calls with bound-variable or unresolved UNSHARD args keep the schedule-time resolution path - allow movement ops (flat-storage views) in kernel graph value positions in the spec - port embedding backward + extra/llama_kernels (local_abs_max, rmsnorm) to the new API * use SINK, not GROUP, for gradient value containers spec.py only blesses GROUP of stores/groups/loops; the gradient value bundles (the forward values, root_grad seeds, and the after->call gradient edge) are plain value containers, and SINK-of-values is already in the spec. also fix extra/llama_kernels/rmsnorm: returned_outputs is a property * CALL is positional: RETURNS work in any src position, convention lives in call_outputs - all resolution paths (gradient, precompile transform, binding) locate RETURNEDs by identity, not by "last srcs"; only call_outputs builds the args-first layout - grad_fxn padding aligns grads with the call's actual src positions - add test_two_return/precompiled * source-compat shim for maketuple/gettuple so foreign code built before the redesign keeps working UOp.maketuple returns a _LegacyTupleValues holder; .call builds call_outputs; CALL.gettuple(i) is returned_outputs[i]. the produced graphs are identical to the new-api versions, so nn/extra/mlperf code is reverted to upstream text * simplify function.py call construction + drop the resolved-call cache - function.py: single and tuple returns both build the call through call_outputs - tensor.py: resolve_function is deterministic and interned, the global cache was unneeded * bind zero-offset views of flat storage to the storage instead of padding them call args need offset 0 and enough length, not views: flat_storage collapses the zero-offset contiguous view chain to the sized storage base, so resolved call args are storage-bare like master (no PAD/SHRINK chains in the kernel graph) * spec.py: drop stray rebase-collision edits, keep only the RETURNED changes * test_multitensor: revert to master, the gettuple shim covers it * materialize all tagged RETURNED afters into real buffers call outputs need real storage regardless of whether they are finals of the current realize: deferred/stateful outputs (the fp8 grad-amax mailbox) are consumed by later realize steps as call args, where a resolved value would have no ranges * call input buffers: wrap RETURNED-based afters, not real-buffer afters precompiled call input binding kept any AFTER unwrapped; an AFTER on a RETURNED placeholder has no storage behind it, so its value leaked into the kernel graph with no consumer able to register ranges (llama3 8B fp8 mailbox pipeline crash). materialize afters whose base has no buffer identity instead. this was the fix matching master for the REDUCE-has-no-ranges crash and restores the llama-kernels amax kernel count * call slots are src positions, always; never rearrange one upstream cause behind the three P1 findings: the raw CALL machinery binds positionally (resolve_function params, gradient padding) but a second args-first convention crept in where RETURNEDs get moved to trailing slots. position is identity now: - transform_precompiled_call keeps RETURNEDs' original src positions: outs take their places, other args become input buffers; no slot renumbering - implicit gradients are emitted aligned to original src positions (None at RETURNED positions) - flat_storage drops the hand-rolled contiguity analysis: reshape itself is the flat-prefix check (it raises ValueError); strided views materialize first * nits on call slot positions; regression tests for interspersed RETURNED - flat_storage back to pad_to().reshape() (reshape keeps movement views, it is not a contiguity check) - input_buffer checks has_buffer_identity(after_ok=True) - TestArgOrder: interspersed RETURNED (plain + precompiled transform), its gradient, padded and strided function inputs * device fixes * TestArgOrder: padded regression uses zero-start padded/shrunk view * TestArgOrder: clone to force buffer identity in padded/strided regression tests * slim: revert prepare formatting, drop reverted-bug tests, restore viz guards, clean comments, mirror returned on param * gut transform_precompiled_call, delete returned_after_finalize the transform keeps master's shape; the prepare-stage resolve_AFTER rule already inlines plain call outputs, and materialization is owned by the input-buffer rule (afters on real buffers bind, afters on RETURNEDs contiguous) * update spec for returned * transform_precompiled_call: inline the input-buffer rule, drop sorted() (body stores are already slot-ordered) * drop dead RETURNED-era rules: prepare's after-shell strip (leftover from returned_after_finalize, which is gone), redundant pattern-covered SINK check, defensive slot-sorts (bodies are slot-ordered by construction) * drop final_tags: final outputs of value calls materialize at sink construction The set of finals is already known precisely (the big_sink's srcs), so track nothing: wrap each final AFTER-on-RETURNED in CONTIGUOUS right after numbering. Precompiled calls are excluded - transform_precompiled_call in the flatten pass gives their outputs real buffers, and wrapping before that transform leaves a stale tag that breaks the output copy. * drop unused default_dtype import
1135 lines
48 KiB
Python
1135 lines
48 KiB
Python
import unittest
|
|
import decimal, sys, json, contextlib, tempfile, pickle, io, math, pathlib
|
|
from dataclasses import dataclass
|
|
from typing import Generator
|
|
|
|
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, TrackedPatternMatcher, graph_rewrite, rewrite_group
|
|
from tinygrad.uop.symbolic import sym
|
|
from tinygrad.dtype import dtypes, AddrSpace
|
|
from tinygrad.helpers import colored, ansistrip, flatten, TracingKey, ProfileRangeEvent, ProfileEvent, Context, cpu_events, profile_marker
|
|
from tinygrad.helpers import cpu_profile, ProfilePointEvent, unwrap, VIZ, BEAM
|
|
from tinygrad.device import Buffer
|
|
|
|
from tinygrad.uop.ops import tracked_keys, tracked_ctxs, uop_fields, active_rewrites, active_group, _name_cnt, RewriteTrace
|
|
from tinygrad.viz.serve import load_rewrites, get_full_rewrite, uop_to_json, VizData, get_render, addrspace_colors
|
|
from tinygrad.codegen import do_to_program
|
|
|
|
@rewrite_group(name=True)
|
|
def exec_rewrite(sink:UOp, pm_lst:list[PatternMatcher], names:None|list[str]=None) -> UOp:
|
|
for i,pm in enumerate(pm_lst):
|
|
sink = graph_rewrite(sink, TrackedPatternMatcher(pm.patterns), name=names[i] if names else None)
|
|
return sink
|
|
|
|
# small container class for the viz server module
|
|
class VizTrace:
|
|
# loader init
|
|
def __init__(self): self._data:VizData|None = None
|
|
@property
|
|
def data(self) -> VizData: return unwrap(self._data)
|
|
def set_data(self) -> None:
|
|
data = VizData(RewriteTrace(tracked_keys.copy(), tracked_ctxs.copy(), uop_fields.copy()))
|
|
load_rewrites(data)
|
|
self._data = data
|
|
# the API
|
|
def list_items(self) -> list[dict]:
|
|
return self.data.ctxs
|
|
def get_details(self, rewrite_idx:int, step:int) -> Generator[dict, None, None]:
|
|
assert len(self.data.trace.rewrites) > rewrite_idx, f"only loaded {len(self.data.trace.rewrites)} traces, expecting at least {rewrite_idx}"
|
|
return get_full_rewrite(self.data, self.data.trace.rewrites[rewrite_idx][step])
|
|
|
|
@contextlib.contextmanager
|
|
def save_viz():
|
|
for lst in [tracked_keys, tracked_ctxs, active_rewrites, active_group, _name_cnt]: lst.clear()
|
|
Buffer.profile_events.clear()
|
|
cpu_events.clear()
|
|
viz = VizTrace()
|
|
with Context(VIZ=-1, TRACK_MATCH_STATS=2, PROFILE=1, PARALLEL=0):
|
|
yield viz
|
|
viz.set_data()
|
|
|
|
needs_tracked_pm = unittest.skipUnless(VIZ, "using TrackedPatternMatcher requires global VIZ=1")
|
|
|
|
class TestViz(unittest.TestCase):
|
|
def test_simple(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 0, 10)
|
|
exec_rewrite((a+0)*1, [sym])
|
|
lst = viz.list_items()
|
|
# VIZ displays rewrites in groups of tracked functions
|
|
self.assertEqual(len(lst), 1)
|
|
# each group has a list of steps
|
|
self.assertEqual(len(lst[0]["steps"]), 1)
|
|
# each step has a list of matches
|
|
self.assertEqual(lst[0]["steps"][0]["match_count"], 2)
|
|
|
|
def test_rewrites(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 0, 10)
|
|
exec_rewrite(a*1, [sym])
|
|
exec_rewrite(a*2, [sym])
|
|
lst = viz.list_items()
|
|
self.assertEqual(len(lst), 2)
|
|
# names dedup using a counter
|
|
self.assertEqual(lst[0]["name"], "exec_rewrite n1")
|
|
self.assertEqual(lst[1]["name"], "exec_rewrite n2")
|
|
|
|
def test_steps(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 0, 10)
|
|
exec_rewrite(a+1, [PatternMatcher([]), PatternMatcher([])], ["x", "y"])
|
|
steps = viz.list_items()[0]["steps"]
|
|
# steps can optionally have a name
|
|
self.assertEqual(steps[0]["name"], "x")
|
|
self.assertEqual(steps[1]["name"], "y")
|
|
|
|
def test_rewrite_location(self):
|
|
def inner(sink): return graph_rewrite(sink, PatternMatcher([]))
|
|
def outer(sink): return inner(sink)
|
|
with save_viz() as viz:
|
|
outer(UOp.variable("a", 1, 10))
|
|
lst = viz.list_items()
|
|
# step location comes from inner rewrite
|
|
fp, lineno = lst[0]["steps"][0]["loc"]
|
|
self.assertEqual(fp, inner.__code__.co_filename)
|
|
self.assertEqual(lineno, inner.__code__.co_firstlineno)
|
|
|
|
def test_exceptions(self):
|
|
# VIZ tracks rewrites up to and including the error
|
|
def count_3(x:UOp):
|
|
assert x.val <= 3
|
|
return UOp.const(x.val+1, x.dtype)
|
|
err_pm = PatternMatcher([(UPat.cvar("x"), count_3),])
|
|
a = UOp.const(1)
|
|
with save_viz() as viz:
|
|
with self.assertRaises(AssertionError): exec_rewrite(a, [err_pm])
|
|
lst = viz.list_items()
|
|
err_step = lst[0]["steps"][0]
|
|
self.assertEqual(err_step["match_count"], 4) # 3 successful rewrites + 1 err
|
|
|
|
def test_default_name(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 1, 10)
|
|
@rewrite_group()
|
|
def name_default(): return graph_rewrite(a, PatternMatcher([]))
|
|
name_default()
|
|
lst = viz.list_items()
|
|
self.assertEqual(lst[0]["name"], "name_default n1")
|
|
|
|
# name can also come from a function that returns a string
|
|
def test_dyn_name_fxn(self):
|
|
with save_viz() as viz:
|
|
@rewrite_group(name=lambda *args,ret,**kwargs: ret.render())
|
|
def name_from_fxn(s:UOp, arg:list|None=None): return graph_rewrite(s, PatternMatcher([]))
|
|
name_from_fxn(UOp.variable("a", 1, 10)+1, arg=["test"])
|
|
lst = viz.list_items()
|
|
# name gets deduped by the function call counter
|
|
self.assertEqual(lst[0]["name"], "(a+1) n1")
|
|
|
|
# name can also come from a function that returns a TracingKey
|
|
def test_tracing_key(self):
|
|
with save_viz() as viz:
|
|
@rewrite_group(name=lambda inp,ret: TracingKey("custom_name", (inp,)))
|
|
def test(s:UOp): return graph_rewrite(s, PatternMatcher([]))
|
|
test(UOp.variable("a", 1, 10)+1)
|
|
lst = viz.list_items()
|
|
# NOTE: names from TracingKey do not get deduped
|
|
self.assertEqual(lst[0]["name"], "custom_name")
|
|
|
|
def test_nested_rewrite_group(self):
|
|
with save_viz() as viz:
|
|
@rewrite_group(name=lambda x,ret: TracingKey(f"inner fxn for {x.render()}", (ret,)))
|
|
def inner(x:UOp): return graph_rewrite(x, PatternMatcher([]), name="each")
|
|
@rewrite_group(name=lambda *args,ret: f"outer rewrite of {len(args)} inputs")
|
|
def outer(*xs:tuple[UOp, ...]): return graph_rewrite(UOp.sink(*[inner(x) for x in xs]), PatternMatcher([]), name="all")
|
|
items = ["a", "b", "c"]
|
|
outer(*[UOp.variable(x, 1, 10) for x in items])
|
|
lst = viz.list_items()
|
|
# inner calls fall outside the outer call
|
|
self.assertEqual(len(lst), len(items)+1)
|
|
self.assertEqual(lst[0]["name"], f"outer rewrite of {len(items)} inputs n1")
|
|
steps = lst[0]["steps"]
|
|
self.assertEqual(len(steps), 1)
|
|
self.assertEqual(steps[0]["name"], "all")
|
|
for i in range(len(items)):
|
|
self.assertEqual(lst[i+1]["name"], f"inner fxn for {items[i]}")
|
|
steps = lst[i+1]["steps"]
|
|
self.assertEqual(len(steps), 1)
|
|
self.assertEqual(steps[0]["name"], "each")
|
|
|
|
def test_rewrite_group_nested(self):
|
|
with save_viz() as viz:
|
|
@rewrite_group(new_ctx=False)
|
|
def nested_function(u:UOp):
|
|
for i in range(2): graph_rewrite(u, PatternMatcher([]), name=f"step {i+1}")
|
|
|
|
@rewrite_group()
|
|
def main_rewrite(u:UOp):
|
|
graph_rewrite(u, PatternMatcher([]), name="init")
|
|
nested_function(u)
|
|
|
|
main_rewrite(UOp.variable("a", 1, 10)+UOp.variable("b", 1, 10))
|
|
steps = viz.list_items()[0]["steps"]
|
|
self.assertEqual(steps[0]["name"], "init")
|
|
self.assertEqual(steps[1]["name"], "nested_function")
|
|
self.assertEqual(len(steps), 4)
|
|
|
|
def test_rewrite_group_invalid_arg(self):
|
|
with save_viz():
|
|
@rewrite_group(new_ctx=False)
|
|
def invalid_fxn(arg:str): return graph_rewrite(UOp(Ops.SINK), PatternMatcher([]))
|
|
with self.assertRaisesRegex(AssertionError, "invalid match tracing input"):
|
|
invalid_fxn("test")
|
|
|
|
def test_colored_label(self):
|
|
# NOTE: dataclass repr prints literal escape codes instead of unicode chars
|
|
@dataclass(frozen=True)
|
|
class TestStruct:
|
|
colored_field: str
|
|
a = UOp(Ops.PYLITERAL, arg=TestStruct(colored("xyz", "magenta")+colored("12345", "blue")))
|
|
a2 = uop_to_json(VizData(), a)[id(a)]
|
|
self.assertEqual(ansistrip(a2["label"]), f"PYLITERAL\n{TestStruct.__qualname__}(colored_field='xyz12345')")
|
|
|
|
def test_colored_label_multiline(self):
|
|
with save_viz() as viz:
|
|
arg = colored("x", "green")+"\n"+colored("y", "red")+colored("z", "yellow")+colored("ww\nw", "magenta")
|
|
src = [Tensor.empty(1).uop for _ in range(10)]
|
|
a = UOp(Ops.PYLITERAL, src=tuple(src), arg=arg)
|
|
exec_rewrite(a, [PatternMatcher([])])
|
|
a2 = next(viz.get_details(0, 0))["graph"][id(a)]
|
|
self.assertEqual(ansistrip(a2["label"]), "PYLITERAL\nx\nyzww\nw")
|
|
|
|
def test_inf_loop(self):
|
|
a = UOp.const(3)
|
|
b = UOp.const(4)
|
|
pm = PatternMatcher([
|
|
(UPat(Ops.CONST, arg=3, name="x"), lambda x: UOp.const(4, x.dtype)),
|
|
(UPat(Ops.CONST, arg=4, name="x"), lambda x: UOp.const(3, x.dtype)),
|
|
])
|
|
with save_viz() as viz:
|
|
# use smaller stack limit for faster test (default is 250000)
|
|
with Context(REWRITE_STACK_LIMIT=100): self.assertRaises(RuntimeError, exec_rewrite, a, [pm])
|
|
graphs = flatten(x["graph"].values() for x in viz.get_details(0, 0))
|
|
self.assertEqual(graphs[0], uop_to_json(VizData(), a)[id(a)])
|
|
self.assertEqual(graphs[1], uop_to_json(VizData(), b)[id(b)])
|
|
# fallback to REWRITE_ERROR with the error message
|
|
self.assertIn("REWRITE_ERROR\nTraceback", graphs[2]["label"])
|
|
# cut after the first error, instead of going through all REWRITE_STACK_LIMIT matches
|
|
self.assertEqual(len(graphs), 3)
|
|
|
|
def test_walk_rewrite(self):
|
|
from tinygrad.uop.ops import _substitute
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 0, 10)
|
|
graph_rewrite(a + 4, TrackedPatternMatcher(_substitute.patterns), {a:a+1}, walk=True)
|
|
list(viz.get_details(0, 0))
|
|
|
|
def test_enter_calls_rewrite(self):
|
|
pm = PatternMatcher([(UPat(Ops.CONST, arg=3, name="x"), lambda x: UOp.const(4, x.dtype))])
|
|
with save_viz() as viz:
|
|
inner = UOp.const(3)
|
|
call = UOp(Ops.CALL, src=(UOp(Ops.SINK, src=(inner,)),))
|
|
graph_rewrite(call, TrackedPatternMatcher(pm.patterns), enter_calls=True)
|
|
details = list(viz.get_details(0, 0))
|
|
self.assertTrue(details[-1]["change"], "viz replay should detect change inside CALL")
|
|
|
|
def test_const_node_visibility(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a", 0, 10, dtype=dtypes.int)
|
|
z = UOp.const(0)
|
|
y = UOp.const(math.pi)
|
|
alu = a*z
|
|
ret = exec_rewrite(sink:=UOp.sink(alu, y), [sym])
|
|
lst = viz.list_items()
|
|
self.assertEqual(len(lst), 1)
|
|
graphs = [x["graph"] for x in viz.get_details(0, 0)]
|
|
# const is always in the graph, client side hides exclude=True nodes by default
|
|
self.assertEqual(list(graphs[0]), [id(a), id(z), id(alu), id(y), id(sink)])
|
|
self.assertTrue(graphs[0][id(z)]["exclude"])
|
|
self.assertTrue(graphs[0][id(y)]["exclude"])
|
|
self.assertFalse(graphs[0][id(alu)]["exclude"])
|
|
self.assertEqual(graphs[0][id(y)]["label"].split("\n")[:2], ["CONST", "3.14159"])
|
|
self.assertEqual(list(graphs[1]), [id(u) for u in ret.toposort()]) # rewrite graph keys follow the rewritten sink's toposort
|
|
|
|
def test_const_reshape_expand_folded(self):
|
|
# CONST->EXPAND should be folded into the ALU node, not shown as separate EXPAND nodes
|
|
c = UOp.const(1.0).expand((3,4)) # creates CONST->EXPAND chain
|
|
a = UOp.variable("a", 0.0, 10.0, dtypes.float)
|
|
alu = a + c
|
|
with save_viz() as viz:
|
|
graph_rewrite(alu, PatternMatcher([]))
|
|
graph = [x["graph"] for x in viz.get_details(0, 0)][0]
|
|
excluded_nodes = {v["label"].split("\n")[0] for v in graph.values() if v["exclude"]}
|
|
self.assertIn("CONST", excluded_nodes)
|
|
self.assertIn("STACK", excluded_nodes)
|
|
self.assertIn("EXPAND", excluded_nodes)
|
|
self.assertIn("CONST1 1", graph[id(alu)]["label"])
|
|
|
|
def test_stack_movement_not_folded_unless_all_const(self):
|
|
a = UOp.variable("a", 0, 10, dtype=dtypes.int)
|
|
c = UOp.const(1)
|
|
stack = a.stack(c)
|
|
reshaped = stack.reshape((1, 2))
|
|
graph = uop_to_json(VizData(), reshaped)
|
|
self.assertFalse(graph[id(stack)]["exclude"])
|
|
|
|
const_stack = c.stack(UOp.const(2))
|
|
const_reshaped = const_stack.reshape((1, 2))
|
|
const_graph = uop_to_json(VizData(), const_reshaped)
|
|
self.assertTrue(const_graph[id(const_stack)]["exclude"])
|
|
reshape_node = const_graph[id(const_reshaped)]
|
|
self.assertFalse(reshape_node["exclude"])
|
|
self.assertIn("STACK0 {1,2} Ops.CONST", reshape_node["label"].split("\n"))
|
|
|
|
# VIZ displays nested graph_rewrites in a tree view
|
|
|
|
def leaf_rewrite(x:UOp): return x.rtag(1) if x.tag is None else None
|
|
leaf = TrackedPatternMatcher([(UPat(Ops.PARAM, name="x"), leaf_rewrite)])
|
|
|
|
def branch_rewrite(x:UOp, y:UOp):
|
|
if x.tag is not None: return
|
|
x2 = graph_rewrite(x, leaf, name="leaf_left")
|
|
y2 = graph_rewrite(y, leaf, name="leaf_right")
|
|
return x2 * y2
|
|
branch = TrackedPatternMatcher([(UPat.var("x")+UPat.var("y"), branch_rewrite)])
|
|
|
|
def root_rewrite(root:UOp):
|
|
new_src = tuple(graph_rewrite(b, branch, name=f"branch_{i}") for i,b in enumerate(root.src))
|
|
return root.replace(src=new_src)
|
|
root = TrackedPatternMatcher([(UPat(Ops.SINK, src=UPat(Ops.ADD), name="root"), root_rewrite),])
|
|
|
|
class TestVizTree(unittest.TestCase):
|
|
def assertStepEqual(self, step:dict, want:dict):
|
|
for k,v in want.items():
|
|
self.assertEqual(step[k], v, f"failed at '{k}': {v} != {step[k]}\n{step=}")
|
|
|
|
def test_tree_view(self):
|
|
with save_viz() as viz:
|
|
a = UOp.variable("a",0,10,param=True)
|
|
b = UOp.variable("b",0,10,param=True)
|
|
c = UOp.variable("c",0,10,param=True)
|
|
d = UOp.variable("d",0,10,param=True)
|
|
sink = UOp.sink(a+b, c+d)
|
|
def tree_rewrite(): return graph_rewrite(sink, root, name="root")
|
|
tree_rewrite()
|
|
lst = viz.list_items()
|
|
steps = lst[0]["steps"]
|
|
self.assertEqual(len(steps), 1+2+4)
|
|
self.assertStepEqual(steps[0], {"name":"root", "depth":0, "match_count":1})
|
|
self.assertStepEqual(steps[1], {"name":"branch_0", "depth":1, "match_count":1})
|
|
self.assertStepEqual(steps[2], {"name":"leaf_left", "depth":2, "match_count":1})
|
|
self.assertStepEqual(steps[3], {"name":"leaf_right", "depth":2, "match_count":1})
|
|
self.assertStepEqual(steps[4], {"name":"branch_1", "depth":1, "match_count":1})
|
|
self.assertStepEqual(steps[5], {"name":"leaf_left", "depth":2, "match_count":1})
|
|
self.assertStepEqual(steps[6], {"name":"leaf_right", "depth":2, "match_count":1})
|
|
|
|
import gc
|
|
|
|
def bufs_allocated() -> int:
|
|
gc.collect()
|
|
return sum([type(x).__name__ == "Buffer" and type(x).__module__ == "tinygrad.device" for x in gc.get_objects()])
|
|
|
|
class TestVizGC(unittest.TestCase):
|
|
def test_gc(self):
|
|
with save_viz() as viz:
|
|
init = bufs_allocated()
|
|
a = UOp.new_buffer("NULL", 10, dtypes.char)
|
|
a.buffer.allocate()
|
|
exec_rewrite(a, [PatternMatcher([])])
|
|
del a
|
|
self.assertEqual(bufs_allocated()-init, 0)
|
|
lst = viz.list_items()
|
|
self.assertEqual(len(lst), 1)
|
|
|
|
@unittest.skip("it's not generic enough to handle arbitrary UOps in arg")
|
|
def test_gc_uop_in_arg(self):
|
|
with save_viz() as viz:
|
|
init = bufs_allocated()
|
|
a = UOp.new_buffer("NULL", 10, dtypes.char)
|
|
a.buffer.allocate()
|
|
exec_rewrite(UOp(Ops.PYLITERAL, src=(a,), arg=a), [PatternMatcher([])])
|
|
del a
|
|
self.assertEqual(bufs_allocated()-init, 0)
|
|
lst = viz.list_items()
|
|
self.assertEqual(len(lst), 1)
|
|
|
|
# VIZ integrates with other parts of tinygrad
|
|
|
|
from tinygrad import Tensor, Device, TinyJit, Variable, function
|
|
|
|
class TestVizIntegration(unittest.TestCase):
|
|
def test_link_sched_codegen(self):
|
|
c1 = Tensor.empty(4, device="NULL")
|
|
c2 = Tensor.empty(8, device="NULL")
|
|
# uniquely named A = B + 1 kernel
|
|
kernel_name = f"custom_add1_link_sched_codegen_{BEAM.value}"
|
|
def custom_add1(A:UOp, B:UOp): return A[0].store(B[0]+1).sink(arg=KernelInfo(kernel_name))
|
|
with save_viz() as viz:
|
|
c1 = Tensor.custom_kernel(c1, c2, fxn=custom_add1)[0]
|
|
c1.realize()
|
|
lst = viz.list_items()
|
|
# schedule graph CALL nodes have a link to jump to codegen
|
|
sched_idx = next(i for i,l in enumerate(lst) if l["name"].startswith("Schedule"))
|
|
viz_kernel = next(i for i,s in enumerate(lst[sched_idx]["steps"]) if s["name"] == "View Kernel Graph")
|
|
graph = next(viz.get_details(sched_idx, viz_kernel))["graph"]
|
|
call_nodes = [n for n in graph.values() if n["label"].startswith("CALL")]
|
|
for i,n in enumerate(call_nodes):
|
|
assert n["ref"] is not None
|
|
self.assertEqual(lst[n["ref"]]["name"], kernel_name)
|
|
assert kernel_name[i] in n["label"], f"CALL must contain kernel name, got {n['label']}"
|
|
# UOp addrspace is colored
|
|
for u in graph.values():
|
|
if u["label"].startswith("PARAM\n"): self.assertEqual(u["addrspace"], addrspace_colors[AddrSpace.GLOBAL])
|
|
|
|
def test_link_sched_codegen_beam(self):
|
|
with Context(BEAM=2):
|
|
self.test_link_sched_codegen()
|
|
|
|
@Context(TRACEMETA=2)
|
|
def test_metadata_tracing(self):
|
|
with save_viz() as viz:
|
|
a = Tensor.empty(1)
|
|
b = Tensor.empty(1)
|
|
metadata = (alu:=a+b).uop.metadata
|
|
alu.schedule_linear()
|
|
graph = next(viz.get_details(0, 0))["graph"]
|
|
self.assertEqual(len([n for n in graph.values() if repr(metadata) in n["label"]]), 1)
|
|
|
|
# tracing also works without a rewrite_group context
|
|
# all graph_rewrites get put into the default group
|
|
def test_default_tracing(self):
|
|
with save_viz() as viz:
|
|
def test(root):
|
|
return graph_rewrite(root, sym)
|
|
test(c:=UOp.const(1))
|
|
test(c+1)
|
|
ls = viz.list_items()
|
|
self.assertEqual(len(ls), 1)
|
|
self.assertEqual(ls[0]["name"], "default graph_rewrite")
|
|
|
|
# using @rewrite_group organizes function calls into groups
|
|
# and nicely counts function calls.
|
|
def test_group_traces(self):
|
|
with save_viz() as viz:
|
|
@rewrite_group()
|
|
def test(root):
|
|
return graph_rewrite(root, sym)
|
|
test(c:=UOp.const(1))
|
|
test(c+1)
|
|
ls = viz.list_items()
|
|
self.assertEqual(len(ls), 2)
|
|
for i in range(2): self.assertEqual(ls[i]["name"], f"test n{i+1}")
|
|
|
|
# @rewrite_group always starts a new group.
|
|
def test_group_combined(self):
|
|
with save_viz() as viz:
|
|
def default_test(root): return graph_rewrite(root, sym)
|
|
tracked_test = rewrite_group()(default_test)
|
|
c = UOp.const(1)
|
|
default_test(c+1) # goes to the default group
|
|
tracked_test(c) # all rewrites after this go inside the second group.
|
|
default_test(c+2)
|
|
ls = viz.list_items()
|
|
self.assertEqual(len(ls), 2)
|
|
graph = next(viz.get_details(0, 0))["graph"]
|
|
# both operands of c+1 are the same bare weak CONST, so the graph has two nodes
|
|
self.assertEqual(list(graph), [id(c), id(c+1)])
|
|
self.assertTrue(graph[id(c)]["exclude"])
|
|
self.assertFalse(graph[id(c+1)]["exclude"])
|
|
self.assertEqual(list(next(viz.get_details(1, 0))["graph"]), [id(c)])
|
|
graph = next(viz.get_details(1, 1))["graph"]
|
|
self.assertEqual(list(graph), [id(c), id((c+2).src[1]), id(c+2)])
|
|
self.assertTrue(graph[id(c)]["exclude"])
|
|
self.assertTrue(graph[id((c+2).src[1])]["exclude"])
|
|
self.assertFalse(graph[id(c+2)]["exclude"])
|
|
|
|
def test_recurse(self):
|
|
with save_viz() as viz:
|
|
a = Tensor.empty(10)
|
|
for _ in range(10_000): a += a
|
|
graph_rewrite(a.uop, PatternMatcher([]))
|
|
lst = viz.list_items()
|
|
assert len(lst) == 1
|
|
|
|
def test_jit(self):
|
|
with save_viz():
|
|
@TinyJit
|
|
def f(a, b, c): return (a+b).contiguous().mul(3), c.add(1).contiguous().assign(a.to(c.device)), b.assign(c.to(b.device))
|
|
a, b, c = Tensor.empty(16, device="NULL"), Tensor.empty(16, device="NULL"), Tensor.empty(16, device="NULL:1")
|
|
for _ in range(3): Tensor.realize(*f(a, b, c))
|
|
out = load_profile(cpu_events)
|
|
self.assertEqual(["NULL", "NULL Graph", "NULL:SDMA:0", "NULL:1", "NULL:1:SDMA:0"], [k for k in out["layout"] if k.startswith("NULL")])
|
|
self.assertEqual(len(out["layout"]["NULL"]["events"]), 2*3)
|
|
self.assertEqual(len(out["layout"]["NULL:SDMA:0"]["events"]), 3)
|
|
self.assertEqual(len(out["layout"]["NULL Graph"]["events"]), 2)
|
|
for graph in out["layout"]["NULL Graph"]["events"]:
|
|
graph_st, graph_et = graph["st"], graph["st"]+graph["dur"]
|
|
for k in ["NULL", "NULL:1", "NULL:SDMA:0", "NULL:1:SDMA:0"]:
|
|
events = [e for e in out["layout"][k]["events"] if graph_st <= e["st"] and e["st"]+e["dur"] <= graph_et]
|
|
self.assertGreater(len(events), 0)
|
|
self.assertEqual([e["st"] for e in events], [graph_st+i*events[0]["dur"] for i in range(len(events))])
|
|
|
|
@needs_tracked_pm
|
|
def test_view_source(self):
|
|
def custom_fn(X:UOp):
|
|
X = X.flatten()
|
|
i = UOp.range(X.numel(), 0)
|
|
custom_op = UOp(Ops.CUSTOMI, src=(X[i],), arg=("{} + undeclared_name", X.dtype))
|
|
return X[i].store(custom_op).end(i).sink(arg=KernelInfo(name=f"custom_fn_{X.numel()}"))
|
|
x = Tensor.custom_kernel(Tensor.empty(1, device="CPU"), fxn=custom_fn)[0]
|
|
with save_viz() as viz:
|
|
with self.assertRaises(Exception) as e:
|
|
x.realize()
|
|
lst = viz.list_items()
|
|
codegen_idx = len(lst)-1
|
|
steps = lst[codegen_idx]["steps"]
|
|
lin_idx = next((i for i,s in enumerate(steps) if s["name"] == "View UOp List"), None)
|
|
src_idx = next((i for i,s in enumerate(steps) if s["name"] == "View Source"), None)
|
|
bin_idx = next((i for i,s in enumerate(steps) if s["name"] == "View Disassembly"), None)
|
|
assert all(i is not None for i in [lin_idx, src_idx, bin_idx]), f"linear, source and disasm must be visible in {steps}"
|
|
# Ops.LINEAR renders
|
|
lin_render = get_render(viz.data, steps[lin_idx]["query"])["src"]
|
|
self.assertIn("Ops.SINK", lin_render)
|
|
self.assertIn("Ops.CUSTOMI", lin_render)
|
|
# Ops.SOURCE renders
|
|
src_render = get_render(viz.data, steps[src_idx]["query"])["src"]
|
|
self.assertIn("undeclared_name", src_render)
|
|
# Ops.BINARY shows the error message since compile failed
|
|
bin_render = get_render(viz.data, steps[bin_idx]["query"])["src"]
|
|
self.assertIn(type(e.exception).__name__, bin_render)
|
|
|
|
def test_view_source_alt(self):
|
|
src = "void E_3(float* data0_3) {}"
|
|
binary = Device["CPU"].renderer.compiler.compile(src)
|
|
def custom_binary(X:UOp):
|
|
sink = UOp.sink(X, arg=KernelInfo("custom_binary"))
|
|
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=sink.src+(sink,)), UOp(Ops.SOURCE, arg=src),
|
|
UOp(Ops.BINARY, arg=binary)))
|
|
x = Tensor.custom_kernel(Tensor.empty(1, device="CPU"), fxn=custom_binary)[0]
|
|
with save_viz() as viz:
|
|
x.realize()
|
|
lst = viz.list_items()
|
|
codegen_idx = len(lst)-1
|
|
steps = lst[codegen_idx]["steps"]
|
|
src_idx = next((i for i,s in enumerate(steps) if s["name"] == "View Source"), None)
|
|
assert src_idx is not None, "must have source rendering in list"
|
|
src_render = get_render(viz.data, steps[src_idx]["query"])["src"]
|
|
self.assertEqual(src, src_render)
|
|
|
|
def test_profiler_duplicate_name(self):
|
|
kernel_name = "duplicate_name"
|
|
def one(A:UOp): return A[0].store(UOp.const(1.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
|
def zero(A:UOp): return A[0].store(UOp.const(0.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
|
with save_viz() as viz:
|
|
@TinyJit
|
|
def f(a:Tensor, b:Tensor): return Tensor.custom_kernel(a, fxn=one)[0], Tensor.custom_kernel(b, fxn=zero)[0]
|
|
a, b = Tensor.empty(4, device="NULL"), Tensor.empty(4, device="NULL")
|
|
# warmup
|
|
for _ in range(2): Tensor.realize(*f(a, b))
|
|
Tensor.realize(*f(a, b))
|
|
kernels = {i for i,c in enumerate(viz.list_items()) if c["name"] == kernel_name}
|
|
profile = decode_profile(unwrap(get_profile(viz.data, cpu_events)))
|
|
events = [e for e in profile["layout"]["NULL"]["events"] if e["name"] == kernel_name]
|
|
self.assertEqual({e["ref"] for e in events}, kernels)
|
|
|
|
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEvent, ProfileGraphEntry
|
|
from tinygrad.viz.serve import get_profile
|
|
from tinygrad.viz.cli import decode_profile
|
|
|
|
def load_profile(lst:list[ProfileEvent]) -> dict: return decode_profile(get_profile(VizData(), lst))
|
|
|
|
class TestVizProfiler(unittest.TestCase):
|
|
def test_transfer_uses_copy_device(self):
|
|
with save_viz():
|
|
a = Tensor.ones(1, device="NULL").contiguous().realize()
|
|
a.to("NULL:1").realize()
|
|
range_events = [e for e in cpu_events if isinstance(e, ProfileRangeEvent)]
|
|
compute_events = [e for e in range_events if e.device == "NULL"]
|
|
copy_events = [e for e in range_events if e.device.endswith(":SDMA:0")]
|
|
self.assertGreater(len(compute_events), 0, "expected compute events on base device")
|
|
self.assertGreater(len(copy_events), 0, "transfer must produce events with ':SDMA' device suffix")
|
|
|
|
def test_node(self):
|
|
prof = [ProfileRangeEvent(device='NV', name='E_2', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000))]
|
|
|
|
j = load_profile(prof)
|
|
|
|
dev_events = j['layout']['NV']['events']
|
|
self.assertEqual(len(dev_events), 1)
|
|
event = dev_events[0]
|
|
self.assertEqual(event['name'], 'E_2')
|
|
self.assertEqual(event['st'], 0)
|
|
self.assertEqual(event['dur'], 10)
|
|
assert event['ref'] is None
|
|
|
|
def test_copy_node(self):
|
|
prof = [ProfileRangeEvent(device='NV:SDMA:0', name='COPYxx', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileRangeEvent(device='NV:2:SDMA:0', name='COPYxx', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileDeviceEvent(device='NV:SDMA:0', tdiff=decimal.Decimal(-100)),
|
|
ProfileDeviceEvent(device='NV:2:SDMA:0', tdiff=decimal.Decimal(-80))]
|
|
|
|
j = load_profile(prof)
|
|
|
|
event = j['layout']['NV:SDMA:0']['events'][0]
|
|
self.assertEqual(event['name'], 'COPYxx')
|
|
self.assertEqual(event['st'], 0) # first event
|
|
self.assertEqual(event['dur'], 10)
|
|
|
|
event2 = j['layout']['NV:2:SDMA:0']['events'][0]
|
|
self.assertEqual(event2['st'], 20) # second event, diff clock
|
|
|
|
self.assertEqual(j["dur"], (event2["st"]+event2["dur"])-event["st"])
|
|
|
|
def test_copy_node_bandwidth(self):
|
|
sz = 256*1024*1024
|
|
dur = 10_000
|
|
prof = [ProfileRangeEvent(device='NV:SDMA:0', name=TracingKey("NV -> NV:1", ret=sz), st=decimal.Decimal(1000), en=decimal.Decimal(1000+dur)),
|
|
ProfileDeviceEvent(device='NV:SDMA:0', tdiff=decimal.Decimal(-1000))]
|
|
j = load_profile(prof)
|
|
event = j['layout']['NV:SDMA:0']['events'][0]
|
|
self.assertEqual(event['fmt'], {"B/s": sz/(dur*1e-6), "B": sz})
|
|
|
|
def test_graph(self):
|
|
prof = [ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000)),
|
|
ProfileDeviceEvent(device='NV:1:SDMA:0', tdiff=decimal.Decimal(-50)),
|
|
ProfileGraphEvent(ents=[ProfileGraphEntry(device='NV', name='E_25_4n2', st_id=0, en_id=1),
|
|
ProfileGraphEntry(device='NV:1:SDMA:0', name='NV -> NV:1', st_id=2, en_id=3)],
|
|
deps=[[], [0]],
|
|
sigs=[decimal.Decimal(1000), decimal.Decimal(1002), decimal.Decimal(1004), decimal.Decimal(1008)])]
|
|
|
|
j = load_profile(prof)
|
|
|
|
tracks = list(j['layout'])
|
|
self.assertEqual(tracks[0], 'NV')
|
|
self.assertEqual(tracks[1], 'NV Graph')
|
|
self.assertEqual(tracks[2], 'NV:1:SDMA:0')
|
|
|
|
nv_events = j['layout']['NV']['events']
|
|
self.assertEqual(nv_events[0]['name'], 'E_25_4n2')
|
|
self.assertEqual(nv_events[0]['st'], 0)
|
|
self.assertEqual(nv_events[0]['dur'], 2)
|
|
|
|
sdma_events = j['layout']['NV:1:SDMA:0']['events']
|
|
self.assertEqual(sdma_events[0]['name'], 'NV -> NV:1')
|
|
self.assertEqual(sdma_events[0]['st'], 954)
|
|
|
|
graph_events = j['layout']['NV Graph']['events']
|
|
self.assertEqual(graph_events[0]['st'], nv_events[0]['st'])
|
|
self.assertEqual(graph_events[0]['st']+graph_events[0]['dur'], sdma_events[0]['st']+sdma_events[0]['dur'])
|
|
|
|
def test_graph_copy_bandwidth(self):
|
|
sz = 256*1024*1024
|
|
dur = 10_000
|
|
prof = [ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000)),
|
|
ProfileDeviceEvent(device='NV:1:SDMA:0', tdiff=decimal.Decimal(-50)),
|
|
ProfileGraphEvent(ents=[ProfileGraphEntry(device='NV:1:SDMA:0', name=TracingKey("NV -> NV:1", ret=sz), st_id=0, en_id=1)],
|
|
deps=[[]],
|
|
sigs=[decimal.Decimal(1004), decimal.Decimal(1004+dur)])]
|
|
|
|
j = load_profile(prof)
|
|
sdma_events = j['layout']['NV:1:SDMA:0']['events']
|
|
self.assertEqual(sdma_events[0]["fmt"], {"B/s": sz/(dur*1e-6), "B": sz})
|
|
|
|
def test_block_ordering(self):
|
|
prof = [ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000)),
|
|
ProfileDeviceEvent(device='NV:1', tdiff=decimal.Decimal(-500)),
|
|
ProfileDeviceEvent(device='NV:SDMA:0', tdiff=decimal.Decimal(-100)),
|
|
ProfileRangeEvent(device='NV', name='E_2', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileRangeEvent(device='NV:1', name='E_3', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileRangeEvent(device='NV:SDMA:0', name='COPY', st=decimal.Decimal(1000), en=decimal.Decimal(1010)),
|
|
ProfileGraphEvent(ents=[ProfileGraphEntry(device='NV', name='E_2', st_id=0, en_id=1)],
|
|
deps=[[]], sigs=[decimal.Decimal(1000), decimal.Decimal(1010)])]
|
|
j = load_profile(prof)
|
|
# graph grouped with its device, memory at the end
|
|
self.assertListEqual(list(j['layout']), ['NV', 'NV Graph', 'NV:SDMA:0', 'NV:1'])
|
|
|
|
@unittest.skipIf(sys.platform == 'win32', "TODO: ops_amd import fails on windows")
|
|
def test_multi_sdma_ordering(self):
|
|
props = {"gfx_target_version": 0}
|
|
D, St, En = decimal.Decimal, decimal.Decimal(1000), decimal.Decimal(1010)
|
|
prof = [# 2 AMD GPUs, 2 SDMA engines each
|
|
ProfileDeviceEvent(device='AMD', tdiff=D(-1000), props=props),
|
|
ProfileDeviceEvent(device='AMD:1', tdiff=D(-900), props=props),
|
|
ProfileDeviceEvent(device='AMD:SDMA:0', tdiff=D(-100), props=props),
|
|
ProfileDeviceEvent(device='AMD:SDMA:1', tdiff=D(-80), props=props),
|
|
ProfileDeviceEvent(device='AMD:1:SDMA:0', tdiff=D(-60), props=props),
|
|
ProfileDeviceEvent(device='AMD:1:SDMA:1', tdiff=D(-40), props=props),
|
|
# compute + copy events
|
|
ProfileRangeEvent(device='AMD', name='E_1', st=St, en=En),
|
|
ProfileRangeEvent(device='AMD:1', name='E_2', st=St, en=En),
|
|
ProfileRangeEvent(device='AMD:SDMA:0', name='COPY0', st=St, en=En),
|
|
ProfileRangeEvent(device='AMD:SDMA:1', name='COPY1', st=St, en=En),
|
|
ProfileRangeEvent(device='AMD:1:SDMA:0', name='COPY2', st=St, en=En),
|
|
ProfileRangeEvent(device='AMD:1:SDMA:1', name='COPY3', st=St, en=En),
|
|
# graph spanning compute + copy on GPU 0
|
|
ProfileGraphEvent(ents=[ProfileGraphEntry(device='AMD', name='E_1', st_id=0, en_id=1),
|
|
ProfileGraphEntry(device='AMD:SDMA:0', name='COPY0', st_id=2, en_id=3)],
|
|
deps=[[], [0]], sigs=[St, En, St, En]),
|
|
# memory alloc on both GPUs
|
|
ProfilePointEvent(device='AMD', name='alloc', key=0, arg={"sz":1024, "dtype":dtypes.float}, ts=St),
|
|
ProfilePointEvent(device='AMD:1', name='alloc', key=1, arg={"sz":512, "dtype":dtypes.float}, ts=St)]
|
|
j = load_profile(prof)
|
|
# graph grouped with its device, memory at the end
|
|
self.assertListEqual(list(j['layout']),
|
|
['AMD', 'AMD Graph', 'AMD:SDMA:0', 'AMD:SDMA:1',
|
|
'AMD:1', 'AMD:1:SDMA:0', 'AMD:1:SDMA:1',
|
|
'AMD Memory', 'AMD:1 Memory'])
|
|
|
|
def test_bytes_per_kernel(self):
|
|
step = 10
|
|
n_events = 1_000
|
|
prof = [ProfileRangeEvent("CPU", name="k_test", st=decimal.Decimal(ts:=i*step), en=decimal.Decimal(ts)+step) for i in range(n_events)]
|
|
sz = len(get_profile(VizData(), prof))
|
|
self.assertLessEqual(sz/n_events, 26)
|
|
|
|
def test_calltrace(self):
|
|
with save_viz() as viz:
|
|
def fxn(): return Tensor.empty(10).mul(2).realize()
|
|
with cpu_profile(TracingKey("test_fxn"), "CUSTOM"):
|
|
fxn()
|
|
codegen_trace = viz.list_items()[0]["steps"][0]["trace"]
|
|
assert any(fxn.__code__.co_filename == f and fxn.__code__.co_firstlineno == l for f,l,*_ in codegen_trace), str(codegen_trace)
|
|
profile_ret = load_profile(cpu_events)
|
|
e = profile_ret["layout"]["CUSTOM"]["events"][0]
|
|
self.assertEqual(e["name"], "test_fxn")
|
|
runtime_trace = e["fmt"]["tb"]
|
|
assert any(fxn.__code__.co_filename == f and fxn.__code__.co_firstlineno+1 == l for f,l,*_ in runtime_trace), str(runtime_trace)
|
|
|
|
# can pack up to 1hr 11 min of trace events
|
|
def test_trace_duration(self):
|
|
dur_mins = 72
|
|
n_events = 1_000
|
|
step = decimal.Decimal(dur_mins*60*1e6//n_events)
|
|
prof = [ProfileRangeEvent("CPU", name="k_test", st=decimal.Decimal(ts:=i*step), en=decimal.Decimal(ts)+step) for i in range(n_events)]
|
|
with self.assertRaisesRegex(ValueError, "timestamp out of range"):
|
|
get_profile(VizData(), prof)
|
|
|
|
def test_python_marker(self):
|
|
with save_viz():
|
|
a = Tensor.empty(1, device="NULL")
|
|
b = Tensor.empty(1, device="NULL")
|
|
(a+b).realize()
|
|
profile_marker("test 1")
|
|
(a*b).realize()
|
|
profile_marker("test 2")
|
|
profile_ret = load_profile(cpu_events)
|
|
markers = profile_ret["markers"]
|
|
kernels = profile_ret["layout"]["NULL"]["events"]
|
|
self.assertEqual(len(markers), 2)
|
|
assert kernels[0]["st"] <= markers[0]["ts"] <= kernels[1]["st"]
|
|
assert markers[1]["ts"] >= kernels[1]["st"]+kernels[1]["dur"]
|
|
|
|
def test_layout_order(self):
|
|
with save_viz():
|
|
def fn(): return
|
|
for dname in ["TINY", "USER", "TEST:1 N1", "TEST:2 N1", "TEST:1 N2", "TEST:1:ENGINE:0", "TEST:1:ENGINE:0 N1", "TEST:1"]:
|
|
with cpu_profile("fn", dname): fn()
|
|
layout = list(load_profile(cpu_events)["layout"])
|
|
self.assertListEqual(layout[:2], ["USER","TINY"])
|
|
self.assertListEqual(layout[2:], ["TEST:1", "TEST:1 N1", "TEST:1 N2", "TEST:1:ENGINE:0", "TEST:1:ENGINE:0 N1", "TEST:2 N1"])
|
|
|
|
def _alloc(b:int):
|
|
a = Tensor.empty(b, device="NULL", dtype=dtypes.char)
|
|
a.uop.buffer.allocate()
|
|
return a
|
|
|
|
class TestVizMemoryLayout(unittest.TestCase):
|
|
def test_double_alloc(self):
|
|
with save_viz():
|
|
a = _alloc(1)
|
|
_b = _alloc(1)
|
|
profile_ret = load_profile(Buffer.profile_events)
|
|
ret = profile_ret["layout"][f"{a.device} Memory"]
|
|
self.assertEqual(ret["peak"], 2)
|
|
self.assertEqual(len(ret["events"]), 4)
|
|
|
|
def test_del_once(self):
|
|
with save_viz():
|
|
a = _alloc(1)
|
|
del a
|
|
b = _alloc(1)
|
|
profile_ret = load_profile(Buffer.profile_events)
|
|
ret = profile_ret["layout"][f"{b.device} Memory"]
|
|
self.assertEqual(ret["peak"], 1)
|
|
self.assertEqual(len(ret["events"]), 4)
|
|
|
|
def test_alloc_free(self):
|
|
with save_viz():
|
|
a = _alloc(1)
|
|
_b = _alloc(1)
|
|
del a
|
|
c = _alloc(1)
|
|
profile_ret = load_profile(Buffer.profile_events)
|
|
ret = profile_ret["layout"][f"{c.device} Memory"]
|
|
self.assertEqual(ret["peak"], 2)
|
|
self.assertEqual(len(ret["events"]), 6)
|
|
|
|
def test_free_last(self):
|
|
with save_viz():
|
|
bufs = []
|
|
for _ in range(3):
|
|
bufs.append(_alloc(1))
|
|
profile_marker("alloc")
|
|
device = bufs[0].device
|
|
while bufs:
|
|
b = bufs.pop()
|
|
del b
|
|
profile_marker("free")
|
|
profile = load_profile(cpu_events+Buffer.profile_events)
|
|
ret = profile["layout"][f"{device} Memory"]
|
|
self.assertEqual(ret["peak"], 3)
|
|
self.assertEqual(len(ret["events"]), 6)
|
|
self.assertEqual(len(profile["markers"]), 6)
|
|
|
|
def test_producer_simple(self):
|
|
with save_viz():
|
|
a = Tensor.ones(10, device="NULL")
|
|
Tensor.realize(a.add(1).contiguous())
|
|
b = Tensor.ones(10, device="NULL")
|
|
Tensor.realize(b.add(1).contiguous())
|
|
profile = load_profile(cpu_events+Buffer.profile_events)
|
|
buffers = profile["layout"]["NULL Memory"]["events"]
|
|
programs = profile["layout"]["NULL"]["events"]
|
|
user_cnt = [len(b["arg"]["users"]) for b in buffers if b["arg"].get("users")]
|
|
self.assertEqual(len(user_cnt), len(programs))
|
|
|
|
@unittest.skip("flaky")
|
|
def test_inflight_buf(self):
|
|
a = Tensor.empty(1, device="NULL")
|
|
n = 4
|
|
for i in range(n): (a+i).realize()
|
|
profile = load_profile(cpu_events+Buffer.profile_events)
|
|
buffers = profile["layout"]["NULL Memory"]["events"]
|
|
user_cnt = [len(b["arg"]["users"]) for b in buffers if b["arg"].get("users")]
|
|
self.assertEqual(max(user_cnt), n)
|
|
input_buf = buffers.pop()
|
|
assert all(u[3] == 0 for u in input_buf["arg"]["users"])
|
|
|
|
def test_annotate_read_write(self):
|
|
with save_viz():
|
|
a = Tensor.ones(4, device="NULL").contiguous().realize()
|
|
b = a.assign(a+2)
|
|
c = a+1
|
|
Tensor.realize(b, c)
|
|
buf_events = load_profile(cpu_events+Buffer.profile_events)["layout"]["NULL Memory"]["events"]
|
|
users = next((b["arg"]["users"] for b in buf_events if len(b["arg"].get("users",[])) == 3))
|
|
self.assertEqual(users[0][3], 1) # write Tensor.ones
|
|
self.assertEqual(users[1][3], 2) # read+write Tensor.assign
|
|
self.assertEqual(users[2][3], 0) # readonly
|
|
|
|
def test_dedup_users(self):
|
|
with save_viz():
|
|
a = Tensor.empty(1, device="NULL")
|
|
for _ in range(n:=4): a.add(1).realize()
|
|
profile = load_profile(cpu_events+Buffer.profile_events)
|
|
programs = profile["layout"][a.device]["events"]
|
|
users = profile["layout"][f"{a.device} Memory"]["events"].pop()["arg"]["users"]
|
|
self.assertEqual(len(programs), len(set(users)), n)
|
|
|
|
from tinygrad.uop.ops import KernelInfo
|
|
from tinygrad.renderer.amd.dsl import s
|
|
from tinygrad.runtime.autogen.amd.rdna3.ins import (s_add_u32, s_branch, s_cbranch_execz, s_cbranch_scc0, s_cbranch_scc1, s_cmp_eq_i32,
|
|
s_cmp_eq_u64, s_code_end, s_endpgm, s_mov_b32, s_nop)
|
|
from extra.gemm.amd_asm_matmul import Kernel
|
|
|
|
@needs_tracked_pm
|
|
class TestCfg(unittest.TestCase):
|
|
def get_cfg(self, name:str, k:Kernel):
|
|
insts = k.finalize()
|
|
def fxn(out:UOp) -> UOp:
|
|
lidx = UOp.special(1, "lidx0")
|
|
gidx = UOp.special(1, "gidx0")
|
|
sink = UOp.sink(out.base, lidx, gidx, arg=KernelInfo(name=name))
|
|
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts]))))
|
|
with save_viz() as viz:
|
|
with Context(DEV="NULL::gfx1100"):
|
|
out = Tensor.custom_kernel(Tensor.empty(1), fxn=fxn)[0]
|
|
_ = do_to_program(out.schedule_linear().src[-1].src[0], Device[out.device].renderer)
|
|
codegen_rewrites = next(s for s in viz.list_items() if s["name"] == name)
|
|
disasm = next(s for s in codegen_rewrites["steps"] if s["name"] == "View Disassembly")
|
|
return get_render(viz.data, disasm["query"])
|
|
|
|
def test_simple(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_branch(), target="bb1")
|
|
k.label("bb1")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
cfg = self.get_cfg("simple", k)["data"]
|
|
self.assertEqual(len(cfg["blocks"]), 2)
|
|
|
|
def test_diamond(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_mov_b32(s[0], 0))
|
|
k.emit(s_mov_b32(s[1], 0))
|
|
k.emit(s_cmp_eq_u64(s[0:1], 0))
|
|
k.emit(s_cbranch_scc1(), target="if")
|
|
k.emit(s_branch(), target="else")
|
|
k.label("if")
|
|
k.emit(s_nop(1))
|
|
k.emit(s_branch(), target="end")
|
|
k.label("else")
|
|
k.emit(s_nop(0))
|
|
k.label("end")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
ret = self.get_cfg("diamond", k)
|
|
cfg = ret["data"]
|
|
self.assertEqual(len(cfg["blocks"]), 5)
|
|
edge_count = sum(len(v) for v in cfg["paths"].values())
|
|
self.assertEqual(edge_count, 5)
|
|
references:dict[str, list[str]] = {}
|
|
for pc, tokens in cfg["pc_tokens"].items():
|
|
for t in tokens:
|
|
for key in t["keys"]: references.setdefault(key, []).append(pc)
|
|
self.assertEqual(len(references["r0"]), 2)
|
|
insts = [cfg["pc_tokens"][pc][0]["st"] for pc in references["r0"]]
|
|
self.assertEqual(insts, ['s_mov_b32', 's_cmp_eq_u64'])
|
|
end_block = [" ".join(t["st"] for t in cfg["pc_tokens"][pc]) for pc in list(cfg["blocks"].values())[-1]]
|
|
code_line = ret["src"].splitlines()[-1]
|
|
self.assertEqual(len(end_block), 2)
|
|
for st in [end_block[-1], code_line]:
|
|
assert st.startswith("s_code_end") and st.endswith("x)"), st
|
|
|
|
def test_loop(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_mov_b32(s[1], 4))
|
|
k.label("loop")
|
|
k.emit(s_add_u32(s[1], s[1], -1))
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_cbranch_scc0(), target="loop")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("simple_loop", k)
|
|
|
|
def test_loop_branch(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_mov_b32(s[1], 4))
|
|
k.label("loop")
|
|
k.emit(s_add_u32(s[1], s[1], -1))
|
|
k.emit(s_cmp_eq_i32(s[1], 2))
|
|
k.emit(s_cbranch_scc1(), target="cond")
|
|
k.emit(s_branch(), target="cont")
|
|
k.label("cond")
|
|
k.emit(s_add_u32(s[1], s[1], -2))
|
|
k.label("cont")
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_cbranch_scc0(), target="loop")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("loop_if", k)
|
|
|
|
def test_loop_break(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_mov_b32(s[1], 8))
|
|
k.label("loop")
|
|
k.emit(s_add_u32(s[1], s[1], -1))
|
|
k.emit(s_cmp_eq_i32(s[1], 5))
|
|
k.emit(s_cbranch_scc1(), target="break")
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_cbranch_scc0(), target="loop")
|
|
k.label("break")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("loop_break", k)
|
|
|
|
def test_switch(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_cmp_eq_i32(s[0], 0))
|
|
k.emit(s_cbranch_scc1(), target="case0")
|
|
k.emit(s_cmp_eq_i32(s[0], 1))
|
|
k.emit(s_cbranch_scc1(), target="case1")
|
|
k.emit(s_branch(), target="case2")
|
|
k.label("case0")
|
|
k.emit(s_nop(0))
|
|
k.emit(s_branch(), target="join")
|
|
k.label("case1")
|
|
k.emit(s_nop(1))
|
|
k.emit(s_branch(), target="join")
|
|
k.label("case2")
|
|
k.emit(s_nop(2))
|
|
k.emit(s_branch(), target="join")
|
|
k.label("join")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("switch_case", k)
|
|
|
|
def test_ping_pong(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_cmp_eq_i32(s[0], 0))
|
|
k.emit(s_cbranch_scc1(), target="ping")
|
|
k.emit(s_branch(), target="pong")
|
|
k.label("ping")
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_cbranch_scc1(), target="pong")
|
|
k.emit(s_branch(), target="end")
|
|
k.label("pong")
|
|
k.emit(s_cmp_eq_i32(s[2], 0))
|
|
k.emit(s_cbranch_scc1(), target="ping")
|
|
k.label("end")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("ping_pong", k)
|
|
|
|
def test_colored_blocks(self):
|
|
N = 10
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_branch(), target="init0")
|
|
for i in range(N):
|
|
loop = f"loop{i}"
|
|
k.label(f"init{i}")
|
|
k.emit(s_mov_b32(s[1], i + 1))
|
|
k.emit(s_branch(), target=loop)
|
|
k.label(loop)
|
|
k.emit(s_nop(i & 7))
|
|
k.emit(s_add_u32(s[1], s[1], -1))
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_cbranch_scc0(), target=loop)
|
|
k.emit(s_branch(), target=f"init{i+1}" if i + 1 < N else "end")
|
|
k.label("end")
|
|
k.emit(s_endpgm())
|
|
k.emit(s_code_end())
|
|
self.get_cfg("test_colored_blocks", k)
|
|
|
|
def test_jump_back_to_end(self):
|
|
k = Kernel()
|
|
k.label("entry")
|
|
k.emit(s_mov_b32(s[1], 2))
|
|
k.emit(s_cbranch_execz(), target="loop")
|
|
k.label("end")
|
|
k.emit(s_endpgm())
|
|
k.label("loop")
|
|
k.emit(s_add_u32(s[1], s[1], -1))
|
|
k.emit(s_cmp_eq_i32(s[1], 0))
|
|
k.emit(s_branch(), target="end")
|
|
k.emit(s_code_end())
|
|
self.get_cfg("jump_back_to_end", k)
|
|
|
|
# launch viz cli without subprocess
|
|
def run_cli(*cli_args) -> list[dict]:
|
|
from tinygrad.viz.cli import main, get_arg_parser
|
|
args = get_arg_parser().parse_args(cli_args+("--json",))
|
|
with contextlib.redirect_stdout(buf:=io.StringIO()):
|
|
main(args)
|
|
return [json.loads(line) for line in buf.getvalue().strip().splitlines()]
|
|
|
|
@contextlib.contextmanager
|
|
def write_files(viz) -> list[str]:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
(r:=pathlib.Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
|
(p:=pathlib.Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
|
yield ["--rewrites-path", str(r), "--profile-path", str(p)]
|
|
|
|
class TestCLI(unittest.TestCase):
|
|
@needs_tracked_pm
|
|
def test_reconstruct_debug(self):
|
|
with save_viz() as viz:
|
|
Tensor.empty(1, device="NULL").add(2.0).realize()
|
|
profile_marker("marker @ 1")
|
|
Tensor.empty(1, device="NULL").add(3.0).realize()
|
|
with write_files(viz) as files, Context(DEBUG=4):
|
|
out = run_cli(*files, "-s", "NULL")
|
|
assert any(s.get("value", "").startswith("void E") for s in out)
|
|
assert any(s.get("name", "") == "marker @ 1" for s in out)
|
|
|
|
def test_aggregate(self):
|
|
N, CNT = 1024, 5
|
|
with save_viz() as viz:
|
|
for _ in range(CNT):
|
|
(Tensor.empty(N, N, device="NULL")@Tensor.empty(N, N, device="NULL")).realize()
|
|
for _ in range(CNT):
|
|
(Tensor.empty(N, N, device="NULL").assign(Tensor.empty(N, N, device="NULL"))).realize()
|
|
with write_files(viz) as files, Context(NO_COLOR=1):
|
|
kernels = run_cli(*files, "-s", "NULL", "-t")
|
|
self.assertEqual(len(kernels), 2)
|
|
gemm_summary = [s for s in kernels if s["name"].startswith("r_")][0]
|
|
copy_summary = [s for s in kernels if s["name"].startswith("E_")][0]
|
|
self.assertEqual(gemm_summary["count"], CNT)
|
|
self.assertEqual(copy_summary["count"], CNT)
|
|
|
|
def test_flops(self):
|
|
test_n = [(8, 16), (16, 32), (32, 64)]
|
|
with save_viz() as viz:
|
|
@TinyJit
|
|
def f(a, b): return (a@a.T), (b@b.T)
|
|
a = Tensor.empty(64, 64, device="NULL")
|
|
b = Tensor.empty(64, 64, device="NULL")
|
|
for i_val, j_val in test_n:
|
|
i = Variable("i", 1, 64).bind(i_val)
|
|
j = Variable("j", 1, 64).bind(j_val)
|
|
Tensor.realize(*f(a[:i], b[:j]))
|
|
with write_files(viz) as files:
|
|
out = run_cli(*files, "-s", "NULL")
|
|
aggregate = run_cli(*files, "-s", "NULL", "-t")
|
|
self.assertEqual(len(out), 3*2)
|
|
# flops increases as N gets larger
|
|
gflops = [row["fmt"]["FLOPS"] for row in out]
|
|
self.assertGreater(gflops[4], gflops[2])
|
|
self.assertGreater(gflops[5], gflops[3])
|
|
# aggregate flops
|
|
self.assertEqual(len(aggregate), 2)
|
|
agg_gflops = [row["fmt"]["FLOPS"] for row in aggregate]
|
|
assert all(min(gflops) < v < max(gflops) for v in agg_gflops), f"{agg_gflops}"
|
|
|
|
def test_dedup(self):
|
|
with save_viz() as viz:
|
|
for _ in range(CNT:=4):
|
|
# use kernel names unique to this test
|
|
Tensor.custom_kernel(Tensor.empty(4, device="NULL"), fxn=lambda _: UOp.sink(arg=KernelInfo("k1_test_viz_dedup")))[0].realize()
|
|
Tensor.custom_kernel(Tensor.empty(8, device="NULL"), fxn=lambda _: UOp.sink(arg=KernelInfo("k2_test_viz_dedup")))[0].realize()
|
|
with write_files(viz) as files, Context(NO_COLOR=1):
|
|
name = run_cli(*files, "-s", "NULL")[0]["name"]
|
|
with Context(DEBUG=3):
|
|
select = run_cli(*files, "-s", "NULL", name)
|
|
self.assertEqual(len([s for s in select if s.get("value")]), 1, "debug output was not deduped")
|
|
self.assertEqual(len([s for s in select if s.get("device") == "NULL"]), CNT, f"expected 4 runs for {name}")
|
|
|
|
@needs_tracked_pm
|
|
def test_call_graph(self):
|
|
@function(precompile=True)
|
|
def f(x):
|
|
r = x.sum(axis=1).reshape(32, 1).expand(32, 32).contiguous()
|
|
return x + r
|
|
# turn off scache because this test requires a complete schedule rewrite
|
|
with save_viz() as viz, Context(SCACHE=0):
|
|
f(f(Tensor.empty(32, 32, device="NULL"))).realize()
|
|
with write_files(viz) as files, Context(NO_COLOR=1):
|
|
prgs = [s["name"] for s in run_cli(*files, "-s", "NULL")]
|
|
with Context(DEBUG=5):
|
|
out = run_cli(*files, "-s", "TINY")
|
|
i = next(i for i,s in enumerate(out) if s.get("value", "").lstrip() == "View Kernel Graph")
|
|
# next print is the CALL graph, CLI outputs exactly as web in TestVizIntegration.test_link_sched_codegen
|
|
call_nodes = [n for n in out[i+1].values() if n["label"].startswith("CALL")]
|
|
for i,n in enumerate(call_nodes):
|
|
assert prgs[i] in n["label"], f"CALL must contain kernel name, got {n['label']}"
|
|
|
|
def test_interval(self):
|
|
def emit_kernel(name:str): Tensor.custom_kernel(Tensor.empty(1, device="NULL"), fxn=lambda _: UOp.sink(arg=KernelInfo(name=name)))[0].realize()
|
|
with save_viz() as viz:
|
|
emit_kernel("pre_1")
|
|
emit_kernel("pre_2")
|
|
profile_marker("interval_start")
|
|
emit_kernel("target_1")
|
|
emit_kernel("target_2")
|
|
profile_marker("interval_end")
|
|
emit_kernel("post_1")
|
|
emit_kernel("post_2")
|
|
with write_files(viz) as files, Context(NO_COLOR=1):
|
|
flat = run_cli(*files, "-s", "NULL", "--interval", "interval_start", "interval_end")
|
|
aggregate = run_cli(*files, "-s", "NULL", "--interval", "interval_start", "interval_end", "-t")
|
|
final = run_cli(*files, "-s", "NULL", "--interval", "interval_end", "-t")
|
|
self.assertEqual([s["name"] for s in flat], ["interval_start", "target_1", "target_2", "interval_end"])
|
|
self.assertEqual(sorted(s["name"] for s in aggregate), ["target_1", "target_2"])
|
|
assert all(s["name"].startswith("post_") for s in final), f"post_* kernels must be present in final, got {final}"
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|