forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ ab07000
This commit is contained in:
61
tinygrad_repo/tinygrad/viz/README
Normal file
61
tinygrad_repo/tinygrad/viz/README
Normal file
@@ -0,0 +1,61 @@
|
||||
VIZ is a tool for inspecting tinygrad's compilation process and performance profiling.
|
||||
|
||||
to use:
|
||||
1. Run tinygrad with VIZ=1 (this saves the pkls and launches the server in interactive shells)
|
||||
2. That's it!
|
||||
|
||||
This can:
|
||||
1. See all schedules
|
||||
2. See all graphs and how they were rewritten
|
||||
3. See generated code
|
||||
4. See profile
|
||||
5. (AMD only) see instruction level SQTT profile
|
||||
|
||||
VIZ pkls can be viewed in two ways:
|
||||
|
||||
1. Web browser: python -m tinygrad.viz.serve
|
||||
2. Command line: python -m tinygrad.viz.cli
|
||||
|
||||
By default, VIZ UIs automatically load the latest files.
|
||||
|
||||
user story: viewing profiling data
|
||||
* tinygrad ran 32 LLM decode steps:
|
||||
web: click "profiler", view the timeline of all python codegen and GPU kernels.
|
||||
cli: Run `DEBUG=3 python -m tinygrad.viz.cli --json` to extract kernel timing info and ASTs in JSON format.
|
||||
- note: Make sure to add NO_COLOR=1 to disable colored output.
|
||||
|
||||
user story: viewing code
|
||||
* tinygrad ran 3 schedules: init the model + first train step, train step, test step
|
||||
* schedule 1 (123) = main.py:97
|
||||
* schedule 2 (97) = main.py:97
|
||||
* schedule 3 (10) = main.py:145
|
||||
* web: click "schedule 1", get list of kernels (like DEBUG=2)
|
||||
* cli: `python -m tinygrad.viz.cli -s TINY "Schedule 3 Kernels n1"`
|
||||
* kernel 1 "E_34_34" -- 'sin'
|
||||
* kernel 2 "R_4545"
|
||||
* web: click "E_34_34"
|
||||
* cli: `python -m tinygrad.viz.cli -s TINY "do_to_program for E_34_34" "initial symbolic"`
|
||||
* pre-rewritten UOp graph (step through rewrite here)
|
||||
* post-rewritten UOp graph
|
||||
* UOp list
|
||||
* generated code
|
||||
|
||||
user story: debugging scheduler
|
||||
* tinygrad ran 3 schedules: init the model + first train step, train step, test step
|
||||
* ...
|
||||
* click "schedule 1 graph", get a graph of the schedule in UOps
|
||||
* step through rewrite rules
|
||||
* see how things are broken into kernels
|
||||
* see why two kernels didn't fuse
|
||||
|
||||
user story: SQTT / PMC profiling
|
||||
|
||||
note: SQTT has additional overhead, to enable it, set VIZ=2.
|
||||
|
||||
* tinygrad ran custom assembly GEMM kernel.
|
||||
* web: click "SQTT gemm SE:1 PKTS", see wave instruction scheduling and CU execution unit occupancy at every clock cycle.
|
||||
* cli: python -m tinygrad.viz.cli -s "kernel SQTT SE:0 PKTS"
|
||||
|
||||
* get bank conflicts:
|
||||
* web: click "gemm PMC"
|
||||
* cli: python -m tinygrad.viz.cli -s "gemm PMC" | rg -A 16 SQC_LDS_BANK_CONFLICT
|
||||
0
tinygrad_repo/tinygrad/viz/__init__.py
Normal file
0
tinygrad_repo/tinygrad/viz/__init__.py
Normal file
1232
tinygrad_repo/tinygrad/viz/assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js
vendored
Normal file
1232
tinygrad_repo/tinygrad/viz/assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
||||
/*! `cpp` grammar compiled for Highlight.js 11.10.0 */
|
||||
(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,a=e.COMMENT("//","$",{
|
||||
contains:[{begin:/\\\n/}]
|
||||
}),n="decltype\\(auto\\)",r="[a-zA-Z_]\\w*::",i="(?!struct)("+n+"|"+t.optional(r)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",s={
|
||||
className:"type",begin:"\\b[a-z\\d_]*_t\\b"},c={className:"string",variants:[{
|
||||
begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
|
||||
begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
|
||||
end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
|
||||
begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
|
||||
className:"number",variants:[{
|
||||
begin:"[+-]?(?:(?:[0-9](?:'?[0-9])*\\.(?:[0-9](?:'?[0-9])*)?|\\.[0-9](?:'?[0-9])*)(?:[Ee][+-]?[0-9](?:'?[0-9])*)?|[0-9](?:'?[0-9])*[Ee][+-]?[0-9](?:'?[0-9])*|0[Xx](?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*(?:\\.(?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)?)?|\\.[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)[Pp][+-]?[0-9](?:'?[0-9])*)(?:[Ff](?:16|32|64|128)?|(BF|bf)16|[Ll]|)"
|
||||
},{
|
||||
begin:"[+-]?\\b(?:0[Bb][01](?:'?[01])*|0[Xx][0-9A-Fa-f](?:'?[0-9A-Fa-f])*|0(?:'?[0-7])*|[1-9](?:'?[0-9])*)(?:[Uu](?:LL?|ll?)|[Uu][Zz]?|(?:LL?|ll?)[Uu]?|[Zz][Uu]|)"
|
||||
}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
|
||||
keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
|
||||
},contains:[{begin:/\\\n/,relevance:0},e.inherit(c,{className:"string"}),{
|
||||
className:"string",begin:/<.*?>/},a,e.C_BLOCK_COMMENT_MODE]},u={
|
||||
className:"title",begin:t.optional(r)+e.IDENT_RE,relevance:0
|
||||
},d=t.optional(r)+e.IDENT_RE+"\\s*\\(",p={
|
||||
type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"],
|
||||
keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"],
|
||||
literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"],
|
||||
_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"]
|
||||
},_={className:"function.dispatch",relevance:0,keywords:{
|
||||
_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"]
|
||||
},
|
||||
begin:t.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,t.lookahead(/(<[^<>]+>|)\s*\(/))
|
||||
},m=[_,l,s,a,e.C_BLOCK_COMMENT_MODE,o,c],f={variants:[{begin:/=/,end:/;/},{
|
||||
begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
|
||||
keywords:p,contains:m.concat([{begin:/\(/,end:/\)/,keywords:p,
|
||||
contains:m.concat(["self"]),relevance:0}]),relevance:0},g={className:"function",
|
||||
begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
|
||||
keywords:p,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:n,keywords:p,relevance:0},{
|
||||
begin:d,returnBegin:!0,contains:[u],relevance:0},{begin:/::/,relevance:0},{
|
||||
begin:/:/,endsWithParent:!0,contains:[c,o]},{relevance:0,match:/,/},{
|
||||
className:"params",begin:/\(/,end:/\)/,keywords:p,relevance:0,
|
||||
contains:[a,e.C_BLOCK_COMMENT_MODE,c,o,s,{begin:/\(/,end:/\)/,keywords:p,
|
||||
relevance:0,contains:["self",a,e.C_BLOCK_COMMENT_MODE,c,o,s]}]
|
||||
},s,a,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++",
|
||||
aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:p,illegal:"</",
|
||||
classNameAliases:{"function.dispatch":"built_in"},
|
||||
contains:[].concat(f,g,_,m,[l,{
|
||||
begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array|tuple|optional|variant|function)\\s*<(?!<)",
|
||||
end:">",keywords:p,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:p},{
|
||||
match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/],
|
||||
className:{1:"keyword",3:"title.class"}}])}}})();hljs.registerLanguage("cpp",e)
|
||||
})();
|
||||
@@ -0,0 +1,42 @@
|
||||
/*! `python` grammar compiled for Highlight.js 11.10.0 */
|
||||
(()=>{var e=(()=>{"use strict";return e=>{
|
||||
const n=e.regex,a=/[\p{XID_Start}_]\p{XID_Continue}*/u,s=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],t={
|
||||
$pattern:/[A-Za-z]\w+|__\w+__/,keyword:s,
|
||||
built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],
|
||||
literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],
|
||||
type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]
|
||||
},i={className:"meta",begin:/^(>>>|\.\.\.) /},r={className:"subst",begin:/\{/,
|
||||
end:/\}/,keywords:t,illegal:/#/},l={begin:/\{\{/,relevance:0},o={
|
||||
className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{
|
||||
begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,
|
||||
contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{
|
||||
begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,
|
||||
contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{
|
||||
begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,
|
||||
contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,
|
||||
end:/"""/,contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([uU]|[rR])'/,end:/'/,
|
||||
relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{
|
||||
begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,
|
||||
end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,
|
||||
contains:[e.BACKSLASH_ESCAPE,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,
|
||||
contains:[e.BACKSLASH_ESCAPE,l,r]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
|
||||
},b="[0-9](_?[0-9])*",c=`(\\b(${b}))?\\.(${b})|\\b(${b})\\.`,d="\\b|"+s.join("|"),g={
|
||||
className:"number",relevance:0,variants:[{
|
||||
begin:`(\\b(${b})|(${c}))[eE][+-]?(${b})[jJ]?(?=${d})`},{begin:`(${c})[jJ]?`},{
|
||||
begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${d})`},{
|
||||
begin:`\\b0[bB](_?[01])+[lL]?(?=${d})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${d})`
|
||||
},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${d})`},{begin:`\\b(${b})[jJ](?=${d})`
|
||||
}]},p={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:t,
|
||||
contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={
|
||||
className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,
|
||||
end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,
|
||||
contains:["self",i,g,o,e.HASH_COMMENT_MODE]}]};return r.contains=[o,g,i],{
|
||||
name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:t,
|
||||
illegal:/(<\/|\?)|=>/,contains:[i,g,{scope:"variable.language",match:/\bself\b/
|
||||
},{beginKeywords:"if",relevance:0},{match:/\bor\b/,scope:"keyword"
|
||||
},o,p,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,a],scope:{1:"keyword",
|
||||
3:"title.function"},contains:[m]},{variants:[{
|
||||
match:[/\bclass/,/\s+/,a,/\s*/,/\(\s*/,a,/\s*\)/]},{match:[/\bclass/,/\s+/,a]}],
|
||||
scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{
|
||||
className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[g,m,o]}]}}})()
|
||||
;hljs.registerLanguage("python",e)})();
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
Theme: Default
|
||||
Description: Original highlight.js style
|
||||
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||
Maintainer: @highlightjs/core-team
|
||||
Website: https://highlightjs.org/
|
||||
License: see project LICENSE
|
||||
Touched: 2021
|
||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
2
tinygrad_repo/tinygrad/viz/assets/d3js.org/d3.v7.min.js
vendored
Normal file
2
tinygrad_repo/tinygrad/viz/assets/d3js.org/d3.v7.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
tinygrad_repo/tinygrad/viz/assets/dagrejs.github.io/project/dagre/latest/dagre.min.js
vendored
Normal file
3
tinygrad_repo/tinygrad/viz/assets/dagrejs.github.io/project/dagre/latest/dagre.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: Tokyo-night-Dark
|
||||
origin: https://github.com/enkia/tokyo-night-vscode-theme
|
||||
Description: Original highlight.js style
|
||||
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
|
||||
License: see project LICENSE
|
||||
Touched: 2022
|
||||
*/.hljs-comment,.hljs-meta{color:#565f89}.hljs-deletion,.hljs-doctag,.hljs-regexp,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-tag,.hljs-template-tag,.hljs-variable.language_{color:#f7768e}.hljs-link,.hljs-literal,.hljs-number,.hljs-params,.hljs-template-variable,.hljs-type,.hljs-variable{color:#ff9e64}.hljs-attribute,.hljs-built_in{color:#e0af68}.hljs-keyword,.hljs-property,.hljs-subst,.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#7dcfff}.hljs-selector-tag{color:#73daca}.hljs-addition,.hljs-bullet,.hljs-quote,.hljs-string,.hljs-symbol{color:#9ece6a}.hljs-code,.hljs-formula,.hljs-section{color:#7aa2f7}.hljs-attr,.hljs-char.escape_,.hljs-keyword,.hljs-name,.hljs-operator{color:#bb9af7}.hljs-punctuation{color:#c0caf5}.hljs{background:#1a1b26;color:#9aa5ce}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
231
tinygrad_repo/tinygrad/viz/cli.py
Executable file
231
tinygrad_repo/tinygrad/viz/cli.py
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse, pathlib, signal, struct, json, os, itertools, heapq
|
||||
os.environ["VIZ"] = "0"
|
||||
if hasattr(signal, "SIGPIPE"): signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
from typing import Iterator
|
||||
from tinygrad.viz import serve as viz
|
||||
from tinygrad.viz.serve import fmt_colored
|
||||
from tinygrad.uop.ops import RewriteTrace
|
||||
from tinygrad.helpers import temp, ansistrip, colored, time_to_str, ansilen, ProfilePointEvent, ProfileRangeEvent, TracingKey, unwrap, NO_COLOR, DEBUG
|
||||
|
||||
# profile decoder used in CLI and tests
|
||||
def decode_profile(data:bytes) -> dict:
|
||||
ret, off = data, 0
|
||||
def u(fmt:str) -> tuple:
|
||||
nonlocal off
|
||||
vals = struct.unpack_from(fmt, ret, off)
|
||||
off += struct.calcsize(fmt)
|
||||
return vals
|
||||
total_dur, global_peak, index_len, layout_len = u("<IQII")
|
||||
strings, dtypes, markers = json.loads(ret[off:off+index_len]).values()
|
||||
off += index_len
|
||||
layout:dict[str, dict] = {}
|
||||
# 0 means None, otherwise it's an enum value
|
||||
def option(i:int) -> int|None: return None if i == 0 else i-1
|
||||
for _ in range(layout_len):
|
||||
klen = u("<B")[0]
|
||||
k = ret[off:off+klen].decode()
|
||||
off += klen
|
||||
event_type, event_count = u("<BI")
|
||||
layout[k] = v = {"event_type":event_type, "events":[]}
|
||||
if event_type == 0:
|
||||
for _ in range(event_count):
|
||||
name, ref, key, st, dur, fmt = u("<IIIIfI")
|
||||
v["events"].append({"name":strings[name], "ref":option(ref), "key":option(key), "st":st, "dur":dur, "fmt":json.loads(strings[fmt])})
|
||||
else:
|
||||
v["linear"] = u("<B")[0]
|
||||
v["peak"] = u("<Q")[0]
|
||||
for _ in range(event_count):
|
||||
if v["linear"]:
|
||||
ts, value = u("<IQ")
|
||||
v["events"].append({"event":"freq", "ts":ts, "value":value})
|
||||
else:
|
||||
alloc, ts, key = u("<BII")
|
||||
if alloc: v["events"].append({"event":"alloc", "ts":ts, "key":key, "arg": {"dtype":strings[u("<I")[0]], "sz":u("<Q")[0]}})
|
||||
else: v["events"].append({"event":"free", "ts":ts, "key":key, "arg": {"users":[(k, strings[rep], num, mode) \
|
||||
for k,rep,num,mode in [u("<IIIB") for _ in range(u("<I")[0])]]}})
|
||||
return {"dur":total_dur, "peak":global_peak, "layout":layout, "markers":markers}
|
||||
|
||||
def to_str(k:str, v) -> str:
|
||||
if k == "FLOPS" or k.startswith("B/s"): return f"{v*1e-9:.0f} G{k}" if v < 1e13 else f"{v*1e-12:.0f} T{k}"
|
||||
if k == "B": return next((f"{v/s:.0f} {u}" for s,u in ((1e9,"GB"),(1e6,"MB"),(1e3,"KB")) if v>=s), f"{v:.0f} B")
|
||||
return f"{k}={v}"
|
||||
def fmt_data(data:dict) -> str: return " ".join((p:=to_str(k, v))+" "*max(0, 14-ansilen(p)) for k,v in data.items())
|
||||
|
||||
def marker_st(markers:list[dict], name:str) -> int:
|
||||
try: return next(e["ts"] for e in markers if e["name"] == name)
|
||||
except StopIteration: raise RuntimeError(f"marker not found: {name}") from None
|
||||
|
||||
def get(data:dict, key:str):
|
||||
for k,v in data.items():
|
||||
if ansistrip(k) == key: return v
|
||||
import difflib
|
||||
match = difflib.get_close_matches(key, [ansistrip(k) for k in data], n=1, cutoff=0.6)
|
||||
raise RuntimeError(f'item "{key}" not found in list'+(f", did you mean {match[0]!r}?" if match else ''))
|
||||
|
||||
def main(args) -> None:
|
||||
viz.load_rewrites(viz_data:=viz.VizData(viz.load_pickle(args.rewrites_path, default=RewriteTrace([], [], {}))))
|
||||
|
||||
def emit(val, to_str=str) -> str: return json.dumps(val if isinstance(val, dict) else {"value":val}) if args.json else to_str(val)
|
||||
|
||||
def print_step(step:dict, print_graph=False, reconstruct_matches=False) -> None:
|
||||
data = viz.get_render(viz_data, step["query"])
|
||||
if isinstance(data.get("value"), Iterator):
|
||||
for m in data["value"]:
|
||||
if print_graph and "graph" in m and not args.json:
|
||||
for k,v in m["graph"].items():
|
||||
print(f"[{k}] {' '.join((lines:=v['label'].splitlines())[:5])}{'...' if len(lines) > 5 else ''}"+(f" tag={v['tag']}" if v['tag'] else ''))
|
||||
if v["src"]:
|
||||
print(" src: "+", ".join([f"{i}->[{x}]" for i,x in v["src"][:5]])+(f", ... and {len(v['src'])-5} more" if len(v["src"]) > 5 else ""))
|
||||
elif "uop" in m: print(emit(m["graph"] if print_graph else m["uop"]))
|
||||
if not reconstruct_matches: return None
|
||||
if m.get("diff"):
|
||||
loc = pathlib.Path(m["upat"][0][0])
|
||||
print(emit(f"{loc.parent.name}/{loc.name}:{m['upat'][0][1]}\n{m['upat'][1]}"))
|
||||
for line in m["diff"]: print(emit(colored(line, "red" if line.startswith("-") else "green" if line.startswith("+") else None)))
|
||||
if data.get("src") is not None: print(emit(data["src"]))
|
||||
|
||||
profile_bytes = viz.get_profile(viz_data, viz.load_pickle(args.profile_path, default=[]))
|
||||
if profile_bytes is None: raise RuntimeError(f"empty profile in {args.profile_path}")
|
||||
profile = decode_profile(profile_bytes)
|
||||
profile["layout"].update([(f'{c["name"][5:]}{" SQTT" if s["name"].endswith("PKTS") else ""} {s["name"]}', s["data"]) for c in viz_data.ctxs
|
||||
if c["name"].startswith("SQTT") for s in c["steps"] if s["name"].endswith(("PMC", "PKTS"))])
|
||||
if args.list and not args.src: return print("\n".join(emit(fmt_colored(k)) for k in ["ALL"]+list(profile["layout"])))
|
||||
|
||||
# ** SQTT printer
|
||||
data = None if not args.src else get(profile["layout"], args.src[0])
|
||||
if args.src and "SQTT" in args.src[0]:
|
||||
# modern terminals support 24-bit color
|
||||
def hex_colored(st:str, color:str) -> str: return f"\x1b[38;2;{int(color[1:3],16)};{int(color[3:5],16)};{int(color[5:7],16)}m{st}\x1b[0m"
|
||||
print(emit(f"{'Clk':<12} {'Unit':<20} {'Op':<22} {'Dur':<4} {'Delay':<4} {'Info'}"))
|
||||
print(emit("-" * 100))
|
||||
pc_map:dict[int, str] = {}
|
||||
pkt_idxs:dict[str, itertools.count] = {}
|
||||
dispatch_to_inst:dict[str, tuple[str, int]] = {}
|
||||
inst_st:int|None = None
|
||||
for e in viz.sqtt_timeline(*unwrap(data)):
|
||||
if isinstance(e, ProfilePointEvent) and e.key == 'pcMap': pc_map = e.arg
|
||||
if not isinstance(e, ProfileRangeEvent): continue
|
||||
if inst_st is None: inst_st = int(e.st)
|
||||
assert isinstance(e.name, TracingKey)
|
||||
op_name, ret, info = e.name.display_name, json.loads(e.name.ret[4:]) if e.name.ret else {}, ""
|
||||
color = next((v for k,v in viz.wave_colors.items() if k in op_name), None)
|
||||
op_str = hex_colored(op_name, color) if color and not NO_COLOR else op_name
|
||||
inst, phase, delay = None, None, 0
|
||||
idx = next(pkt_idxs.setdefault(e.device, itertools.count()))
|
||||
if e.device.startswith("WAVE"):
|
||||
inst = f"0x{pc:05x} {pc_map[pc]}" if (pc:=ret.get("pc")) is not None else f"{'':7} {op_name}"
|
||||
dispatch_to_inst[f"{e.device}-{idx}"] = (inst, int(e.st))
|
||||
phase = "DISPATCH"
|
||||
if (link:=ret.get("link")) is not None:
|
||||
inst, dispatch_st = dispatch_to_inst[link]
|
||||
phase, delay = "EXEC", int(e.st) - dispatch_st
|
||||
if inst and phase: info = f"{phase:<8} {inst}"
|
||||
unit = e.device.replace(" ", "-")
|
||||
row = {"clk":int(e.st)-inst_st, "cycle":int(e.st), "unit":unit, "op":op_name, "dur":int(unwrap(e.en)-e.st), "delay":delay or "", "info":info}
|
||||
print(emit(row, lambda _: f"{row['clk']:<12} {unit:<20} {op_str}{' '*(22-ansilen(op_str))} {row['dur']:<4} {str(row['delay']):<4} {info}"))
|
||||
|
||||
# ** PMC printer
|
||||
elif args.src and "PMC" in args.src[0]:
|
||||
pmc = viz.unpack_pmc(unwrap(data))
|
||||
pmc_fmt:list[str] = []
|
||||
for name,val,*detail in pmc["rows"]:
|
||||
pmc_fmt += [f"{name} {val}"]+([" ".join(f"{k}={v}" for k,v in zip(detail[0]["cols"], r)) for r in detail[0]["rows"]] if detail else [])
|
||||
print(emit(pmc, lambda _: "\n".join(pmc_fmt)))
|
||||
|
||||
# ** Memory printer
|
||||
elif data is not None and data["event_type"] == 1:
|
||||
print(emit({"peak":data["peak"]}, lambda _: f"Peak: {data['peak']}"+"\n"+f"{'TS':<10} {'Event':<6} {'Key':>8} Info"))
|
||||
for e in data["events"]:
|
||||
info = str(arg:=e.pop("arg", {}))
|
||||
if e["event"] == "free": info = ', '.join([f"{fmt_colored(k)} {['read','write','write+read'][m]}@data{n}" for _,k,n,m in arg["users"]])
|
||||
print(emit({**e, "info":info}, lambda _: f"{e['ts']:<10} {e['event']:<6} {e.get('key', ''):>8} {info}"))
|
||||
|
||||
# ** Profiler printer
|
||||
else:
|
||||
timelines = [(n,l) for n,l in profile["layout"].items() if isinstance(l, dict) and l.get("event_type") == 0]
|
||||
markers = profile.get("markers", [])
|
||||
interval:tuple[int, int]|None = None if not args.interval else (marker_st(markers, args.interval[0]), marker_st(markers, args.interval[1]))
|
||||
def produce_top_kernels() -> Iterator[dict]:
|
||||
tagged = ((n,e) for n,l in timelines for e in l["events"]) if not args.src else ((args.src[0],e) for e in unwrap(data)["events"])
|
||||
agg:dict[tuple[str,str], tuple[float, int, int|None, dict[str, float]]] = {} # map (device, kernel name) to (total time, count, ref, est)
|
||||
est_keys = ("FLOPS", "B/s mem", "B/s lds")
|
||||
total = 0
|
||||
for dev,e in tagged:
|
||||
if interval and not interval[0] <= e["st"] <= interval[1]: continue
|
||||
et = e["dur"] * 1e-3
|
||||
t, c, ref, est = agg.get((dev,e["name"]), (0.0, 0, None, {}))
|
||||
est.update({k:est.get(k, 0.0)+e["fmt"][k]*e["dur"]*1e-6 for k in est_keys if k in e["fmt"]})
|
||||
agg[(dev,e["name"])] = (t+et, c+1, e["ref"], est)
|
||||
total += et
|
||||
items = sorted(agg.items(), key=lambda kv:kv[1][0], reverse=True)
|
||||
num_rows = len(items) if args.t < 0 else args.t
|
||||
for (dev,name),(t,c,ref,est) in items[:num_rows]:
|
||||
display = f"{dev[:7]:7s} {fmt_colored(name)}" if not args.src else fmt_colored(name)
|
||||
yield {"name":display, "dur_ms":t, "count":c, "pct":t/total*100.0, "ref":ref, "fmt":{k:int(est[k]/(t*1e-3)) for k in est_keys if k in est}}
|
||||
if num_rows > 0 and items[num_rows:]:
|
||||
other_t = sum(t for _,(t,_,_,_) in items[num_rows:])
|
||||
other_c = sum(c for _,(_,c,_,_) in items[num_rows:])
|
||||
yield {"name":"Other", "dur_ms":other_t, "count":other_c, "pct":other_t/total*100.0, "ref":None, "fmt":None}
|
||||
def produce_all_kernels() -> Iterator[dict]:
|
||||
event_streams = [[(e["st"], n, e) for e in l["events"]] for n,l in timelines] if not args.src \
|
||||
else [[(e["st"], args.src[0], e) for e in unwrap(data)["events"]]]
|
||||
if not args.src:
|
||||
for n,l in profile["layout"].items():
|
||||
if not isinstance(l, dict) or l.get("event_type") != 0: yield {"device":"SOURCE", "name":n, "st_ms":0, "ref":None, "ext":None}
|
||||
marker_stream = sorted([(m["ts"], "MARKER", m) for m in markers], key=lambda t:t[0])
|
||||
for ts,dev,e in heapq.merge(*event_streams, marker_stream, key=lambda t:t[0]):
|
||||
if interval is not None and not interval[0] <= ts <= interval[1]: continue
|
||||
if dev == "MARKER":
|
||||
yield {"device":dev, "name":fmt_colored(e["name"]), "st_ms":ts*1e-3, "ref":None, "ext":None}
|
||||
continue
|
||||
ext, fmt = [], e["fmt"]
|
||||
if (tb:=fmt.pop("tb", [])):
|
||||
while tb:
|
||||
file, lineno, fxn, code = tb.pop()
|
||||
line = f"{file.split('/')[-1]}:{lineno} {fxn}"
|
||||
if fmt: ext.append(f"{line} {code}")
|
||||
elif not file.startswith("<") and not fxn.startswith("<"): fmt["loc"] = line
|
||||
yield {"device":dev, "name":fmt_colored(e["name"]), "dur_ms":e["dur"]*1e-3, "st_ms":e["st"]*1e-3, "fmt":fmt, "ref":e["ref"],
|
||||
"ext":"\n".join(ext)}
|
||||
def fmt_top(k:dict) -> str:
|
||||
return f"{fmt_colored(k['name'])}{' ' * max(0, 38-ansilen(k['name']))} {time_to_str(k['dur_ms']*1e-3, w=9)} {k['count']:7d} {k['pct']:6.2f}%"+\
|
||||
(" "*4+fmt_data(k['fmt']) if k['fmt'] else "")
|
||||
def fmt_all(k:dict) -> str:
|
||||
if k["device"] in {"MARKER", "SOURCE"}: return f"--- {k['device']} {k['name']}"+(f"/{k['st_ms']:9.2f}ms" if k['st_ms'] else "")
|
||||
ptm = colored(time_to_str(k["dur_ms"]*1e-3, w=9), "yellow" if k["dur_ms"] > 10 else None)
|
||||
name = f"*** {k['device'][:7]:7s} "+k["name"]+" "*(46-ansilen(k["name"]))
|
||||
return f"{name} tm {ptm}/{k['st_ms']:9.2f}ms"+(f" ({fmt_data(k['fmt'])})" if k["fmt"] else "")
|
||||
fmt_row = fmt_top if args.t else fmt_all
|
||||
seen_refs:set[int] = set()
|
||||
def render_event(k:dict, ls=args.list) -> None:
|
||||
if len(args.src) > 1 and ansistrip(k["name"]) not in args.src: return None
|
||||
print(emit(k, to_str=fmt_row))
|
||||
if k["ref"] is not None and k["ref"] not in seen_refs:
|
||||
seen_refs.add(k["ref"])
|
||||
for i,s in enumerate(viz_data.ctxs[k["ref"]]["steps"]):
|
||||
if DEBUG >= 3 and s["name"] == "View Base AST": print_step(s)
|
||||
if DEBUG >= 4 and s["name"] == "View Source": print_step(s)
|
||||
if DEBUG >= 5 or ls: print(emit(" "*s["depth"]+s["name"]+(f" - {s['match_count']}" if s.get('match_count', 0) else '')))
|
||||
if DEBUG >= 6 or (DEBUG >= 5 and s["name"] == "View Kernel Graph") or (s["name"] in args.src):
|
||||
print_step(s, print_graph=True, reconstruct_matches=s["name"] in args.src)
|
||||
if DEBUG >= 7: print_step(s, reconstruct_matches=True)
|
||||
elif DEBUG >= 3 and k.get("ext"): print(emit(k["ext"]))
|
||||
for k in (produce_top_kernels if args.t else produce_all_kernels)(): render_event(k)
|
||||
|
||||
def get_arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="python -m tinygrad.viz.cli")
|
||||
parser.add_argument("-s", "--src", nargs="+", default=[], metavar="NAME", help="Select a data source (default: all)")
|
||||
parser.add_argument("--list", "--ls", dest="list", action="store_true", help="List sources")
|
||||
parser.add_argument("--interval", nargs=2, metavar=("START", "END"), help="Optional start and end marker")
|
||||
parser.add_argument("-t", nargs="?", type=int, const=20, metavar="COUNT", help="Aggregate top kernels (optional count, default 20)")
|
||||
parser.add_argument("--profile-path", type=str, metavar="PATH", help="Optional path to profile.pkl (default: latest profile)",
|
||||
default=temp("profile.pkl", append_user=True))
|
||||
parser.add_argument("--rewrites-path", type=str, metavar="PATH", help="Optional path to rewrites.pkl (default: latest rewrites)",
|
||||
default=temp("rewrites.pkl", append_user=True))
|
||||
parser.add_argument("--json", action="store_true", help="Emit profiler output as JSON")
|
||||
return parser
|
||||
|
||||
if __name__ == "__main__":
|
||||
try: main(get_arg_parser().parse_args())
|
||||
except KeyboardInterrupt: pass
|
||||
14
tinygrad_repo/tinygrad/viz/fetch_assets.sh
Executable file
14
tinygrad_repo/tinygrad/viz/fetch_assets.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
fetch() {
|
||||
echo "fetch $1"
|
||||
mkdir -p assets/$1
|
||||
rmdir assets/$1
|
||||
curl -o assets/$1 https://$1
|
||||
}
|
||||
fetch "d3js.org/d3.v7.min.js"
|
||||
fetch "dagrejs.github.io/project/dagre/latest/dagre.min.js"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/python.min.js"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/cpp.min.js"
|
||||
fetch "unpkg.com/@highlightjs/cdn-assets@11.10.0/styles/tokyo-night-dark.min.css"
|
||||
437
tinygrad_repo/tinygrad/viz/index.html
Normal file
437
tinygrad_repo/tinygrad/viz/index.html
Normal file
@@ -0,0 +1,437 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>tinygrad viz</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<script src="assets/d3js.org/d3.v7.min.js" charset="utf-8"></script>
|
||||
<script src="assets/dagrejs.github.io/project/dagre/latest/dagre.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css">
|
||||
<script src="assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
|
||||
<script src="assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/python.min.js"></script>
|
||||
<script src="assets/cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/cpp.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/unpkg.com/@highlightjs/cdn-assets@11.10.0/styles/tokyo-night-dark.min.css" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html, body {
|
||||
color: #f0f0f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variation-settings: "wdth" 100;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
background-color: #08090e;
|
||||
scrollbar-color: #686977 #151720;
|
||||
}
|
||||
::-webkit-scrollbar-track { background: #151720; }
|
||||
::-webkit-scrollbar-thumb { background: #686977; }
|
||||
a {
|
||||
color: #4a90e2;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
ul {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul > p {
|
||||
opacity: 0.6;
|
||||
}
|
||||
ul.active > p {
|
||||
opacity: 1;
|
||||
}
|
||||
ul > ul {
|
||||
display: none;
|
||||
margin-left: 6px;
|
||||
}
|
||||
ul.has-children ul {
|
||||
margin-left: calc(6px + 1ch);
|
||||
}
|
||||
ul.has-children > p::before {
|
||||
content:"▸ ";
|
||||
}
|
||||
ul.has-children.expanded > p::before {
|
||||
content:"▾ ";
|
||||
}
|
||||
ul.expanded > ul {
|
||||
display: block;
|
||||
}
|
||||
ul.disabled > p {
|
||||
opacity: 0.4;
|
||||
}
|
||||
ul.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
#graph svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
svg * {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
g.clickable * {
|
||||
cursor: pointer;
|
||||
user-select: auto;
|
||||
}
|
||||
g.tag circle {
|
||||
fill: #FFD700;
|
||||
stroke: #B8860B;
|
||||
}
|
||||
g.tag.collapsed circle, g.tag.collapsed rect {
|
||||
fill: #5CD68D;
|
||||
stroke: #4a4b57;
|
||||
}
|
||||
g.tag.expanded circle, g.tag.expanded rect {
|
||||
fill: #9FDDE6;
|
||||
stroke: #4a4b57;
|
||||
}
|
||||
g.port circle {
|
||||
fill: #b3dcc2;
|
||||
}
|
||||
g.tag circle, g.tag rect, #edge-labels circle {
|
||||
stroke-width: 0.8;
|
||||
}
|
||||
g.tag text, #edge-labels text {
|
||||
font-family: monospace;
|
||||
text-anchor: middle;
|
||||
font-size: 6px;
|
||||
fill: #08090e;
|
||||
}
|
||||
g.tag.ref * {
|
||||
cursor: pointer;
|
||||
}
|
||||
g.tag.ref circle {
|
||||
fill: #9FDDE6;
|
||||
stroke: #4a4b57;
|
||||
}
|
||||
g.tag.ref path {
|
||||
fill: none;
|
||||
stroke: #08090e;
|
||||
stroke-width: 0.7;
|
||||
stroke-linejoin: miter;
|
||||
}
|
||||
.label :is(text, p) {
|
||||
font-weight: 350;
|
||||
}
|
||||
g.node rect {
|
||||
stroke-width: 1.4;
|
||||
stroke: #4a4b57;
|
||||
}
|
||||
g.overlay rect {
|
||||
fill: rgba(26, 27, 38, 0.5);
|
||||
}
|
||||
.edgePath {
|
||||
fill: none;
|
||||
stroke-width: 1.4px;
|
||||
}
|
||||
g.label rect.bg {
|
||||
fill: transparent;
|
||||
stroke: none;
|
||||
}
|
||||
g.label rect.bg.highlight {
|
||||
fill: #5f0059;
|
||||
}
|
||||
#insts .line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#insts .left {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
#insts .left.highlight {
|
||||
background-color: rgba(0, 199, 47, 0.2);
|
||||
}
|
||||
#insts .pc {
|
||||
color: #73daca;
|
||||
}
|
||||
g.node.highlight rect.node, .edgePath.highlight, g.port circle {
|
||||
stroke: #89C9A2;
|
||||
}
|
||||
g.highlight.child rect.node, .edgePath.highlight.child {
|
||||
stroke: #C888B0;
|
||||
}
|
||||
#edge-labels g.port.highlight {
|
||||
display: block
|
||||
}
|
||||
#edge-labels g.port {
|
||||
display: none
|
||||
}
|
||||
#arrowhead {
|
||||
fill: #4a4b57;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.container {
|
||||
flex: 0 0 auto;
|
||||
background-color: #0f1018;
|
||||
padding: 20px;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.metadata > * + *, .info > * + *, .rewrite-container > * + *, .ctx-list > * + * {
|
||||
margin-top: 12px;
|
||||
}
|
||||
ul > * + *, .args > * + * {
|
||||
margin-top: 4px;
|
||||
}
|
||||
#graph {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
#profiler, #custom {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
margin-top: 50px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.ctx-list-parent {
|
||||
width: 15%;
|
||||
padding-top: 50px;
|
||||
border-right: 1px solid #4a4b56;
|
||||
}
|
||||
.ctx-list, .metadata {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.metadata-parent {
|
||||
width: 20%;
|
||||
border-left: 1px solid #4a4b56;
|
||||
margin-left: auto;
|
||||
}
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 3;
|
||||
background-color: transparent;
|
||||
}
|
||||
.floating-container {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 20px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
outline: none;
|
||||
background-color: #1a1b26;
|
||||
border: 1px solid #4a4b56;
|
||||
color: #f0f0f5;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #2a2b36;
|
||||
}
|
||||
.collapsed .container {
|
||||
display: none;
|
||||
}
|
||||
.rewrite-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rewrite-list > ul {
|
||||
padding: 2px;
|
||||
}
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
pre code.hljs {
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
padding: 8px;
|
||||
}
|
||||
pre.full-height code.hljs {
|
||||
max-height: none;
|
||||
}
|
||||
#progress-message {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 50%;
|
||||
top: 2%;
|
||||
color: #ffd230;
|
||||
display: none;
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background-color: #1e2029;
|
||||
padding: 4px 8px;
|
||||
max-width: 164px;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
}
|
||||
#device-list > div {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
}
|
||||
#device-list > div:hover {
|
||||
background-color: rgba(20, 23, 35, 0.3);
|
||||
}
|
||||
#device-list {
|
||||
height: fit-content;
|
||||
}
|
||||
.raw-text {
|
||||
padding-left: 15px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.raw-text > pre {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
}
|
||||
.raw-text code {
|
||||
max-height: none !important;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: #1a1b26;
|
||||
color: #f0f0f5;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
table td {
|
||||
border-bottom: 1px solid #4a4b56;
|
||||
vertical-align: top;
|
||||
}
|
||||
table tr:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
tr.main-row:hover, tr.main-row.expanded, tr.nested-row > td > table, tr.nested-row thead {
|
||||
background-color: #2a2d3a;
|
||||
}
|
||||
tr.main-row > td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
td.Instruction {
|
||||
font-family: monospace;
|
||||
}
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: #20222e;
|
||||
}
|
||||
thead th {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #4a4b56;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
tr.nested-row > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
tr.nested-row table tr.main-row:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
tr.main-row.has-children > td:first-child > p {
|
||||
display: inline-block;
|
||||
}
|
||||
tr.main-row.has-children > td:first-child::before {
|
||||
content: "▸ ";
|
||||
width: 1em;
|
||||
margin-left: -0.25em;
|
||||
}
|
||||
tr.main-row.has-children.expanded > td:first-child::before {
|
||||
content: "▾ ";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="floating-container">
|
||||
<button class="btn collapse-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20"><path d="M15 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
<button class="btn" id="zoom-to-fit-btn" aria-label="Fit graph">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 0 0 3.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0 1 20.25 6v1.5m0 9V18A2.25 2.25 0 0 1 18 20.25h-1.5m-9 0H6A2.25 2.25 0 0 1 3.75 18v-1.5M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="progress-message"></div>
|
||||
<div class="container ctx-list-parent"><div class="ctx-list"></div></div>
|
||||
<div class="view" id="profiler"></div>
|
||||
<div class="view" id="graph">
|
||||
<svg id="graph-svg" preserveAspectRatio="xMidYMid meet">
|
||||
<g id="render">
|
||||
<g id="edges"></g>
|
||||
<g id="nodes"></g>
|
||||
<g id="edge-labels"></g> <!-- NOTE: this ensures edge labels are always on top -->
|
||||
</g>
|
||||
<defs>
|
||||
<marker id="arrowhead" viewBox="0 -5 10 10" refX="10" refY="0" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M0,-5L10,0L0,5" fill="context-stroke"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="view" id="custom"></div>
|
||||
<div class="container metadata-parent"><div class="metadata"></div></div>
|
||||
</div>
|
||||
<div id="tooltip" class="wrap"></div>
|
||||
<script src="/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1215
tinygrad_repo/tinygrad/viz/js/index.js
Normal file
1215
tinygrad_repo/tinygrad/viz/js/index.js
Normal file
File diff suppressed because it is too large
Load Diff
119
tinygrad_repo/tinygrad/viz/js/worker.js
Normal file
119
tinygrad_repo/tinygrad/viz/js/worker.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const NODE_PADDING = 10;
|
||||
const rectDims = (lw, lh) => ({ width:lw+NODE_PADDING*2, height:lh+NODE_PADDING*2, labelWidth:lw, labelHeight:lh });
|
||||
|
||||
const canvas = new OffscreenCanvas(0, 0);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
onmessage = (e) => {
|
||||
try {
|
||||
const { data, opts } = e.data;
|
||||
const g = new dagre.graphlib.Graph({ compound: true }).setDefaultEdgeLabel(function() { return {}; });
|
||||
(data.blocks != null ? layoutCfg : layoutUOp)(g, data, opts);
|
||||
postMessage({result: dagre.graphlib.json.write(g)});
|
||||
self.close();
|
||||
} catch (err) {
|
||||
postMessage({error: err.stack || err.message || String(err)});
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
const layoutCfg = (g, { blocks, paths, pc_tokens }) => {
|
||||
const lineHeight = 18;
|
||||
g.setGraph({ rankdir:"TD", font:"monospace", lh:lineHeight, textSpace:"1ch" });
|
||||
ctx.font = `350 ${lineHeight}px ${g.graph().font}`;
|
||||
// basic blocks render the assembly in nodes
|
||||
const tokenColors = {0:"#7aa2f7", 1:"#9aa5ce"};
|
||||
for (const [lead, members] of Object.entries(blocks)) {
|
||||
let [width, height, label] = [0, 0, []];
|
||||
for (const m of members) {
|
||||
const tokens = pc_tokens[m];
|
||||
label.push(tokens.map((t, i) => ({st:t.st, keys:t.keys, color:tokenColors[t.kind]})));
|
||||
width = Math.max(width, ctx.measureText(tokens.map((t) => t.st).join("")).width);
|
||||
height += lineHeight;
|
||||
}
|
||||
g.setNode(lead, { ...rectDims(width, height), label, labelX:0, id:lead, color:"#1a1b26", addrspace:null });
|
||||
}
|
||||
// paths become edges between basic blocks
|
||||
const pathColors = {0:"#3f7564", 1:"#7a4540", 2:"#3b5f7e"};
|
||||
for (const [lead, value] of Object.entries(paths)) {
|
||||
for (const [id, color] of Object.entries(value)) g.setEdge(lead, id, {label:{type:"port", text:""}, color:pathColors[color]});
|
||||
}
|
||||
dagre.layout(g);
|
||||
}
|
||||
|
||||
const layoutUOp = (g, { graph, change }, opts) => {
|
||||
const lineHeight = 14;
|
||||
g.setGraph({ rankdir: "LR", font:"sans-serif", lh:lineHeight });
|
||||
ctx.font = `350 ${lineHeight}px ${g.graph().font}`;
|
||||
if (change?.length) g.setNode("overlay", {label:"", labelWidth:0, labelHeight:0, labelX:0, className:"overlay"});
|
||||
let callCount = 0;
|
||||
for (const [k, {label, src, ref, color, tag, exclude, addrspace}] of Object.entries(graph)) {
|
||||
// adjust node dims by label size (excluding escape codes) + add padding
|
||||
let [width, height] = [0, 0];
|
||||
for (line of label.replace(/\u001B\[(?:K|.*?m)/g, "").split("\n")) {
|
||||
width = Math.max(width, ctx.measureText(line).width);
|
||||
height += lineHeight;
|
||||
}
|
||||
const callNode = label.startsWith("CALL\n") || label.startsWith("FUNCTION\n");
|
||||
if (callNode) callCount++;
|
||||
g.setNode(k, {...rectDims(width, height), label, labelX:0, ref, id:k, color, tag, callNode, exclude, addrspace});
|
||||
// add edges
|
||||
const edgeCounts = {};
|
||||
for (const [_, s] of src) edgeCounts[s] = (edgeCounts[s] || 0)+1;
|
||||
for (const [port, s] of src) g.setEdge(s, k, { label: edgeCounts[s] > 1 ? {type:"tag", text:edgeCounts[s]} : {type:"port", text:port},
|
||||
...(callNode && port === 0 && {color:"#a0a1b8"})});
|
||||
if (change?.includes(parseInt(k))) g.setParent(k, "overlay");
|
||||
}
|
||||
// optionally hide nodes from the layout
|
||||
if (!opts.showSink) {
|
||||
for (const n of g.nodes()) {
|
||||
const node = g.node(n);
|
||||
if ((node.label === "SINK" || node.label.startsWith("SINK\n")) && (g.successors(n) || []).length === 0) g.removeNode(n);
|
||||
}
|
||||
}
|
||||
if (!opts.showIndexing) {
|
||||
for (const n of g.nodes()) {
|
||||
const node = g.node(n);
|
||||
if (node.label.includes("dtypes.weakint")) g.removeNode(n);
|
||||
}
|
||||
}
|
||||
// optionally remove node srcs, track affected nodes
|
||||
const disconnected = new Set();
|
||||
const CALL_TAG_WIDTH = 14;
|
||||
for (const n of g.nodes()) {
|
||||
const node = g.node(n);
|
||||
for (const consumerId of (g.successors(n) || [])) {
|
||||
const consumer = g.node(consumerId);
|
||||
// add +- toggle if this consumer has collapsible sources
|
||||
const edge = g.edge(n, consumerId);
|
||||
const collapsible = consumer.callNode ? edge?.label?.text === 0 : node.exclude;
|
||||
if (!collapsible) continue;
|
||||
consumer.collapsible = true;
|
||||
// increase width of call/function nodes to make space for a toggle
|
||||
if (consumer.callNode) { consumer.width = consumer.labelWidth+NODE_PADDING*2+CALL_TAG_WIDTH; consumer.labelX = CALL_TAG_WIDTH/2; }
|
||||
// make sources invisible if UI has toggled it off
|
||||
const collapsed = consumer.callNode ? opts.showCallSrc === opts.callSrcMask.has(consumerId) : !opts.expandedNodes.has(consumerId);
|
||||
if (!collapsed) continue;
|
||||
consumer.collapsed = true;
|
||||
g.removeEdge(n, consumerId);
|
||||
disconnected.add(n);
|
||||
}
|
||||
}
|
||||
// remove nodes that are now disconnected (no successors), only from affected subtree
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const n of disconnected) {
|
||||
if (!g.hasNode(n)) continue;
|
||||
if ((g.successors(n) || []).length === 0) {
|
||||
for (const pred of (g.predecessors(n) || [])) disconnected.add(pred);
|
||||
g.removeNode(n);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.graph().callCount = callCount;
|
||||
dagre.layout(g);
|
||||
// remove overlay node if it's empty
|
||||
if (!g.node("overlay")?.width) g.removeNode("overlay");
|
||||
}
|
||||
723
tinygrad_repo/tinygrad/viz/serve.py
Executable file
723
tinygrad_repo/tinygrad/viz/serve.py
Executable file
@@ -0,0 +1,723 @@
|
||||
#!/usr/bin/env python3
|
||||
import multiprocessing, pickle, difflib, os, threading, json, time, sys, webbrowser, socket, argparse, codecs, io, struct, re, traceback, itertools
|
||||
import socketserver
|
||||
from contextlib import redirect_stdout, redirect_stderr, contextmanager
|
||||
from decimal import Decimal
|
||||
from dataclasses import dataclass, field
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from typing import Any, TypedDict, TypeVar, Generator, Callable
|
||||
from tinygrad.helpers import colored, getenv, tqdm, unwrap, word_wrap, TRACEMETA, ProfileEvent, ProfileRangeEvent, TracingKey, ProfilePointEvent, temp
|
||||
from tinygrad.helpers import printable, Context, START_TIME, NO_COLOR, ansistrip
|
||||
from tinygrad.renderer.amd.dsl import Inst
|
||||
from tinygrad.renderer.amd import detect_format
|
||||
|
||||
# NOTE: using HTTPServer forces a potentially slow socket.getfqdn
|
||||
class TCPServerWithReuse(socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
def __init__(self, server_address, RequestHandlerClass):
|
||||
print(f"*** started server on http://127.0.0.1:{server_address[1]} at {time.perf_counter()-START_TIME:.2f} s")
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
|
||||
class HTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
def send_data(self, data:bytes, content_type:str="application/json", status_code:int=200):
|
||||
self.send_response(status_code)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
return self.wfile.write(data)
|
||||
def stream_json(self, source:Generator):
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
for r in source:
|
||||
self.wfile.write(f"data: {json.dumps(r)}\n\n".encode("utf-8"))
|
||||
self.wfile.flush()
|
||||
self.wfile.write("data: [DONE]\n\n".encode("utf-8"))
|
||||
# pass if client closed connection
|
||||
except (BrokenPipeError, ConnectionResetError): return
|
||||
|
||||
from tinygrad.uop.ops import TrackedGraphRewrite, RewriteTrace, UOp, Ops, GroupOp, srender, sint, sym_infer, range_str, range_start, multirange_str
|
||||
from tinygrad.uop.ops import KernelInfo
|
||||
from tinygrad.uop.render import print_uops, pyrender
|
||||
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEvent, ProfileGraphEntry, ProfileProgramEvent
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
|
||||
uops_colors = {Ops.LOAD: "#ffc0c0", Ops.STORE: "#87CEEB", Ops.CONST: "#e0e0e0", Ops.REDUCE: "#FF5B5B",
|
||||
**{x:"#f2cb91" for x in {Ops.DEFINE_LOCAL, Ops.DEFINE_REG}}, Ops.SHAPED_WMMA: "#FF5B5B",
|
||||
Ops.RANGE: "#c8a0e0", Ops.BARRIER: "#ff8080", Ops.IF: "#c8b0c0", Ops.SPECIAL: "#c0c0ff",
|
||||
Ops.INDEX: "#D8F9E4", Ops.STACK: "#D8F9E4",
|
||||
Ops.WMMA: "#efefc0", Ops.MULTI: "#f6ccff", Ops.INS: "#eec4ff",
|
||||
**{x:"#D8F9E4" for x in GroupOp.Movement}, **{x:"#ffffc0" for x in GroupOp.ALU}, Ops.THREEFRY:"#ffff80",
|
||||
Ops.SLICE: "#E5EAFF", Ops.BUFFER: "#B0BDFF", Ops.GETADDR: "#9DB1F0", Ops.COPY: "#a040a0", Ops.CUSTOM_FUNCTION: "#bf71b6",
|
||||
Ops.CALL: "#00B7C8", Ops.FUNCTION: "#C07788", Ops.PARAM: "#14686F", Ops.SOURCE: "#c0c0c0", Ops.BINARY: "#404040",
|
||||
Ops.LINEAR: "#7DF4FF",
|
||||
Ops.ALLREDUCE: "#ff40a0", Ops.MSELECT: "#d040a0", Ops.MSTACK: "#d040a0", Ops.CONTIGUOUS: "#FFC14D",
|
||||
Ops.STAGE: "#AC640D", Ops.REWRITE_ERROR: "#ff2e2e", Ops.AFTER: "#8A7866", Ops.END: "#524C46"}
|
||||
|
||||
addrspace_colors = {AddrSpace.REG:"#e68181", AddrSpace.LOCAL:"#e7c86a", AddrSpace.GLOBAL:"#75bd7b"}
|
||||
|
||||
# VIZ API
|
||||
|
||||
# A step is a lightweight descriptor for a trace entry
|
||||
# Includes a name, metadata and a URL path for fetching the full data
|
||||
|
||||
def create_step(name:str, query:tuple[str, int, int], data=None, depth:int=0, **kwargs) -> dict:
|
||||
return {"name":name, "query":f"{query[0]}?ctx={query[1]}&step={query[2]}", "data":data, "depth":depth, **kwargs}
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VizData:
|
||||
trace:RewriteTrace = field(default_factory=lambda: RewriteTrace([], [], {}))
|
||||
ctxs:list[dict] = field(default_factory=list)
|
||||
ref_map:dict[Any, int] = field(default_factory=dict)
|
||||
all_uops:dict[int, UOp] = field(default_factory=dict)
|
||||
|
||||
# ** load all saved rewrites
|
||||
|
||||
def load_rewrites(data:VizData) -> None:
|
||||
assert not data.ctxs and not data.ref_map, "load_rewrites called multiple times"
|
||||
for i,k in enumerate(data.trace.keys):
|
||||
steps:list[dict] = []
|
||||
p:UOp|None = None
|
||||
for j,s in enumerate(data.trace.rewrites[i]):
|
||||
steps.append(create_step(s.name, ("/graph-rewrites", i, j), loc=s.loc, match_count=len(s.matches), code_line=printable(s.loc),
|
||||
trace=k.tb if j==0 else None, depth=s.depth))
|
||||
# get source and binary from Ops.PROGRAM
|
||||
if s.name == "View Program":
|
||||
p = _reconstruct(data, s.sink, depth=1)
|
||||
steps.append(create_step("View UOp List", ("/uops", i, len(steps))))
|
||||
steps.append(create_step("View Source", ("/code", i, len(steps)), p.src[3].arg))
|
||||
steps.append(create_step("View Disassembly", ("/asm", i, len(steps)), (k.ret, p.src[4].arg)))
|
||||
for key in k.keys: data.ref_map[canonicalize_ast(key) if isinstance(key, UOp) else key] = i
|
||||
data.ctxs.append({"name":k.display_name, "steps":steps, "prg":p})
|
||||
|
||||
# ** get the complete UOp graphs for one rewrite
|
||||
|
||||
class GraphRewriteDetails(TypedDict):
|
||||
graph: dict # JSON serialized UOp for this rewrite step
|
||||
uop: str # strigified UOp for this rewrite step
|
||||
diff: list[str]|None # diff of the single UOp that changed
|
||||
change: list[int]|None # the new UOp id + all its parents ids
|
||||
upat: tuple[tuple[str, int], str]|None # [loc, source_code] of the matched UPat
|
||||
|
||||
def shape_to_str(s:tuple[sint, ...]): return "(" + ','.join(srender(x) for x in s) + ")"
|
||||
def mask_to_str(s:tuple[tuple[sint, sint], ...]): return "(" + ','.join(shape_to_str(x) for x in s) + ")"
|
||||
def pystr(u:UOp) -> str:
|
||||
# pyrender may check for shape mismatch
|
||||
try: return pyrender(u)
|
||||
except Exception: return str(u)
|
||||
|
||||
def fmt_colored(s:str) -> str: return ansistrip(s) if NO_COLOR else s
|
||||
|
||||
def canonicalize_ast(u:UOp) -> UOp: return u.replace(arg=KernelInfo()) if u.op is Ops.SINK and isinstance(u.arg, KernelInfo) else u
|
||||
|
||||
def uop_to_json(data:VizData, x:UOp) -> dict[int, dict]:
|
||||
assert isinstance(x, UOp)
|
||||
graph: dict[int, dict] = {}
|
||||
excluded: set[UOp] = set()
|
||||
for u in (toposort:=x.toposort()):
|
||||
# always exclude DEVICE/CONST/UNIQUE
|
||||
if u.op in {Ops.DEVICE, Ops.CONST, Ops.UNIQUE, Ops.LUNIQUE} and u is not x: excluded.add(u)
|
||||
if u.op is Ops.CONST and len(u.src) and u.src[0].op in {Ops.UNIQUE, Ops.LUNIQUE}: excluded.remove(u)
|
||||
if u.op is Ops.STACK and len(u.src) == 0: excluded.add(u)
|
||||
# exclude RESHAPE/EXPAND that only serve to broadcast a CONST
|
||||
if u.op in {Ops.RESHAPE, Ops.EXPAND} and len(u.src) >= 1 and u.src[0] in excluded and u is not x: excluded.add(u)
|
||||
if u.op in GroupOp.Movement: excluded.update(s for s in u.src if s.op is Ops.STACK and all(x.op is Ops.CONST for x in s.src))
|
||||
for u in toposort:
|
||||
argst = codecs.decode(str(u.arg), "unicode_escape")
|
||||
if u.op in GroupOp.Movement: argst = (mask_to_str if u.op in {Ops.SHRINK, Ops.PAD} else shape_to_str)(u.marg)
|
||||
if u.op is Ops.BINARY: argst = f"<{len(u.arg)} bytes>"
|
||||
if u.op is Ops.CONST and dtypes.is_float(u.dtype): argst = f"{u.arg:g}"
|
||||
wrap_len = 200 if u.op is Ops.SOURCE else 80
|
||||
label = f"{str(u.op).split('.')[1]}{(chr(10)+word_wrap(argst.replace(':', ''), wrap=wrap_len)) if u.arg is not None else ''}"
|
||||
if u.dtype != dtypes.void: label += f"\n{u.dtype}"
|
||||
for idx,x in enumerate(u.src[:1] if u.op in {Ops.STAGE, Ops.INDEX} else (u.src if u.op is not Ops.END else [])):
|
||||
if x in excluded:
|
||||
# walk through excluded movement ops to find the underlying CONST
|
||||
cx = x
|
||||
while cx.op in GroupOp.Movement and len(cx.src) >= 1 and cx.src[0] in excluded: cx = cx.src[0]
|
||||
arg = f"{cx.arg:g}" if cx.op is Ops.CONST and dtypes.is_float(cx.dtype) else f"{cx.arg}"
|
||||
label += f"\n{cx.op.name}{idx} {arg}" + (f" {cx.src[0].op}" if len(cx.src) else "")
|
||||
try:
|
||||
if len(rngs:=u.ranges):
|
||||
label += f"\n({multirange_str(rngs, color=True)})"
|
||||
if u._shape is not None:
|
||||
label += f"\n{shape_to_str(u.shape)}"
|
||||
if u.op in {Ops.CALL, Ops.FUNCTION}:
|
||||
label += f"\n{u.src[0].key.hex()[:8]}"
|
||||
if u.op in {Ops.INDEX, Ops.STAGE}:
|
||||
if len(u.toposort()) < 30: label += f"\n{u.render()}"
|
||||
ranges: list[UOp] = []
|
||||
for us in u.src[1:]: ranges += [s for s in us.toposort() if s.op in {Ops.RANGE, Ops.SPECIAL}]
|
||||
if ranges: label += "\n"+' '.join([f"{s.render()}={s.vmax+1}" for s in ranges])
|
||||
if u.op in {Ops.END, Ops.REDUCE} and len(trngs:=list(UOp.sink(*u.src[range_start[u.op]:]).ranges)):
|
||||
label += "\n"+' '.join([f"{range_str(s, color=True)}({s.vmax+1})" for s in trngs])
|
||||
except Exception:
|
||||
label += "\n<ISSUE GETTING LABEL>"
|
||||
ref = data.ref_map.get(canonicalize_ast(u.src[0])) if u.op in {Ops.CALL, Ops.FUNCTION} else None
|
||||
if ref is not None: label += f"\ncodegen@{fmt_colored(data.ctxs[ref]['name'])}"
|
||||
# NOTE: kernel already has metadata in arg
|
||||
if TRACEMETA >= 2 and u.metadata is not None and u.op not in {Ops.CALL, Ops.FUNCTION}: label += "\n"+str(u.metadata)
|
||||
# limit SOURCE labels line count
|
||||
if u.op is Ops.SOURCE and len(lines:=label.split("\n")) > 40:
|
||||
label = "\n".join(lines[:30]) + "\n..."
|
||||
graph[id(u)] = {"label":label, "src":[(i,id(x)) for i,x in enumerate(u.src)], "exclude":u in excluded, "color":uops_colors.get(u.op, "#ffffff"),
|
||||
"ref":ref, "tag":repr(u.tag) if u.tag is not None else None,
|
||||
"addrspace":addrspace_colors.get(u.addrspace, None) if u.addrspace is not None else None}
|
||||
return graph
|
||||
|
||||
def _reconstruct(data:VizData, a:int, depth:int|None=None):
|
||||
if depth is None and a in data.all_uops: return data.all_uops[a]
|
||||
op, dtype, src, arg, *rest = data.trace.uop_fields[a]
|
||||
if depth is not None and depth <= 0: return UOp(op, dtype, (), arg, *rest)
|
||||
ret = UOp(op, dtype, tuple(_reconstruct(data, s, None if depth is None else depth-1) for s in src), arg, *rest)
|
||||
if depth is None: data.all_uops[a] = ret
|
||||
return ret
|
||||
|
||||
def get_full_rewrite(data:VizData, ctx:TrackedGraphRewrite) -> Generator[GraphRewriteDetails, None, None]:
|
||||
next_sink = _reconstruct(data, ctx.sink)
|
||||
yield {"graph":uop_to_json(data, next_sink), "uop":pystr(next_sink), "change":None, "diff":None, "upat":None}
|
||||
replaces: dict[UOp, UOp] = {}
|
||||
for u0_num,u1_num,upat_loc,dur in tqdm(ctx.matches, disable=not ctx.matches):
|
||||
replaces[u0:=_reconstruct(data, u0_num)] = u1 = _reconstruct(data, u1_num)
|
||||
try: new_sink = next_sink.substitute(replaces)
|
||||
except RuntimeError as e: new_sink = UOp(Ops.NOOP, arg=str(e))
|
||||
match_repr = f"# {dur*1e6:.2f} us\n"+printable(upat_loc)
|
||||
yield {"graph":(sink_json:=uop_to_json(data, new_sink)), "uop":pystr(new_sink), "change":[id(x) for x in u1.toposort() if id(x) in sink_json],
|
||||
"diff":list(difflib.unified_diff(pystr(u0).splitlines(), pystr(u1).splitlines())), "upat":(upat_loc, match_repr)}
|
||||
if not ctx.bottom_up: next_sink = new_sink
|
||||
|
||||
# encoder helpers
|
||||
|
||||
def enum_str(s, cache:dict[str, int]) -> int:
|
||||
if (cret:=cache.get(s)) is not None: return cret
|
||||
cache[s] = ret = len(cache)
|
||||
return ret
|
||||
|
||||
def option(s:int|None) -> int: return 0 if s is None else s+1
|
||||
|
||||
def rel_ts(ts:int|Decimal, start_ts:int, ctx:str="") -> int:
|
||||
val = int(ts) - start_ts
|
||||
if val < 0 or val > 0xFFFFFFFF: raise ValueError(f"timestamp out of range: {ctx} diff={val} (ts={ts} start={start_ts})")
|
||||
return val
|
||||
|
||||
# Profiler API
|
||||
|
||||
def cpu_ts_diff(device_ts_diffs:dict[str, Decimal], device:str) -> Decimal: return device_ts_diffs.get(device, Decimal(0))
|
||||
|
||||
DevEvent = ProfileRangeEvent|ProfileGraphEntry|ProfilePointEvent
|
||||
def flatten_events(profile:list[ProfileEvent], device_ts_diffs:dict[str, Decimal]) -> Generator[tuple[Decimal, Decimal, DevEvent], None, None]:
|
||||
for e in profile:
|
||||
if isinstance(e, ProfileRangeEvent): yield (e.st+(diff:=cpu_ts_diff(device_ts_diffs, e.device)), (e.en if e.en is not None else e.st)+diff, e)
|
||||
elif isinstance(e, ProfilePointEvent): yield (e.ts, e.ts, e)
|
||||
elif isinstance(e, ProfileGraphEvent):
|
||||
cpu_ts = []
|
||||
for ent in e.ents: cpu_ts += [e.sigs[ent.st_id]+(diff:=cpu_ts_diff(device_ts_diffs, ent.device)), e.sigs[ent.en_id]+diff]
|
||||
yield (st:=min(cpu_ts)), (et:=max(cpu_ts)), ProfileRangeEvent(f"{e.ents[0].device.split(':')[0]} Graph", f"batched {len(e.ents)}", st, et)
|
||||
for i,ent in enumerate(e.ents): yield (cpu_ts[i*2], cpu_ts[i*2+1], ent)
|
||||
|
||||
# normalize event timestamps and attach kernel metadata
|
||||
def timeline_layout(data:VizData, dev_events:list[tuple[int, int, float, DevEvent]], start_ts:int, scache:dict[str, int]) -> bytes|None:
|
||||
events:list[bytes] = []
|
||||
ei:ProfilePointEvent|None = None
|
||||
for st,et,dur,e in dev_events:
|
||||
if isinstance(e, ProfilePointEvent) and e.name == "exec": ei = e
|
||||
if dur == 0: continue
|
||||
name, key = e.name, None
|
||||
fmt:dict = {}
|
||||
if (ref:=data.ref_map.get(name)) is not None and ref < len(data.ctxs):
|
||||
name = data.ctxs[ref]["name"]
|
||||
if (p:=data.ctxs[ref].get("prg")) is not None and (ki:=p.src[0].arg).estimates is not None and ei is not None:
|
||||
fmt["FLOPS"] = int(sym_infer(ki.estimates.ops, var_vals:=ei.arg['var_vals'])/(t:=dur*1e-6))
|
||||
fmt["B/s mem"], fmt["B/s lds"] = int(sym_infer(ki.estimates.mem, var_vals)/t), int(sym_infer(ki.estimates.lds, var_vals)/t)
|
||||
if ei.arg["metadata"]: fmt["metadata"] = ",".join([str(m) for m in ei.arg['metadata']+["batched" if isinstance(e,ProfileGraphEntry) else ""]])
|
||||
key = ei.key
|
||||
elif isinstance(e.name, TracingKey):
|
||||
name = e.name.display_name
|
||||
ref = next((v for k in e.name.keys if (v:=data.ref_map.get(k)) is not None), None)
|
||||
if isinstance(e.name.ret, str): fmt.update(json.loads(e.name.ret[4:]) if e.name.ret.startswith("JSON") else {"metadata":e.name.ret})
|
||||
elif isinstance(e.name.ret, int): fmt["B/s"], fmt["B"] = int(e.name.ret/(dur*1e-6)), e.name.ret
|
||||
elif e.name.tb: fmt["tb"] = e.name.tb
|
||||
events.append(struct.pack("<IIIIfI", enum_str(name, scache), option(ref), option(key), rel_ts(st,start_ts, f"'{name}' on {e.device}"),
|
||||
dur, enum_str(json.dumps(fmt),scache)))
|
||||
return struct.pack("<BI", 0, len(events))+b"".join(events) if events else None
|
||||
|
||||
def encode_mem_free(key:int, ts:int, execs:list[ProfilePointEvent], scache:dict) -> bytes:
|
||||
ei_encoding:list[tuple[int, int, int, int]] = [] # <[u32, u32, u32, u8] [run id, display name, buffer number and mode (2 = r/w, 1 = w, 0 = r)]
|
||||
for e in execs:
|
||||
num = next(i for i,k in enumerate(e.arg["bufs"]) if k == key)
|
||||
mode = 2 if (num in e.arg["inputs"] and num in e.arg["outputs"]) else 1 if (num in e.arg["outputs"]) else 0
|
||||
ei_encoding.append((e.key, enum_str(e.arg["name"], scache), num, mode))
|
||||
return struct.pack("<BIII", 0, ts, key, len(ei_encoding))+b"".join(struct.pack("<IIIB", *t) for t in ei_encoding)
|
||||
|
||||
def graph_layout(k:str, dev_events:list[tuple[int, int, float, DevEvent]], start_ts:int, end_ts:int, peaks:list[int], dtype_size:dict[str, int],
|
||||
scache:dict[str, int]) -> tuple[str, bytes|None]:
|
||||
if k.startswith("LINE:"):
|
||||
xy = [(rel_ts(e.ts, start_ts, f"line '{k}' on {e.device}"), e.key) for st,_,_,e in dev_events if isinstance(e, ProfilePointEvent)]
|
||||
peaks.append(peak:=max([y for _,y in xy]))
|
||||
return k.replace("LINE:", ""), struct.pack("<BIBQ", 1, len(xy), 1, peak)+b"".join(struct.pack("<IQ", x, y) for x,y in xy)
|
||||
peak, mem = 0, 0
|
||||
temp:dict[int, int] = {}
|
||||
events:list[bytes] = []
|
||||
buf_ei:dict[int, list[ProfilePointEvent]] = {}
|
||||
for st,_,_,e in dev_events:
|
||||
if not isinstance(e, ProfilePointEvent): continue
|
||||
if e.name == "alloc":
|
||||
safe_sz = min(1_000_000_000_000, e.arg["sz"])
|
||||
events.append(struct.pack("<BIIIQ", 1, rel_ts(e.ts, start_ts, f"alloc on {e.device}"), e.key, enum_str(e.arg["dtype"].name, scache), safe_sz))
|
||||
dtype_size.setdefault(e.arg["dtype"].name, e.arg["dtype"].itemsize)
|
||||
temp[e.key] = nbytes = safe_sz*e.arg["dtype"].itemsize
|
||||
mem += nbytes
|
||||
if mem > peak: peak = mem
|
||||
if e.name == "exec" and e.arg["bufs"]:
|
||||
for b in e.arg["bufs"]: buf_ei.setdefault(b, []).append(e)
|
||||
if e.name == "free":
|
||||
events.append(encode_mem_free(e.key, rel_ts(e.ts, start_ts, f"free on {e.device}"), buf_ei.pop(e.key, []), scache))
|
||||
mem -= temp.pop(e.key)
|
||||
for t in temp: events.append(encode_mem_free(t, rel_ts(end_ts, start_ts, f"end_ts for {k}"), buf_ei.pop(t, []), scache))
|
||||
peaks.append(peak)
|
||||
return f"{k} Memory", struct.pack("<BIBQ", 1, len(events), 0, peak)+b"".join(events) if events else None
|
||||
|
||||
# by default, VIZ does not start when there is an error
|
||||
# use this to instead display the traceback to the user
|
||||
@contextmanager
|
||||
def soft_err(fn:Callable):
|
||||
try: yield
|
||||
except Exception: fn({"src":traceback.format_exc()})
|
||||
|
||||
def row_tuple(row:str) -> tuple[tuple[int, int], ...]:
|
||||
return ((0, 0),) if "Clock" in row else tuple((ord(ss[0][0]), int(ss[1])) if len(ss:=x.split(":"))>1 else (999,999) for x in row.split())
|
||||
|
||||
# *** Performance counters
|
||||
|
||||
metrics:dict[str, Callable[[dict[str, tuple[int, int, int]]], str]] = {
|
||||
"VALU utilization": lambda s: f"{100 * (s['SQ_INSTS_VALU'][0] / s['SQ_INSTS_VALU'][2]) / (s['GRBM_GUI_ACTIVE'][1] * 4):.1f}%",
|
||||
"SALU utilization": lambda s: f"{100 * (s['SQ_INSTS_SALU'][0] / s['SQ_INSTS_SALU'][2]) / (s['GRBM_GUI_ACTIVE'][1] * 4):.1f}%",
|
||||
}
|
||||
|
||||
def unpack_pmc(e) -> dict:
|
||||
agg_cols = ["Name", "Sum"]
|
||||
rows:list[list] = []
|
||||
stats:dict[str, tuple[int, int, int]] = {} # name -> (sum, max, count)
|
||||
view, ptr = memoryview(e.blob).cast('Q'), 0
|
||||
for s in e.sched:
|
||||
sample_cols = ["XCC", "INST", "SE", "SA"] + [f"WGP:{i}" for i in range(s.wgp)]
|
||||
row:list = [s.name, 0, {"cols":sample_cols, "rows":[]}]
|
||||
max_val, cnt = 0, 0
|
||||
for sample in itertools.product(range(s.xcc), range(s.inst), range(s.se), range(s.sa)):
|
||||
vals:list[int] = []
|
||||
# pack work group processors on the same se
|
||||
for _ in range(s.wgp):
|
||||
row[1] += (val:=int(view[ptr]))
|
||||
max_val, cnt = max(max_val, val), cnt + 1
|
||||
vals.append(val)
|
||||
ptr += 1
|
||||
row[2]["rows"].append(sample+tuple(vals))
|
||||
stats[s.name] = (row[1], max_val, cnt)
|
||||
rows.append(row)
|
||||
for name, fn in metrics.items():
|
||||
try: rows.append([name, fn(stats)])
|
||||
except KeyError: pass
|
||||
return {"rows":rows, "cols":agg_cols}
|
||||
|
||||
# ** on startup, list all the performance counter traces
|
||||
|
||||
def load_amd_counters(data:VizData, profile:list) -> None:
|
||||
counter_events:dict[tuple[int, int], dict] = {}
|
||||
durations:dict[str, list[float]] = {}
|
||||
prg_events:dict[int, ProfileProgramEvent] = {}
|
||||
arch = ""
|
||||
for e in profile:
|
||||
if type(e).__name__ in {"ProfilePMCEvent", "ProfileSQTTEvent"}:
|
||||
counter_events.setdefault((e.kern, e.exec_tag), {}).setdefault(type(e).__name__, []).append(e)
|
||||
if isinstance(e, ProfileRangeEvent) and e.device.startswith("AMD") and e.en is not None:
|
||||
durations.setdefault(str(e.name), []).append(float(e.en-e.st))
|
||||
if isinstance(e, ProfileProgramEvent) and e.tag is not None: prg_events[e.tag] = e
|
||||
if isinstance(e, ProfileDeviceEvent) and e.device.startswith("AMD"): arch = f"gfx{unwrap(e.props)['gfx_target_version']//1000}"
|
||||
if len(counter_events) == 0: return None
|
||||
data.ctxs.append({"name":"All Counters", "steps":[create_step("PMC", ("/all-pmc", len(data.ctxs), 0), (durations, all_counters:={}))]})
|
||||
run_number = {n:0 for n,_ in counter_events}
|
||||
for (k, tag),v in counter_events.items():
|
||||
# use the colored name if it exists
|
||||
name = data.ctxs[r]["prg"].src[0].arg.name if (r:=data.ref_map.get(pname:=prg_events[k].name)) is not None else pname
|
||||
run_number[k] += 1
|
||||
steps:list[dict] = []
|
||||
if (pmc:=v.get("ProfilePMCEvent")):
|
||||
steps.append(create_step("PMC", ("/prg-pmc", len(data.ctxs), len(steps)), pmc[0]))
|
||||
all_counters[(name, run_number[k], pname)] = pmc[0]
|
||||
# to decode a SQTT trace, we need the raw stream, program binary and device properties
|
||||
if (sqtt:=v.get("ProfileSQTTEvent")):
|
||||
for e in sqtt:
|
||||
if e.itrace: steps.append(create_step(f"SE:{e.se} PKTS", (f"/sqtt-{e.se}",len(data.ctxs),len(steps)), data=(e.blob,prg_events[k].lib,arch)))
|
||||
try:
|
||||
with Context(DEBUG=0): from extra.sqtt.roc import unpack_occ
|
||||
steps.append(create_step("OCC", ("/amd-sqtt-occ", len(data.ctxs), len(steps)),
|
||||
data={"fxn":unpack_occ, "args":((k, tag), sqtt, prg_events[k], arch)}))
|
||||
except Exception: pass
|
||||
data.ctxs.append({"name":f"SQTT {name}"+(f" n{run_number[k]}" if run_number[k] > 1 else ""), "steps":steps})
|
||||
|
||||
wave_colors = {"WMMA": "#1F7857", **{x:"#ffffc0" for x in ["VALU", "VINTERP"]}, "SALU": "#cef263", "SMEM": "#ffc0c0", "STORE": "#4fa3cc",
|
||||
**{x:"#b2b7c9" for x in ["VMEM", "SGMEM"]}, "LDS": "#9fb4a6", "IMMEDIATE": "#f3b44a", "BARRIER": "#d00000",
|
||||
"JUMP_NO": "#fb8500", "JUMP": "#ffb703", "WAVERDY": "#1a2a2a"}
|
||||
|
||||
def sqtt_timeline(data:bytes, lib:bytes, target:str) -> Generator[ProfileEvent, None, None]:
|
||||
from tinygrad.renderer.amd.sqtt import (map_insts, InstructionInfo, PacketType, INST, InstOp, VALUINST, IMMEDIATE, IMMEDIATE_MASK, VMEMEXEC,
|
||||
ALUEXEC, INST_RDNA4, InstOpRDNA4, TS_DELTA_OR_MARK, TS_DELTA_OR_MARK_RDNA4, CDNA_INST, InstOpCDNA,
|
||||
WAVEEND, WAVEEND_RDNA4, CDNA_WAVEEND, WAVERDY)
|
||||
pc_map = {addr:str(inst) for addr,inst in amd_decode(lib, target).items()}
|
||||
row_ends:dict[str, Decimal] = {}
|
||||
row_counts:dict[str, itertools.count] = {}
|
||||
curr_barrier:dict[int, ProfileRangeEvent] = {}
|
||||
exec_pending:dict[str, list[tuple[str, str]]] = {}
|
||||
dispatch_to_exec = {"WMMA":"VALU", "VALU":"VALU", "VALU1":"VALU", "VALUT":"VALU", "VALUB":"VALU", "VALUINST":"VALU", "VINTERP":"VALU",
|
||||
"SGMEM":"VMEM", "FLAT":"VMEM", "LDS":"LDS", "SALU":"SALU", "SMEM":"SALU", "VMEM":"VMEM"}
|
||||
def add(name:str, p:PacketType, wave:int|None=None, info:InstructionInfo|None=None) -> Generator[ProfileEvent, None, None]:
|
||||
row = f"WAVE:{wave}" if (wave:=getattr(p, "wave", wave)) is not None else f"{p.__class__.__name__}:0 {name.replace('_ALT', '')}"
|
||||
# by default we extend the packet to one cycle after timestamp
|
||||
start_time, end_time = p._time, p._time+1
|
||||
# exec links to dispatch, dispatch links to PC
|
||||
link:dict|None = {"pc":info.pc} if info else None
|
||||
if isinstance(p, (ALUEXEC, VMEMEXEC)):
|
||||
dispatch_id, op_type = exec_pending[name].pop(0)
|
||||
# wmma exec gets its own color and its own row on rdna4
|
||||
if op_type.startswith("WMMA"):
|
||||
name = name+"_WMMA"
|
||||
if not op_type.startswith("WMMA_VALU"): row = "ALUEXEC:0 WMMA"
|
||||
# transcendental valu gets its own row
|
||||
if op_type.startswith("VALUT"): row = "ALUEXEC:0 TFU"
|
||||
# extend execs by the op type's known duration, p._time marks the first or last cycle based on the op type
|
||||
duration = int(dur_match.group(1)) if (dur_match:=re.match(r".*_(\d+)$", op_type)) else 1
|
||||
if any(ss in row for ss in ("SALU", "TFU", "VMEM", "LDS")): start_time, end_time = p._time, p._time+duration
|
||||
else: start_time, end_time = p._time-duration, p._time
|
||||
link = {"link":dispatch_id}
|
||||
# queue inst dispatches
|
||||
idx = next(row_counts.setdefault(row, itertools.count(0)))
|
||||
if isinstance(p, (VALUINST, INST, INST_RDNA4)) and (exec_type:=dispatch_to_exec.get(name.replace("OTHER_", "").split("_")[0])) is not None:
|
||||
if name.startswith("OTHER_"): exec_type = f"{exec_type}_ALT"
|
||||
# detect rdna3 wmma from the asm, only rdna4 has an op type for it
|
||||
if isinstance(p, VALUINST) and (asm:=getattr(unwrap(info).inst, "op_name", "")).startswith("V_WMMA"):
|
||||
name = f"WMMA_VALU_{16 if 'IU4' in asm else 32}"
|
||||
exec_pending.setdefault(exec_type, []).append((f"{row}-{idx}", name))
|
||||
# construct and yield the event for this packet
|
||||
if row not in row_ends: yield ProfilePointEvent(row, "JSON", "pcMap", pc_map, ts=Decimal(0))
|
||||
yield (e:=ProfileRangeEvent(row, TracingKey(name, ret="JSON"+json.dumps(link) if link else None), Decimal(start_time), Decimal(end_time)))
|
||||
row_ends[row] = unwrap(e.en)
|
||||
# barrier on this wave extends to fill the time it was waiting
|
||||
if wave is not None:
|
||||
if (barrier:=curr_barrier.pop(wave, None)) is not None: barrier.en = Decimal(p._time)
|
||||
if name in {"BARRIER", "BARRIER_SIGNAL"}: curr_barrier[wave] = e
|
||||
NS_PER_TICK = 10 # 100MHz
|
||||
prev_pair:tuple[int, int]|None = None # (shader, realtime)
|
||||
yield ProfilePointEvent("", "JSON", "waveColors", list(wave_colors.items()), ts=Decimal(0))
|
||||
for p, info in map_insts(data, lib, target):
|
||||
if isinstance(p, (TS_DELTA_OR_MARK, TS_DELTA_OR_MARK_RDNA4)) and p.is_marker:
|
||||
pair = (p._time, p.delta)
|
||||
if prev_pair is None: prev_pair = pair
|
||||
else:
|
||||
(s0, r0), (s1, r1) = prev_pair, pair
|
||||
freq_hz = (s1 - s0) * 1_000_000_000 // ((r1 - r0) * NS_PER_TICK)
|
||||
yield ProfilePointEvent("LINE:Shader Clock", "freq_hz", freq_hz, ts=Decimal(p._time))
|
||||
prev_pair = pair
|
||||
if isinstance(p, (INST, INST_RDNA4, CDNA_INST)):
|
||||
name = p.op.name if isinstance(p.op, (InstOp, InstOpRDNA4, InstOpCDNA)) else f"0x{p.op:02x}"
|
||||
yield from add(name, p, info=info)
|
||||
if isinstance(p, (VALUINST, IMMEDIATE, WAVEEND, WAVEEND_RDNA4, CDNA_WAVEEND)): yield from add(p.__class__.__name__, p, info=info)
|
||||
if isinstance(p, IMMEDIATE_MASK): yield from add("IMMEDIATE", p, wave=unwrap(info).wave, info=info)
|
||||
if isinstance(p, WAVERDY):
|
||||
for wave in range(16):
|
||||
if p.mask & (1 << wave):
|
||||
if wave in curr_barrier: yield from add("WAVERDY", p, wave=wave)
|
||||
if isinstance(p, (VMEMEXEC, ALUEXEC)):
|
||||
name = str(p.src).split('.')[1]
|
||||
if name == "VALU_SALU":
|
||||
yield from add("VALU", p)
|
||||
yield from add("SALU", p)
|
||||
else:
|
||||
yield from add(name, p)
|
||||
|
||||
def device_sort_fn(k:str) -> tuple:
|
||||
special = {"GC": 0, "USER": 1, "TINY": 2, "ALLDEVS":100, "DISK": 999}
|
||||
is_memory = k.endswith(" Memory")
|
||||
p = k.split(" ")[0].split(":")
|
||||
dev_base = p[0] if len(p) < 2 or not p[1].isdigit() else f"{p[0]}:{p[1]}"
|
||||
return (is_memory, special.get(p[0], special['ALLDEVS']), dev_base, k)
|
||||
|
||||
def get_profile(data:VizData, profile:list[ProfileEvent], sort_fn:Callable[[str], Any]=device_sort_fn) -> bytes|None:
|
||||
# start by getting the time diffs
|
||||
device_ts_diffs:dict[str, Decimal] = {}
|
||||
device_decoders:dict[str, Callable[[VizData, list[ProfileEvent]], None]] = {}
|
||||
for ev in profile:
|
||||
if isinstance(ev, ProfileDeviceEvent):
|
||||
device_ts_diffs[ev.device] = ev.tdiff
|
||||
if (d:=ev.device.split(":")[0]) == "AMD": device_decoders[d] = load_amd_counters
|
||||
if d == "NV": device_decoders[d] = load_nv_counters
|
||||
# load device specific counters
|
||||
for fxn in device_decoders.values(): fxn(data, profile)
|
||||
# map events per device
|
||||
dev_events:dict[str, list[tuple[int, int, float, DevEvent]]] = {}
|
||||
markers:list[ProfilePointEvent] = []
|
||||
ext_data:dict[str, Any] = {}
|
||||
start_ts:int|None = None
|
||||
end_ts:int|None = None
|
||||
for ts,en,e in flatten_events(profile, device_ts_diffs):
|
||||
dev_events.setdefault(e.device,[]).append((st:=int(ts), et:=int(en), float(en-ts), e))
|
||||
if start_ts is None or st < start_ts: start_ts = st
|
||||
if end_ts is None or et > end_ts: end_ts = et
|
||||
if isinstance(e, ProfilePointEvent) and e.name == "marker": markers.append(e)
|
||||
if isinstance(e, ProfilePointEvent) and e.name == "JSON": ext_data[e.key] = e.arg
|
||||
if start_ts is None: return None
|
||||
# return layout of per device events
|
||||
layout:dict[str, bytes|None] = {}
|
||||
scache:dict[str, int] = {}
|
||||
peaks:list[int] = []
|
||||
dtype_size:dict[str, int] = {}
|
||||
for k,v in dev_events.items():
|
||||
v.sort(key=lambda e:e[0])
|
||||
layout[k] = timeline_layout(data, v, start_ts, scache)
|
||||
layout.update([graph_layout(k, v, start_ts, unwrap(end_ts), peaks, dtype_size, scache)])
|
||||
sorted_layout = sorted([k for k,v in layout.items() if v is not None], key=sort_fn)
|
||||
ret = [b"".join([struct.pack("<B", len(k)), k.encode(), unwrap(layout[k])]) for k in sorted_layout]
|
||||
index = json.dumps({"strings":list(scache), "dtypeSize":dtype_size,
|
||||
"markers":[{"ts":rel_ts(e.ts, start_ts, f"marker '{e.arg.get('name','?')}'"), **e.arg} for e in markers],
|
||||
**ext_data}).encode()
|
||||
return struct.pack("<IQII", rel_ts(unwrap(end_ts), start_ts, "end_ts"), max(peaks,default=0), len(index), len(ret))+index+b"".join(ret)
|
||||
|
||||
# ** PMA counters
|
||||
|
||||
def load_nv_counters(data:VizData, profile:list) -> None:
|
||||
steps:list[dict] = []
|
||||
sm_version = {e.device:e.props.get("sm_version", 0x800) for e in profile if isinstance(e, ProfileDeviceEvent) and e.props is not None}
|
||||
run_number:dict[str, int] = {}
|
||||
for e in profile:
|
||||
if type(e).__name__ == "ProfilePMAEvent":
|
||||
run_number[e.kern] = run_num = run_number.get(e.kern, 0)+1
|
||||
steps.append(create_step(f"PMA {e.kern}"+(f"n{run_num}" if run_num>1 else ""), ("/prg-pma-pkts", len(data.ctxs), len(steps)),
|
||||
data=(e.blob, sm_version[e.device])))
|
||||
if steps: data.ctxs.append({"name":"All Counters", "steps":steps})
|
||||
|
||||
def pma_timeline(blob:bytes, sm_version:int) -> list[ProfileEvent]:
|
||||
from extra.nv_pma.decode import decode, decode_tpc_id
|
||||
ret:list[ProfileEvent] = []
|
||||
rows:dict[str, None] = {}
|
||||
tpc_count:dict[int, int] = {}
|
||||
# assume every sample is 32 cycles
|
||||
cycles_per_sample = 32
|
||||
for s, tpc_id in decode(blob, sm_version):
|
||||
if len(ret) > getenv("MAX_SQTT_PKTS", 50_000): break
|
||||
gpc, tpc, sm = decode_tpc_id(tpc_id)
|
||||
tpc_count[tpc_id] = (n:=tpc_count.get(tpc_id,0)) + 1
|
||||
rows.setdefault(row:=f"GPC:{gpc} TPC:{tpc} SM:{sm} WAVE:{s.wave_id}")
|
||||
ret.append(ProfileRangeEvent(row, TracingKey(s.stall_reason.name, ret=f"pc=0x{s.pc_offset:06x} active={s.active}"),
|
||||
Decimal(n*cycles_per_sample), Decimal((n+1)*cycles_per_sample)))
|
||||
return [ProfilePointEvent(r, "start", r, ts=Decimal(0)) for r in rows]+ret
|
||||
|
||||
# ** Assembly static analyzers
|
||||
|
||||
def get_stdout(f: Callable) -> str:
|
||||
buf = io.StringIO()
|
||||
try:
|
||||
with redirect_stdout(buf), redirect_stderr(buf): f()
|
||||
except Exception: traceback.print_exc(file=buf)
|
||||
return buf.getvalue()
|
||||
|
||||
def get_elf_section(lib:bytes, name:str):
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
return next((sh for sh in elf_loader(lib)[1] if sh.name == name))
|
||||
|
||||
def amd_decode(lib:bytes, target:str) -> dict[int, Inst]:
|
||||
text = get_elf_section(lib, ".text")
|
||||
off, buf = text.header.sh_addr, text.content
|
||||
arch = "rdna3" if target.startswith("gfx11") else "rdna4" if target.startswith("gfx12") else "cdna"
|
||||
addr_table:dict[int, Inst] = {}
|
||||
offset = 0
|
||||
while offset < len(buf):
|
||||
remaining = buf[offset:]
|
||||
fmt = detect_format(remaining, arch)
|
||||
decoded = fmt.from_bytes(remaining)
|
||||
addr_table[off+offset] = decoded
|
||||
offset += decoded.size()
|
||||
return addr_table
|
||||
|
||||
def parse_branch(inst) -> int|None:
|
||||
if "branch" in getattr(inst, "op_name", "").lower():
|
||||
x = inst.simm16 & 0xffff
|
||||
return (x - 0x10000 if x & 0x8000 else x)*4
|
||||
return None
|
||||
|
||||
COND_TAKEN, COND_NOT_TAKEN, UNCOND = range(3)
|
||||
def amdgpu_cfg(lib:bytes, target:str) -> dict:
|
||||
# decode
|
||||
pc_table = amd_decode(lib, target)
|
||||
# get leaders
|
||||
leaders:set[int] = {next(iter(pc_table))}
|
||||
for pc, inst in pc_table.items():
|
||||
if (offset:=parse_branch(inst)) is not None: leaders.update((pc+inst.size()+offset, pc+inst.size()))
|
||||
# build the cfg
|
||||
curr:int|None = None
|
||||
blocks:dict[int, list[int]] = {}
|
||||
paths:dict[int, dict[int, int]] = {}
|
||||
for pc, inst in pc_table.items():
|
||||
if pc in leaders:
|
||||
paths[curr:=pc] = {}
|
||||
blocks[pc] = []
|
||||
else: assert curr is not None, f"no basic block found for {pc}"
|
||||
blocks[curr].append(pc)
|
||||
# otherwise a basic block can have exactly one or two paths
|
||||
nx = pc+inst.size()
|
||||
if (offset:=parse_branch(inst)) is not None:
|
||||
if inst.op_name == "S_BRANCH": paths[curr][nx+offset] = UNCOND
|
||||
else: paths[curr].update([(nx+offset, COND_TAKEN), (nx, COND_NOT_TAKEN)])
|
||||
elif nx in leaders: paths[curr][nx] = UNCOND
|
||||
pc_tokens:dict[int, list[dict]] = {}
|
||||
from tinygrad.renderer.amd.dsl import Reg
|
||||
for pc, inst in pc_table.items():
|
||||
pc_tokens[pc] = tokens = []
|
||||
for name, f in inst._fields:
|
||||
if isinstance(val:=getattr(inst, name), Reg): tokens.append({"st":val.fmt(), "keys":[f"r{val.offset+i}" for i in range(val.sz)], "kind":1})
|
||||
elif name in {"op","opx","opy"}: tokens.append({"st":(op_name:=val.name.lower()), "keys":[op_name], "kind":0})
|
||||
elif name != "encoding" and val != f.default: tokens.append({"st":(s:=repr(val)), "keys":[s], "kind":1})
|
||||
# show a smaller view for repeated instructions in the graph
|
||||
lines:list[str] = []
|
||||
disasm = {pc:str(inst) for pc,inst in pc_table.items()}
|
||||
asm_width = max(len(asm) for asm in disasm.values())
|
||||
for pcs in blocks.values():
|
||||
new_pcs:list[int] = []
|
||||
i, n = 0, len(pcs)
|
||||
while i < n:
|
||||
j = i+1
|
||||
while j<n and pc_table[pcs[j]] == pc_table[pcs[i]]: j += 1
|
||||
new_pcs.append(pcs[i])
|
||||
if j-i>1:
|
||||
pc_tokens[pcs[i]].append({"st":f"({j-i}x)", "keys":[], "kind":0})
|
||||
for k in range(i+1, j): del pc_tokens[pcs[k]]
|
||||
lines.append(f"{disasm[pcs[i]]:<{asm_width}} # {pcs[i]:012X}"+(f"...{pcs[j-1]:012X} ({j-i}x)" if j-i>1 else ""))
|
||||
i = j
|
||||
pcs[:] = new_pcs
|
||||
from tinygrad.runtime.autogen import amdgpu_kd
|
||||
kd = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t.from_buffer_copy(bytearray(get_elf_section(lib, ".rodata").content))
|
||||
vgpr_gran = kd.compute_pgm_rsrc1 & amdgpu_kd.COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT
|
||||
return {"data":{"blocks":blocks, "paths":paths, "pc_tokens":pc_tokens}, "src":"\n".join(lines), "lang":"python",
|
||||
"metadata":[[{"label":f"{r} Alloc", "value":v} for r,v in [("VGPR", (vgpr_gran+1)*8-7), ("LDS", kd.group_segment_fixed_size),
|
||||
("Scratch", kd.private_segment_fixed_size)] if v>0]]}
|
||||
|
||||
# ** Main render function to get the complete details about a trace event
|
||||
|
||||
def get_render(viz_data:VizData, query:str) -> dict:
|
||||
url = urlparse(query)
|
||||
i, j, fmt = get_int(qs:=parse_qs(url.query), "ctx"), get_int(qs, "step"), url.path.lstrip("/")
|
||||
data = viz_data.ctxs[i]["steps"][j]["data"]
|
||||
if fmt == "graph-rewrites": return {"value":get_full_rewrite(viz_data, viz_data.trace.rewrites[i][j]), "content_type":"text/event-stream"}
|
||||
if fmt == "uops": return {"src":get_stdout(lambda: print_uops(_reconstruct(viz_data, viz_data.trace.rewrites[i][j-1].sink).src[2].src))}
|
||||
if fmt == "code": return {"src":data, "lang":"cpp"}
|
||||
if fmt == "asm":
|
||||
ret:dict = {}
|
||||
renderer, lib = data
|
||||
if renderer.target.arch.startswith("gfx"):
|
||||
with soft_err(lambda err: ret.update(err)): ret.update(amdgpu_cfg(lib, renderer.target.arch))
|
||||
else: ret["src"] = get_stdout(lambda: renderer.compiler.disassemble(lib))
|
||||
return ret
|
||||
if fmt == "all-pmc":
|
||||
durations, pmc = data
|
||||
ret = {"cols":{}, "rows":[]}
|
||||
for (name, n, k),events in pmc.items():
|
||||
pmc_table = unpack_pmc(events)
|
||||
ret["cols"].update([(r[0], None) for r in pmc_table["rows"]])
|
||||
ret["rows"].append((name, durations[k][n-1], *[r[1] for r in pmc_table["rows"]]))
|
||||
ret["cols"] = ["Kernel", "Duration", *ret["cols"]]
|
||||
return ret
|
||||
if fmt == "prg-pmc": return unpack_pmc(data)
|
||||
if fmt.startswith("sqtt"):
|
||||
ret = {}
|
||||
with soft_err(lambda err:ret.update(err)):
|
||||
if (events:=get_profile(viz_data, list(itertools.islice(sqtt_timeline(*data), getenv("MAX_SQTT_PKTS", 50_000))), sort_fn=row_tuple)):
|
||||
ret = {"value":events, "content_type":"application/octet-stream"}
|
||||
else: ret = {"src":"No SQTT trace on this SE."}
|
||||
return ret
|
||||
# viewers for the amd decoder in extra
|
||||
if fmt.startswith("amd-sqtt"): return data["fxn"](viz_data, i, j, *data["args"])
|
||||
if fmt == "cu-sqtt": return {"value":get_profile(viz_data, data, sort_fn=row_tuple), "content_type":"application/octet-stream"}
|
||||
if fmt == "prg-pma-pkts":
|
||||
ret = {}
|
||||
with soft_err(lambda err:ret.update(err)):
|
||||
if (events:=get_profile(viz_data, pma_timeline(*data), sort_fn=row_tuple)): ret = {"value":events, "content_type":"application/octet-stream"}
|
||||
else: ret = {"src":"No PMA samples found."}
|
||||
return ret
|
||||
return data
|
||||
|
||||
# ** HTTP server
|
||||
|
||||
def get_int(query:dict[str, list[str]], k:str) -> int: return int(query.get(k,["0"])[0])
|
||||
|
||||
class Handler(HTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
ret, status_code, content_type = b"", 200, "text/html"
|
||||
|
||||
if (url:=urlparse(self.path)).path == "/":
|
||||
with open(os.path.join(os.path.dirname(__file__), "index.html"), "rb") as f: ret = f.read()
|
||||
elif self.path.startswith(("/assets/", "/js/")) and '/..' not in self.path:
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(__file__), self.path.strip('/')), "rb") as f: ret = f.read()
|
||||
if url.path.endswith(".js"): content_type = "application/javascript"
|
||||
if url.path.endswith(".css"): content_type = "text/css"
|
||||
except FileNotFoundError: status_code = 404
|
||||
|
||||
elif url.path == "/ctxs":
|
||||
lst = [{"name":c["name"], "steps":[{k:v for k, v in s.items() if k != "data"} for s in c["steps"]]} for c in data.ctxs]
|
||||
ret, content_type = json.dumps(lst).encode(), "application/json"
|
||||
elif url.path == "/get_profile" and profile_ret: ret, content_type = profile_ret, "application/octet-stream"
|
||||
else:
|
||||
if not (render_src:=get_render(data, self.path)): status_code = 404
|
||||
else:
|
||||
if "content_type" in render_src: ret, content_type = render_src["value"], render_src["content_type"]
|
||||
else: ret, content_type = json.dumps(render_src).encode(), "application/json"
|
||||
if content_type == "text/event-stream": return self.stream_json(render_src["value"])
|
||||
|
||||
return self.send_data(ret, content_type, status_code)
|
||||
|
||||
# ** main loop
|
||||
|
||||
def reloader():
|
||||
mtime = os.stat(__file__).st_mtime
|
||||
while not stop_reloader.is_set():
|
||||
if mtime != os.stat(__file__).st_mtime:
|
||||
print("reloading server...")
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
time.sleep(0.1)
|
||||
|
||||
T = TypeVar("T")
|
||||
# unpickling may load libraries, turn off DEBUG=3 output
|
||||
@Context(DEBUG=0)
|
||||
def load_pickle(path:str, default:T) -> T:
|
||||
if not os.path.exists(path): return default
|
||||
with open(path, "rb") as f: return pickle.load(f)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rewrites-path', type=str, help='Path to rewrites', default=temp("rewrites.pkl", append_user=True))
|
||||
parser.add_argument('--profile-path', type=str, help='Path to profile', default=temp("profile.pkl", append_user=True))
|
||||
args = parser.parse_args()
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
if s.connect_ex(((HOST:="http://127.0.0.1").replace("http://", ""), PORT:=getenv("PORT", 8000))) == 0:
|
||||
raise RuntimeError(f"{HOST}:{PORT} is occupied! use PORT= to change.")
|
||||
stop_reloader = threading.Event()
|
||||
multiprocessing.current_process().name = "VizProcess"
|
||||
Context(ALLOW_DEVICE_USAGE=0).__enter__() # disallow opening of devices
|
||||
st = time.perf_counter()
|
||||
print("*** viz is starting")
|
||||
|
||||
data = VizData(load_pickle(args.rewrites_path, default=RewriteTrace([], [], {})))
|
||||
load_rewrites(data)
|
||||
profile_ret = get_profile(data, load_pickle(args.profile_path, default=[]))
|
||||
|
||||
server = TCPServerWithReuse(('', PORT), Handler)
|
||||
reloader_thread = threading.Thread(target=reloader)
|
||||
reloader_thread.start()
|
||||
print(colored(f"*** ready in {(time.perf_counter()-st)*1e3:4.2f}ms", "green"), flush=True)
|
||||
if len(getenv("BROWSER", "")) > 0: webbrowser.open(f"{HOST}:{PORT}")
|
||||
try: server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("*** viz is shutting down...")
|
||||
stop_reloader.set()
|
||||
Reference in New Issue
Block a user