Compare commits

...
Author SHA1 Message Date
geohot cbf41054a8 mypy 2026-08-21 17:10:42 -07:00
geohot a440a759da refactor the AMD emulator slop (kimi) 2026-08-21 16:26:33 -07:00
3 changed files with 539 additions and 755 deletions
+389 -735
View File
File diff suppressed because it is too large Load Diff
+50 -20
View File
@@ -1,5 +1,20 @@
# Tokenizer-based expression parser for AMD pcode
import ast, itertools, operator, re
from typing import Any, Callable
_BINOPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.LShift: operator.lshift, ast.RShift: operator.rshift,
ast.BitAnd: operator.and_, ast.BitOr: operator.or_, ast.BitXor: operator.xor}
def _const_int(expr: str) -> int:
"""Evaluate a compile-time integer expression (integer literals and basic arithmetic only)."""
def ev(node: ast.AST) -> int:
if isinstance(node, ast.Expression): return ev(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, int): return node.value
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
return (-1 if isinstance(node.op, ast.USub) else 1) * ev(node.operand)
if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS: return _BINOPS[type(node.op)](ev(node.left), ev(node.right))
raise ValueError(f"not a constant integer expression: {expr!r}")
return ev(ast.parse(expr.strip(), mode='eval'))
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import Ops, UOp
from tinygrad.codegen.decomp.dtype import f2f
@@ -360,22 +375,13 @@ _FUNCS: dict[str, Callable[..., UOp]] = {
'fp8_to_f32': _fp8_to_f32, 'bf8_to_f32': _bf8_to_f32, 'f32_to_fp8': _f32_to_fp8, 'f32_to_bf8': _f32_to_bf8,
'f32_to_bf16': _f32_to_bf16, 'f32_to_bf16_SR': _f32_to_bf16_sr, 'f32_to_bf16_sr': _f32_to_bf16_sr,
}
for is_max, name in [(False, 'min'), (True, 'max')]:
for dt, sfx in [(dtypes.float32, 'f32'), (dtypes.int, 'i32'), (dtypes.uint32, 'u32'), (dtypes.int16, 'i16'), (dtypes.uint16, 'u16')]:
_FUNCS[f'v_{name}_{sfx}'] = lambda *a, im=is_max, d=dt: _minmax_reduce(im, d, *a)
_FUNCS[f'v_{name}3_{sfx}'] = lambda *a, im=is_max, d=dt: _minmax_reduce(im, d, *a)
# f16 min/max/min3/max3/med3
for is_max, name in [(False, 'min'), (True, 'max')]:
_FUNCS[f'v_{name}_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}3_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}_num_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}_num_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
_FUNCS[f'v_{name}3_num_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}3_num_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
_FUNCS[f'v_{name}imum_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}imum_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
_FUNCS[f'v_{name}imum3_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
_FUNCS[f'v_{name}imum3_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
# min/max family: min/max + 3-input (x3), IEEE num variants (f16/f32 only), and long names minimum/maximum (f16/f32 only)
for is_max, name, full in [(False, 'min', 'minimum'), (True, 'max', 'maximum')]:
for dt, sfx, pre in [(dtypes.float32, 'f32', None), (dtypes.int, 'i32', None), (dtypes.uint32, 'u32', None),
(dtypes.int16, 'i16', None), (dtypes.uint16, 'u16', None), (dtypes.half, 'f16', _f16_extract)]:
def mm(*a, im=is_max, d=dt, p=pre): return _minmax_reduce(im, d, *(a if p is None else [p(x) for x in a]))
extra = (f'v_{name}_num_{sfx}', f'v_{name}3_num_{sfx}', f'v_{full}_{sfx}', f'v_{full}3_{sfx}') if dt in (dtypes.float32, dtypes.half) else ()
for fn in (f'v_{name}_{sfx}', f'v_{name}3_{sfx}', *extra): _FUNCS[fn] = mm
# ═══════════════════════════════════════════════════════════════════════════════
# TOKENIZER/PARSER
@@ -890,6 +896,8 @@ class Parser:
return result & _isnan(l).logical_not() & _isnan(r).logical_not()
return result
_break_var_ids = itertools.count() # unique names for per-loop break-tracking variables
def _match_bracket(toks: list[Token], start: int) -> tuple[int, list[Token]]:
"""Match brackets from start, return (end_idx, inner_tokens)."""
j, depth = start + 1, 1
@@ -987,7 +995,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
i += 1
# Execute loop with break support
has_break = any('break' in bl.lower() for bl in body_lines)
found_var = f'_found_{id(body_lines)}' if has_break else None
found_var = f'_found_{next(_break_var_ids)}' if has_break else None
if found_var: env[found_var] = block_assigns[found_var] = _const(dtypes.bool, False)
for loop_i in range(start_val, end_val + 1):
subst_lines = [_subst_loop_var(bl, loop_var, loop_i) for bl in body_lines if not (has_break and bl.strip().lower() == 'break')]
@@ -1087,7 +1095,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
j, slice_toks = _match_bracket(toks, j)
slice_str = _tok_str(slice_toks)
hi_str, lo_str = slice_str.split(':')
hi_val, lo_val = int(eval(hi_str.strip())), int(eval(lo_str.strip()))
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
if j < len(toks) and toks[j].type == 'DOT': j += 2 # skip .type suffix
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
ln = parse_tokens(lane_toks, env, funcs)
@@ -1145,7 +1153,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
hi_str = ' '.join(t.val for t in toks[bracket_start:colon_pos] if t.type != 'EOF')
lo_str = ' '.join(t.val for t in toks[colon_pos+1:j] if t.type != 'EOF')
try:
hi_val, lo_val = int(eval(hi_str)), int(eval(lo_str))
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
hi, lo = max(hi_val, lo_val), min(hi_val, lo_val)
j += 1
if j < len(toks) and toks[j].type == 'DOT': j += 2
@@ -1159,7 +1167,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
block_assigns[var] = env[var] = _set_bits(old, _val_to_bits(val), hi - lo + 1, lo)
i += 1
continue
except Exception: pass
except (ValueError, SyntaxError): pass # non-constant slice bounds - fall through to other statement forms
elif toks[1].type == 'LBRACKET': # bit index: var[expr] (only for var[...], not var.type[...])
existing = block_assigns.get(var, env.get(var))
if existing is not None and isinstance(existing, UOp) and \
@@ -1360,3 +1368,25 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
def parse_expr(expr: str, env: dict[str, VarVal], funcs: dict | None = None) -> UOp:
return parse_tokens(tokenize(expr.strip().rstrip(';')), env, funcs)
def parse_pcode(pcode: str, srcs: dict[str, UOp | int] | None = None) -> tuple[dict, list]:
env: dict = srcs.copy() if srcs else {}
assigns: list[tuple[str, UOp]] = []
raw_lines = [l.strip().rstrip(';') for l in pcode.split('\n') if l.strip() and not l.strip().startswith('//')]
# TODO: pcode.py should tokenize full pcode string instead of line-by-line, then this hack can be removed
lines: list[str] = []
for l in raw_lines:
if lines and re.search(r'(&&|\|\||[&|+\-*/^])\s*$', lines[-1]): lines[-1] = lines[-1] + ' ' + l
else: lines.append(l)
_, final, _ = parse_block(lines, 0, env, assigns=assigns)
sliced = set(d.split('[')[0] for d, _ in assigns if '[' in d)
for var, val in final.items():
if var in ['D0', 'S0', 'SCC', 'VCC', 'EXEC', 'PC', 'RETURN_DATA', 'VDATA'] and isinstance(val, UOp):
if var in sliced and not any(re.match(rf'{var}\.\w+\s*=', l) for l in lines): continue
for l in lines:
if (m := re.match(rf'{var}\.(\w+(?:\[\w+\])?)', l)):
assigns.append((f'{var}.{m.group(1)}', val))
break
else: assigns.append((var, val))
return env, assigns
+100
View File
@@ -0,0 +1,100 @@
# SQTT trace encoder for the emulator (the decoder lives in tinygrad/renderer/amd/sqtt.py).
# run_asm emits packets inline as instructions execute; finished traces end up in emu.sqtt_traces.
from __future__ import annotations
from tinygrad.renderer.amd.dsl import Inst
from tinygrad.renderer.amd.sqtt import (_build_decode_tables, PACKET_TYPES_RDNA3, PacketType, InstOp,
LAYOUT_HEADER, WAVESTART, WAVEEND, INST, IMMEDIATE, VALUINST)
_NIB_COUNTS = {cls: nc for _, (cls, nc, *_) in _build_decode_tables(PACKET_TYPES_RDNA3)[0].items()}
def _emit_nibbles(nibbles: list[int], pkt_cls: type[PacketType], **kwargs):
raw = pkt_cls.encoding.default
for k, v in kwargs.items(): raw = pkt_cls.__dict__[k].set(raw, v)
nibbles.extend((raw >> (i * 4)) & 0xF for i in range(_NIB_COUNTS[pkt_cls]))
def make_encoder():
"""Build an SQTT trace encoder for the emulator. Returns (emit, finish, finalize)."""
from tinygrad.runtime.autogen.amd.rdna3.enum import SOPPOp as SOPPOp3
from tinygrad.runtime.autogen.amd.rdna4.enum import SOPPOp as SOPPOp4
from tinygrad.runtime.autogen.amd.rdna3 import ins as ir3
from tinygrad.runtime.autogen.amd.rdna4 import ins as ir4
from tinygrad.runtime.autogen.amd.cdna import ins as irc
import re
def _kinds(*names: str) -> tuple[type[Inst], ...]:
return tuple(getattr(m, n) for m in (ir3, ir4, irc) for n in names if hasattr(m, n))
_SOPP, _SMEM, _DS = _kinds('SOPP'), _kinds('SMEM'), _kinds('DS')
_GLOBAL, _FLAT, _SCRATCH = _kinds('GLOBAL', 'VGLOBAL'), _kinds('FLAT', 'VFLAT'), _kinds('SCRATCH', 'VSCRATCH')
_VALU = _kinds('VOP1', 'VOP2', 'VOP3', 'VOP3P', 'VOP3PX2', 'VOPC', 'VOPD', 'VOP3SD', 'VOP3_SDST', 'VOP1_SDST')
# SOPP classification sets
_SOPP_SKIP = {SOPPOp3.S_ENDPGM.value, SOPPOp3.S_ENDPGM_SAVED.value, SOPPOp3.S_ENDPGM_ORDERED_PS_DONE.value, SOPPOp3.S_DELAY_ALU.value}
_SOPP_IMMEDIATE = {SOPPOp3.S_NOP.value, SOPPOp3.S_CLAUSE.value, SOPPOp3.S_WAITCNT.value, SOPPOp3.S_WAITCNT_DEPCTR.value,
SOPPOp3.S_WAIT_IDLE.value, SOPPOp3.S_WAIT_EVENT.value, SOPPOp3.S_SLEEP.value, SOPPOp3.S_SET_INST_PREFETCH_DISTANCE.value}
for _op in (SOPPOp4.S_WAIT_ALU, SOPPOp4.S_WAIT_LOADCNT, SOPPOp4.S_WAIT_STORECNT, SOPPOp4.S_WAIT_SAMPLECNT,
SOPPOp4.S_WAIT_BVHCNT, SOPPOp4.S_WAIT_EXPCNT, SOPPOp4.S_WAIT_DSCNT, SOPPOp4.S_WAIT_KMCNT,
SOPPOp4.S_WAIT_LOADCNT_DSCNT, SOPPOp4.S_WAIT_STORECNT_DSCNT):
_SOPP_IMMEDIATE.add(_op.value)
_SOPP_BARRIER = {SOPPOp3.S_BARRIER.value}
if hasattr(SOPPOp4, 'S_BARRIER_WAIT'): _SOPP_BARRIER.add(SOPPOp4.S_BARRIER_WAIT.value)
if hasattr(SOPPOp4, 'S_BARRIER_LEAVE'): _SOPP_BARRIER.add(SOPPOp4.S_BARRIER_LEAVE.value)
_SOPP_BRANCH = {SOPPOp3.S_BRANCH.value, SOPPOp3.S_CBRANCH_SCC0.value, SOPPOp3.S_CBRANCH_SCC1.value,
SOPPOp3.S_CBRANCH_VCCZ.value, SOPPOp3.S_CBRANCH_VCCNZ.value,
SOPPOp3.S_CBRANCH_EXECZ.value, SOPPOp3.S_CBRANCH_EXECNZ.value}
# VALU sub-classification patterns
_VALUT_4_RE = re.compile(r'V_(EXP|LOG|RCP|RSQ|SQRT|SIN|COS|CEIL|FLOOR|TRUNC|RNDNE|FRACT|FREXP)_')
_VALUB_2_RE = re.compile(r'V_(LSHLREV|LSHRREV|ASHRREV)_(B|I)64')
_VALUB_4_RE = re.compile(r'V_MAD_(U|I)64')
_VALUB_16_RE = re.compile(r'V_\w+_F64')
def _valu_op(op_name: str) -> InstOp|None:
if 'CMPX' in op_name: return InstOp.VALU1_WR_EXEC
if _VALUB_2_RE.search(op_name): return InstOp.VALUB_2
if _VALUB_4_RE.search(op_name): return InstOp.VALUB_4
if _VALUB_16_RE.search(op_name): return InstOp.VALUB_16
if _VALUT_4_RE.search(op_name): return InstOp.VALUT_4
return None
def _mem_op(t: type[Inst], op_name: str) -> InstOp:
is_store = "STORE" in op_name
if issubclass(t, _DS): return InstOp.LDS_WR_2 if is_store else InstOp.LDS_RD
if issubclass(t, _GLOBAL): return InstOp.SGMEM_WR_2 if is_store else InstOp.SGMEM_RD_1
if issubclass(t, _FLAT) or issubclass(t, _SCRATCH): return InstOp.FLAT_WR_3 if is_store else InstOp.FLAT_RD_2
return InstOp.SALU
nibbles: list[int] = []
started: set[int] = set()
_emit_nibbles(nibbles, LAYOUT_HEADER, layout=3, sel_a=6)
def emit(wave_id: int, inst: Inst, branch_taken: bool|None):
"""Emit an SQTT packet for one executed instruction."""
w = wave_id & 0x1F
if wave_id not in started:
_emit_nibbles(nibbles, WAVESTART, delta=1, simd=0, wgp=0, wave=w, id7=wave_id)
started.add(wave_id)
inst_type, inst_op, op_name = type(inst), inst.op.value if hasattr(inst, 'op') else 0, inst.op.name if hasattr(inst, 'op') else ""
if issubclass(inst_type, _SOPP):
if inst_op in _SOPP_SKIP: return
if inst_op in _SOPP_IMMEDIATE: _emit_nibbles(nibbles, IMMEDIATE, delta=1, wave=w)
elif inst_op in _SOPP_BARRIER: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.BARRIER)
elif inst_op in _SOPP_BRANCH: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.JUMP if branch_taken else InstOp.JUMP_NO)
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.SALU)
elif issubclass(inst_type, _VALU):
if (op := _valu_op(op_name)) is None: _emit_nibbles(nibbles, VALUINST, delta=1, wave=w)
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=op)
elif issubclass(inst_type, _SMEM): _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.SMEM_RD)
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=_mem_op(inst_type, op_name))
def finish(wave_id: int):
"""Emit WAVEEND for a completed wave."""
if wave_id in started: _emit_nibbles(nibbles, WAVEEND, delta=1, simd=0, wgp=0, wave=wave_id & 0x1F)
def finalize() -> bytes:
"""Pad and return the encoded SQTT blob."""
while len(nibbles) % 2 != 0: nibbles.append(0)
nibbles.extend([0] * 32)
while len(nibbles) % 64 != 0: nibbles.append(0)
return bytes(nibbles[i] | ((nibbles[i + 1] if i + 1 < len(nibbles) else 0) << 4) for i in range(0, len(nibbles), 2))
return emit, finish, finalize