import unittest import functools from dataclasses import dataclass from typing import Callable import numpy as np from tinygrad import Tensor, Device, dtypes from tinygrad.uop.ops import UOp, Ops, KernelInfo from tinygrad.engine.realize import run_linear, estimate_uop, lower_and_compile from tinygrad.renderer import Estimates from tinygrad.dtype import AddrSpace from tinygrad.helpers import getenv, Context from tinygrad.runtime.autogen.amd.rdna3.ins import * import tinygrad.runtime.autogen.amd.rdna3.ins as r3 import tinygrad.runtime.autogen.amd.rdna4.ins as r4 from tinygrad.renderer.amd.dsl import s, v, NULL, Reg, Inst from test.amd.helpers import TARGET_TO_ARCH from extra.gemm.amd_asm_matmul import Kernel # small pattern matcher converting CALL to INS from tinygrad.uop.ops import PatternMatcher, UPat, graph_rewrite, rewrite_group @dataclass(frozen=True) class InstInfo: op: Callable[..., Inst] def __repr__(self): op = self.op if isinstance(op, functools.partial): args = [op.args[0].name.lower(), *map(repr, op.args[1:]), *(f"{k}={v!r}" for k, v in op.keywords.items())] return f"InstInfo({', '.join(args)})" return f"InstInfo({op.__name__})" def __reduce__(self): from test.amd.test_asm_kernel import InstInfo return (InstInfo, (self.op,)) def assemble_inst(call:UOp) -> UOp|None: if not isinstance(call.arg, InstInfo): return None src = [u.without_after for u in call.src[1:] if u.dtype != dtypes.void] regs = [(u.src[0].without_after, len(u.src)) if u.op is Ops.STACK else (u, 1) for u in src] args = [Reg(u.arg.slot + (256 if u.arg.name == "v" else 0), size) for u, size in regs] return UOp(Ops.INS, src=call.src[1:], arg=(call.arg.op(*args), dtypes.void)) assemble_sink_pm = PatternMatcher([(UPat(Ops.CALL, name="call"), assemble_inst),]) @rewrite_group("call_to_ins") def call_to_ins(sink:UOp) -> UOp: ins = graph_rewrite(sink, assemble_sink_pm, name="convert CALL to INS") lin = UOp(Ops.LINEAR, src=tuple(u for u in ins.toposort() if u.op is Ops.INS)) return UOp(Ops.PROGRAM, src=(sink.replace(src=tuple(u for u in sink.src if u.op not in {Ops.CALL, Ops.AFTER})), lin)) def custom_add_one(A:UOp) -> UOp: A = A.flatten() assert dtypes.is_float(A.dtype), f"buffer dtype must be float32, got {A.dtype}" threads = UOp.special(A.numel(), "lidx0") # SGPRs have shape (1,) dest = tuple(UOp.param(i, dtypes.int32, (1,), addrspace=AddrSpace.REG, name="s") for i in range(2)) # use STACK for SGPR pairs, s[0:1] has shape (2, 1) kernarg = UOp.stack(*dest) # NOTE: this call body doesn't implement the instruction, this exists in the emulator kernarg_load = UOp(Ops.CALL, src=(UOp.sink(), UOp.stack(*dest), kernarg), arg=InstInfo(functools.partial(s_load_b64, soffset=NULL))) kernarg_wait = UOp(Ops.CALL, src=(UOp.sink(), kernarg_load), arg=InstInfo(functools.partial(s_waitcnt_lgkmcnt, sdst=NULL, simm16=0))) saddr_after = UOp.stack(*(d.after(kernarg_wait) for d in dest)) # VPGRs have shape (32,) offset_val = UOp.param(0, dtypes.int32, (32,), addrspace=AddrSpace.REG, name="v") offset_call = UOp(Ops.CALL, src=(UOp.sink(), offset_val, offset_val), arg=InstInfo(functools.partial(v_lshlrev_b32_e32, src0=2))) offset_after = offset_val.after(offset_call) val = UOp.param(1, dtypes.float32, (32,), addrspace=AddrSpace.REG, name="v") load_call = UOp(Ops.CALL, src=(UOp.sink(), val, offset_after, offset_val, saddr_after), arg=InstInfo(global_load_b32)) wait_call = UOp(Ops.CALL, src=(UOp.sink(), load_call), arg=InstInfo(functools.partial(s_waitcnt_vmcnt, sdst=NULL, simm16=0))) c1_dest = UOp.param(2, dtypes.float32, (32,), addrspace=AddrSpace.REG, name="v") mov_call = UOp(Ops.CALL, src=(UOp.sink(), c1_dest), arg=InstInfo(functools.partial(v_mov_b32_e32, src0=1.0))) add_dest = val add_after = add_dest.after(UOp(Ops.CALL, src=(UOp.sink(), add_dest, val.after(wait_call), c1_dest.after(mov_call)), arg=InstInfo(v_add_f32_e32))) store_to_global = UOp(Ops.CALL, src=(UOp.sink(), offset_val, offset_after, add_after, saddr_after), arg=InstInfo(global_store_b32)) end_call = UOp(Ops.CALL, src=(UOp.sink(), store_to_global), arg=InstInfo(s_endpgm)) sink = UOp.sink(A.base, threads, end_call, arg=KernelInfo(f"custom_add_one_{A.numel()}", estimates=Estimates(ops=A.numel(), mem=A.numel()*4*2))) return call_to_ins(sink) def custom_add_var(A:UOp, B:UOp) -> UOp: A,B = A.flatten(), B.flatten() assert A.dtype == dtypes.uint32, f"buffer dtype must be uint32, got {A.dtype}" threads = UOp.special(A.numel(), "lidx0") var = UOp.param(2, dtypes.int, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU) insts = [ s_load_b128(s[4:7], s[0:1]), s_load_b32(s[8], s[0:1], offset=0x10), # all threads load the same variable s_waitcnt_lgkmcnt(sdst=NULL, simm16=0), v_lshlrev_b32_e32(v[0], 2, v[0]), # element offset, different per thread global_load_b32(v[1], v[0], saddr=s[6:7]), s_waitcnt_vmcnt(sdst=NULL, simm16=0), v_add_nc_u32_e32(v[1], s[8], v[1]), global_store_b32(addr=v[0], data=v[1], saddr=s[4:5]), s_endpgm(), ] sink = UOp.sink(A.base, B.base, var, threads, arg=KernelInfo(f"custom_add_var_{A.numel()}")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) def custom_wave_sync(A:UOp, arch:str) -> UOp: # 4 waves across 1024 WG — enough to saturate a SIMD with many concurrent WGs # s_sleep yields the SIMD so waves from different WGs interleave, causing barrier packet reordering threads = UOp.special(128, "lidx0") wg = UOp.special(1024, "gidx0") insts = [] for _ in range(4): insts.append(s_sleep(4)) insts += [s_barrier()] if arch == "rdna3" else [r4.s_barrier_signal(), r4.s_barrier_wait()] insts += [s_nop(0)]*4 insts.append(s_endpgm()) sink = UOp.sink(A.base, threads, wg, arg=KernelInfo("custom_wave_sync")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) def custom_lds_sync(A:UOp, arch:str) -> UOp: A = A.flatten() num_threads = A.shape[0] threads = UOp.special(num_threads, "lidx0") wg = UOp.special(1, "gidx0") lds = UOp.placeholder((512,), dtypes.uint8, 0, AddrSpace.LOCAL) # 128 * 4 bytes isa = r4 if arch == "rdna4" else r3 wait_kmcnt = [isa.s_wait_kmcnt(simm16=0)] if arch == "rdna4" else [isa.s_waitcnt_lgkmcnt(sdst=NULL, simm16=0)] wait_dscnt = [isa.s_wait_dscnt(simm16=0)] if arch == "rdna4" else [isa.s_waitcnt_lgkmcnt(sdst=NULL, simm16=0)] barrier = [isa.s_barrier_signal(ssrc0=-1), isa.s_barrier_wait(simm16=-1)] if arch == "rdna4" else [isa.s_barrier()] global_store = [isa.global_store_b32(vaddr=v[6:7], saddr=s[0:1], vsrc=v[5])] if arch == "rdna4" \ else [isa.global_store_b32(addr=v[6], data=v[5], saddr=s[0:1])] insts = [ isa.s_load_b64(s[0:1], s[0:1], soffset=NULL), *wait_kmcnt, isa.v_lshlrev_b32_e32(v[1], 2, v[0]), # lds[thread_idx] = thread_idx isa.ds_store_b32(addr=v[1], data0=v[0]), *wait_dscnt, *barrier, # out[threaed_idx] = thread_idx == num_threads ? -1 : lds[thread_idx + 1] isa.v_add_nc_u32_e32(v[2], 4, v[1]), isa.v_cmp_gt_u32_e32(num_threads-1, v[0]), isa.ds_load_b32(vdst=v[3], addr=v[2]), *wait_dscnt, isa.v_mov_b32_e32(v[4], -1), isa.v_cndmask_b32_e32(v[5], v[4], v[3]), isa.v_lshlrev_b32_e32(v[6], 2, v[0]), *global_store, isa.s_endpgm(), ] sink = UOp.sink(A.base, lds, threads, wg, arg=KernelInfo("custom_lds_sync")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) def custom_handwritten(A:UOp) -> UOp: A = A.flatten() threads = UOp.special(128, "lidx0") wg = UOp.special(1, "gidx0") lds = UOp.placeholder((512,), dtypes.uint8, 0, AddrSpace.LOCAL) # 128 * 4 bytes pipes = {getenv("PIPE", "")} if getenv("PIPE", "") else {"SALU", "VALU", "TRANSCENDENTAL", "WMMA"} k = Kernel() # wrap in loop to filter out icache misses LOOP_N, UNROLL_N = 8, 5 k.emit(r4.s_mov_b32(s[1], LOOP_N)) k.label("loop") if "SALU" in pipes: for i in range(UNROLL_N): k.emit(r4.s_mov_b32(s[20+i], i)) k.emit(r4.s_min_i32(s[30+i], i)) k.emit(r4.s_mov_b32(s[40+i], i)) k.emit(r4.s_mul_i32(s[14+i], s[12+i], 32)) if "VALU" in pipes: for i in range(UNROLL_N): k.emit(r4.v_mov_b32_e32(v[20+i], i)) k.emit(r4.v_lshlrev_b64_e32(v[30+2*i:31+2*i], 2, v[12+i:13+i])) k.emit(r4.v_mad_co_u64_u32(v[40+2*i:41+2*i], NULL, v[12+i], v[13+i], v[14+i:15+i])) if "TRANSCENDENTAL" in pipes: # transcendental VALU runs on the TFU, it can run regular VALU at the same time for i in range(UNROLL_N): k.emit(r4.v_mov_b32_e32(v[20+i], i)) k.emit(r4.v_s_rcp_f32(s[20+i], s[12+i])) k.emit(r4.v_rcp_f32_e32(v[30+i], v[12+i])) k.emit(r4.v_s_exp_f32(s[30+i], s[12+i])) if "WMMA" in pipes: base = 30 for i in range(UNROLL_N): a = base + i*40 b, cd = a + 4, a + 8 k.emit(r4.v_wmma_f32_16x16x16_f16(v[cd:cd+7], v[a:a+3], v[b:b+3], v[cd:cd+7])) a = base + i*40 + 16 b, cd = a + 2, a + 4 k.emit(r4.v_wmma_i32_16x16x16_iu8(v[cd:cd+7], v[a:a+1], v[b:b+1], v[cd:cd+7])) k.emit(r4.s_add_co_i32(s[1], s[1], -1)) k.emit(r4.s_cmp_eq_i32(s[1], 0)) k.emit(r4.s_cbranch_scc0(), target="loop") k.emit(r4.s_endpgm()) insts = k.finalize() sink = UOp.sink(A.base, threads, wg, lds, arg=KernelInfo("custom_handwritten")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) def custom_data_deps(A:UOp) -> UOp: A = A.flatten() threads = UOp.special(A.numel(), "lidx0") k = Kernel() k.emit(s_load_b64(s[0:1], s[0:1], soffset=NULL)) k.emit(s_waitcnt_lgkmcnt(sdst=NULL, simm16=0)) k.emit(v_lshlrev_b32_e32(v[0], 2, v[0])) k.emit(global_load_b32(v[1], v[0], saddr=s[0:1])) k.emit(s_waitcnt_vmcnt(sdst=NULL, simm16=0)) k.emit(v_add_f32_e32(v[1], 1.0, v[1])) k.emit(global_store_b32(addr=v[0], data=v[1], saddr=s[0:1])) k.emit(s_endpgm()) insts = k.finalize() sink = UOp.sink(A.base, threads, arg=KernelInfo("custom_data_deps")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) # import contextvar to use it from test.mockgpu.amd.emu import ASM_CALL # noqa: F401 @unittest.skipUnless(Device.DEFAULT == "AMD", "requires AMD device") class TestAsmKernel(unittest.TestCase): def setUp(self): self.arch = TARGET_TO_ARCH[Device["AMD"].arch] self.enterContext(Context(ASM_CALL=1)) def test_simple(self): if self.arch != "rdna3": self.skipTest("only rdna3") a = Tensor.full((16, 16), 1.).contiguous().realize() a = Tensor.custom_kernel(a, fxn=custom_add_one)[0] linear = lower_and_compile(a.schedule_linear()) est = estimate_uop(linear.src[-1]) self.assertEqual(est.ops, a.numel()) self.assertEqual(est.mem, a.nbytes()*2) run_linear(linear) self.assertTrue((a.numpy() == 2.).all()) def test_variable(self): if self.arch != "rdna3": self.skipTest("only rdna3") b = Tensor.full((16, 16), 1, dtype=dtypes.uint32).contiguous().realize() a = Tensor.zeros_like(b).contiguous().realize() a = Tensor.custom_kernel(a, b, fxn=custom_add_var)[0] linear = a.schedule_linear() for i in range(4): run_linear(linear, var_vals={"var":i}) self.assertTrue((a.numpy() == 1+i).all()) def test_lds_sync(self): if self.arch not in ("rdna3", "rdna4"): self.skipTest("only rdna3/rdna4") a = Tensor.empty(128, dtype=dtypes.int32).contiguous().realize() a = Tensor.custom_kernel(a, fxn=functools.partial(custom_lds_sync, arch=self.arch))[0] a.realize() ref = Tensor.arange(1, 129, dtype=dtypes.int32) ref[127] = -1 self.assertListEqual(a.tolist(), ref.tolist()) def test_handwritten(self): if self.arch != "rdna4": self.skipTest("only tested on rdna4") a = Tensor.empty(1024, dtype=dtypes.int32).contiguous().realize() a = Tensor.custom_kernel(a, fxn=custom_handwritten)[0] a.realize() def test_data_deps(self): if self.arch != "rdna3": self.skipTest("only tested on rdna3") a = Tensor(np.full(32, 5.0, dtype=np.float32)).realize() a = Tensor.custom_kernel(a, fxn=custom_data_deps)[0] a.realize() self.assertTrue((a.numpy() == 6.0).all()) @unittest.expectedFailure def test_cfg_branch_diamond(self): def cfg_kernel(out:UOp): k = Kernel() k.emit(s_load_b64(s[0:1], s[0:1], soffset=NULL)) k.emit(s_waitcnt_lgkmcnt(sdst=NULL, simm16=0)) for i in range(4): k.emit(v_mov_b32_e32(v[i], float(i))) for i in range(2): k.emit(s_cmp_eq_i32(0, 1-i)) k.emit(s_cbranch_scc0(), target=f"branch_{i}") k.emit(global_store_b32(addr=v[0], data=v[1], saddr=s[0:1], offset=i*4)) k.emit(s_branch(), target=f"after_branch_{i}") k.label(f"branch_{i}") k.emit(global_store_b32(addr=v[0], data=v[2], saddr=s[0:1], offset=i*4)) k.label(f"after_branch_{i}") k.emit(s_branch(), target="final") k.label("final") k.emit(global_store_b32(addr=v[0], data=v[3], saddr=s[0:1], offset=8)) k.emit(s_endpgm()) insts = k.finalize() sink = UOp.sink(out.base, arg=KernelInfo("cfg_kernel")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) out = Tensor.empty(3).custom_kernel(fxn=cfg_kernel)[0] self.assertListEqual(out.tolist(), [2.0, 1.0, 3.0]) def test_cfg_loop(self): def cfg_kernel(out:UOp): k = Kernel() k.emit(s_load_b64(s[0:1], s[0:1], soffset=NULL)) k.emit(s_waitcnt_lgkmcnt(sdst=NULL, simm16=0)) k.emit(s_mov_b32(s[2], 0)) k.label("loop") k.emit(s_add_u32(s[2], s[2], 1)) k.emit(s_cmp_lt_i32(s[2], 4)) k.emit(s_cbranch_scc1(), target="loop") k.emit(v_mov_b32_e32(v[0], 0)) k.emit(v_mov_b32_e32(v[1], s[2])) k.emit(global_store_b32(addr=v[0], data=v[1], saddr=s[0:1])) k.emit(s_endpgm()) insts = k.finalize() sink = UOp.sink(out.base, arg=KernelInfo("cfg_loop_kernel")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts])))) out = Tensor.empty(1, dtype=dtypes.int).custom_kernel(fxn=cfg_kernel)[0] self.assertListEqual(out.tolist(), [4]) def test_store_offset2(self): if self.arch != "rdna3": self.skipTest("only rdna3") def store_kernel(out:UOp): insts = [ s_load_b64(s[0:1], s[0:1], soffset=NULL), s_waitcnt_lgkmcnt(sdst=NULL, simm16=0), v_mov_b32_e32(v[0], 2), v_mov_b32_e32(v[1], 0x12345678), global_store_b32(addr=v[0], data=v[1], saddr=s[0:1]), s_endpgm(), ] sink = UOp.sink(out.base, UOp.special(1, "lidx0"), arg=KernelInfo("unaligned_global_store")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts)))) out = Tensor.full((8,), 0xA5, dtype=dtypes.uint8).contiguous().realize() out = out.custom_kernel(fxn=store_kernel)[0] self.assertListEqual(out.tolist(), [0xA5, 0xA5, 0x78, 0x56, 0x34, 0x12, 0xA5, 0xA5]) def test_readlane(self): if self.arch != "rdna3": self.skipTest("only rdna3") def readlane_kernel(out:UOp): insts = [ s_load_b64(s[0:1], s[0:1], soffset=NULL), s_waitcnt_lgkmcnt(sdst=NULL, simm16=0), v_readfirstlane_b32_e32(s[2], v[0]), s_branch(simm16=0), v_mov_b32_e32(v[1], s[2]), v_lshlrev_b32_e32(v[0], 2, v[0]), global_store_b32(addr=v[0], data=v[1], saddr=s[0:1]), s_endpgm(), ] sink = UOp.sink(out.base, UOp.special(64, "lidx0"), arg=KernelInfo("wave_sgprs")) return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts)))) out = Tensor(np.full(64, 0xFFFFFFFF, dtype=np.uint32)).realize().custom_kernel(fxn=readlane_kernel)[0] self.assertListEqual(out.tolist(), [0]*32 + [32]*32) def test_plus_tensor(self): out = Tensor.arange(1, 4).clone() + Tensor.arange(4, 7).clone() self.assertListEqual(out.tolist(), [5, 7, 9]) def test_gemm_tensor(self): N = 64 a = Tensor.ones(N,N, dtype=dtypes.float).contiguous() b = Tensor.eye(N, dtype=dtypes.float).clone() out = a@b self.assertEqual(out.tolist(), (a.numpy()@b.numpy()).tolist()) if __name__ == "__main__": unittest.main()