mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-24 14:06:06 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b960a62e2 | ||
|
|
20a3783d14 | ||
|
|
ea358fdca6 | ||
|
|
bf894a8285 | ||
|
|
2de0f6a9aa | ||
|
|
0645e48de3 | ||
|
|
8239301df0 | ||
|
|
fa02105546 | ||
|
|
057dc173ab | ||
|
|
0ff30b003d | ||
|
|
48a7627b04 | ||
|
|
6837881b06 | ||
|
|
d08c76d9cb | ||
|
|
742b3894d7 | ||
|
|
4cf2759fc8 |
@@ -538,6 +538,8 @@ jobs:
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: Test GPU crash recovery
|
||||
run: DEV=AMD python3 -m pytest -rA test/external/external_test_gpu_crash.py
|
||||
- name: Train MNIST
|
||||
run: time PYTHONPATH=. DEV=AMD TARGET_EVAL_ACC_PCT=96.0 python3 examples/beautiful_mnist.py
|
||||
- name: Run 10 CIFAR training steps
|
||||
@@ -709,6 +711,8 @@ jobs:
|
||||
run: time DEBUG=3 DEV=AMD AM_RESET=1 python3 test/test_tiny.py TestTiny.test_plus
|
||||
- name: Test driver warm start time
|
||||
run: time DEBUG=3 DEV=AMD python3 test/test_tiny.py TestTiny.test_plus
|
||||
- name: Test GPU crash recovery
|
||||
run: DEV=AMD python3 -m pytest -rA test/external/external_test_gpu_crash.py
|
||||
# Fails on 9070
|
||||
# - name: Test tensor cores
|
||||
# run: |
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# tinygrad allows you to write kernels at many different abstractions levels.
|
||||
# This is for RDNA3, but if you don't have one you can run with the emulator
|
||||
# PYTHONPATH="." MOCKGPU=1 DEV=AMD
|
||||
|
||||
from tinygrad import Tensor, Context, GlobalCounters, UOp, Device
|
||||
from tinygrad.helpers import DEBUG, getenv
|
||||
from tinygrad.uop.ops import AxisType, KernelInfo, Ops
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
|
||||
SZ = 32*1024 if getenv("MOCKGPU") else 1024*1024*1024
|
||||
|
||||
if __name__ == "__main__":
|
||||
correct = None
|
||||
# First define a Tensor and realize it. We will focus on a 1GB sum kernel on RDNA3
|
||||
a = (Tensor.randn(SZ) if getenv("RAND") else Tensor.ones(SZ)).contiguous().realize()
|
||||
|
||||
def eval_harness(name, fxn, check=None):
|
||||
print(f"***** {name}")
|
||||
GlobalCounters.reset()
|
||||
with Context(DEBUG=max(DEBUG.value, 2)): out = fxn(a).item()
|
||||
assert check is None or abs(out - check) < abs(check) * 1e-3, f"out was wrong {out}, expected {check}, off by {out/check}x"
|
||||
print(f"computed in {GlobalCounters.time_sum_s*1000:.2f} ms, {(a.nbytes()/1e9)/GlobalCounters.time_sum_s:.2f} GB/s")
|
||||
return out
|
||||
|
||||
if not getenv("ASM"):
|
||||
# *****
|
||||
# This is the high level tinygrad way.
|
||||
# Note that this is split into multiple kernels for speed.
|
||||
|
||||
correct = eval_harness("basic kernel", lambda x: x.sum())
|
||||
|
||||
# *****
|
||||
# Now we get to the lower abstraction layers.
|
||||
# You can write a kernel in UOps, and it's 2.5x faster.
|
||||
|
||||
# This GPU has 32 CUs, keep them all busy
|
||||
CU_COUNT = 32
|
||||
def custom_sum(out:UOp, buf:UOp) -> UOp:
|
||||
LCLS = 256
|
||||
buf = buf.reshape(CU_COUNT, -1, LCLS)
|
||||
|
||||
glbl = UOp.range(CU_COUNT, 0, AxisType.GLOBAL)
|
||||
lane = UOp.range(LCLS, 1, AxisType.LOCAL)
|
||||
|
||||
# accumulate the globals into a per lane accumulator
|
||||
reduce_loop = UOp.range(buf.shape[1], 2, AxisType.REDUCE)
|
||||
acc = UOp.placeholder((1,), dtypes.float, slot=6, addrspace=AddrSpace.REG)
|
||||
acc = acc.after(acc.store(0))
|
||||
acc = acc.after(acc[0].store(acc.after(reduce_loop)[0] + buf[glbl, reduce_loop, lane]).end(reduce_loop))
|
||||
|
||||
# store all the per lane accumulators to LOCAL
|
||||
local_accs = UOp.placeholder((LCLS,), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
local_accs = local_accs.after(local_accs[lane].store(acc[0]).barrier())
|
||||
|
||||
# accumulate LOCALs into a single per CU accumulator
|
||||
late_reduce_loop = UOp.range(LCLS, 3, AxisType.REDUCE)
|
||||
acc2 = UOp.placeholder((1,), dtypes.float, slot=7, addrspace=AddrSpace.REG)
|
||||
acc2 = acc2.after(acc2.store(0))
|
||||
acc2 = acc2.after(acc2[0].store(acc2.after(late_reduce_loop)[0] + local_accs[late_reduce_loop]).end(late_reduce_loop))[0]
|
||||
|
||||
# store (NOTE: since the address doesn't depend on the warp, this will be automatically gated)
|
||||
return out[glbl].store(acc2).end(lane, glbl).sink(arg=KernelInfo(opts_to_apply=()))
|
||||
|
||||
eval_harness("custom UOp kernel", lambda x: Tensor.empty(CU_COUNT).custom_kernel(x, fxn=custom_sum)[0].sum(), check=correct)
|
||||
|
||||
# *****
|
||||
# You can also BEAM search stock tinygrad for a faster kernel.
|
||||
# This does even better than the custom kernel in this simple case.
|
||||
|
||||
with Context(BEAM=2): eval_harness("BEAMed kernel", lambda x: x.sum(), check=correct)
|
||||
|
||||
# *****
|
||||
# Though if you really want to go crazy with speed, you can code in assembly
|
||||
|
||||
# Kernel class copied from amd_asm_matmul
|
||||
class Kernel:
|
||||
def __init__(self, arch='gfx1100'): self.instructions, self.labels, self.pos, self.arch = [], {}, 0, arch
|
||||
def label(self, name): self.labels[name] = self.pos
|
||||
def emit(self, inst, target=None):
|
||||
self.instructions.append(inst)
|
||||
inst._target, inst._pos = target, self.pos
|
||||
self.pos += inst.size()
|
||||
return inst
|
||||
def waitcnt(self, lgkm=None, vm=None):
|
||||
# Wait for memory operations. lgkm=N waits until N lgkm ops remain, vm=N waits until N vmem ops remain.
|
||||
vmcnt, lgkmcnt, expcnt = vm if vm is not None else 63, lgkm if lgkm is not None else 63, 7
|
||||
waitcnt = (expcnt & 0x7) | ((lgkmcnt & 0x3f) << 4) | ((vmcnt & 0x3f) << 10)
|
||||
self.emit(s_waitcnt(simm16=waitcnt))
|
||||
def finalize(self, sink:UOp) -> UOp:
|
||||
for inst in self.instructions:
|
||||
if inst._target is None: continue
|
||||
offset_dwords = (self.labels[inst._target] - inst._pos - inst.size()) // 4
|
||||
if not -32768 <= offset_dwords <= 32767: raise ValueError(f"branch to '{inst._target}' offset {offset_dwords} exceeds simm16 range")
|
||||
inst.simm16 = offset_dwords
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=Device.DEFAULT),
|
||||
UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in self.instructions]))))
|
||||
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
CU_COUNT = 32
|
||||
LANES = 64
|
||||
def asm_sum(out:UOp, buf:UOp) -> UOp:
|
||||
V_LANE_ID = 0 # lane_id set on startup
|
||||
S_WORKGROUP_X = 2 # workgroup_id_x
|
||||
S_LOOP_CTR = 3
|
||||
k = Kernel()
|
||||
# mul lane id by 16 for offsets (4 for float, 4 for b128)
|
||||
k.emit(v_mul_lo_u32(v[0], v[V_LANE_ID], 16))
|
||||
k.emit(v_add_nc_u32_e32(v[1], 4096, v[0]))
|
||||
k.emit(v_add_nc_u32_e32(v[2], 4096, v[1]))
|
||||
k.emit(v_add_nc_u32_e32(v[3], 4096, v[2]))
|
||||
# load both addresses
|
||||
k.emit(s_load_b128(sdata=s[4:7], sbase=s[0:1], offset=0x0, soffset=NULL))
|
||||
k.waitcnt(lgkm=0)
|
||||
# offset buffer pointer by workgroup_id_x * chunk_size_bytes
|
||||
k.emit(s_mul_i32(s[S_LOOP_CTR], s[S_WORKGROUP_X], buf.numel()*4//CU_COUNT))
|
||||
k.emit(s_add_u32(s[6], s[6], s[S_LOOP_CTR]))
|
||||
k.emit(s_addc_u32(s[7], s[7], 0))
|
||||
# zero the accumulators
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, vdstx=v[4], vdsty=v[5], srcx0=0, srcy0=0))
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, vdstx=v[6], vdsty=v[7], srcx0=0, srcy0=0))
|
||||
|
||||
def emit_loads(base_vreg, reg_len):
|
||||
assert reg_len%4 == 0
|
||||
k.emit(s_clause(simm16=(reg_len//4)-1))
|
||||
for i in range(reg_len//4):
|
||||
offset = i*LANES*16
|
||||
assert offset < 16384
|
||||
k.emit(global_load_b128(vdst=v[base_vreg+i*4:base_vreg+i*4+3], addr=v[offset//4096], saddr=s[6:7], offset=offset%4096))
|
||||
k.emit(s_add_u32(s[6], s[6], reg_len * LANES * 4))
|
||||
k.emit(s_addc_u32(s[7], s[7], 0))
|
||||
|
||||
def tree_reduce_to_4567(base_vreg, reg_len):
|
||||
assert reg_len%4 == 0
|
||||
reg_len //= 4
|
||||
while reg_len > 1:
|
||||
half = reg_len // 2
|
||||
for j in range(half):
|
||||
a, b = base_vreg + j*4, base_vreg + (j+half)*4
|
||||
# v[a+0](bank0) += v[b+2](bank2), v[a+1](bank1) += v[b+3](bank3) — src0 and src1 on different banks
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_ADD_F32, VOPDOp.V_DUAL_ADD_F32, vdstx=v[a], vdsty=v[a+1], srcx0=v[a], vsrcx1=v[b+2], srcy0=v[a+1], vsrcy1=v[b+3]))
|
||||
# v[a+2](bank2) += v[b+0](bank0), v[a+3](bank3) += v[b+1](bank1) — src0 and src1 on different banks
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_ADD_F32, VOPDOp.V_DUAL_ADD_F32, vdstx=v[a+2], vdsty=v[a+3], srcx0=v[a+2], vsrcx1=v[b], srcy0=v[a+3], vsrcy1=v[b+1]))
|
||||
reg_len = half
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_ADD_F32, VOPDOp.V_DUAL_ADD_F32, vdstx=v[4], vdsty=v[5], srcx0=v[4], vsrcx1=v[base_vreg], srcy0=v[5], vsrcy1=v[base_vreg+1]))
|
||||
k.emit(VOPD(VOPDOp.V_DUAL_ADD_F32, VOPDOp.V_DUAL_ADD_F32, vdstx=v[6], vdsty=v[7], srcx0=v[6], vsrcx1=v[base_vreg+2], srcy0=v[7], vsrcy1=v[base_vreg+3]))
|
||||
|
||||
BASE_REG = 8
|
||||
LOAD_UNROLL = 64
|
||||
INNER_UNROLL = 2
|
||||
|
||||
assert buf.numel() % (CU_COUNT*LANES*LOAD_UNROLL*INNER_UNROLL) == 0
|
||||
total_batches = buf.numel()//(CU_COUNT*LANES*LOAD_UNROLL*INNER_UNROLL)
|
||||
k.emit(s_mov_b32(s[S_LOOP_CTR], total_batches-1))
|
||||
|
||||
k.label('LOOP')
|
||||
for _ in range(INNER_UNROLL):
|
||||
emit_loads(BASE_REG, reg_len=LOAD_UNROLL)
|
||||
k.waitcnt(vm=0)
|
||||
tree_reduce_to_4567(BASE_REG, reg_len=LOAD_UNROLL)
|
||||
k.emit(s_sub_u32(s[S_LOOP_CTR], s[S_LOOP_CTR], 1))
|
||||
k.emit(s_cbranch_scc0(), target='LOOP')
|
||||
|
||||
# add into v[4]
|
||||
k.emit(v_add_f32_e32(v[4], v[4], v[5]))
|
||||
k.emit(v_add_f32_e32(v[6], v[6], v[7]))
|
||||
k.emit(v_add_f32_e32(v[4], v[4], v[6]))
|
||||
|
||||
# warp shuffle into v[4] on lane 0 using DPP row_shl within each 16-lane row
|
||||
for shift in [1, 2, 4, 8]:
|
||||
k.emit(v_add_f32_e32(v[4], DPP, v[4], vsrc0=v[4], dpp=0x100 | shift, row_mask=0xf, bank_mask=0xf, bc=1))
|
||||
# combine rows: get lane 16's value to lane 0 via permlanex16
|
||||
k.emit(v_permlanex16_b32(v[5], v[4], 0, 0))
|
||||
k.emit(v_add_f32_e32(v[4], v[4], v[5]))
|
||||
|
||||
# atomic store (only on lane 0)
|
||||
k.emit(s_mov_b32(EXEC_LO, 1))
|
||||
k.emit(v_mov_b32_e32(v[0], 0))
|
||||
k.emit(global_atomic_add_f32(addr=v[0], saddr=s[4:5], data=v[4]))
|
||||
|
||||
k.emit(s_sendmsg(simm16=3)) # DEALLOC_VGPRS
|
||||
k.emit(s_endpgm())
|
||||
return k.finalize(UOp.sink(UOp.special(CU_COUNT, 'gidx0'), UOp.special(LANES, 'lidx0'), out, buf,
|
||||
arg=KernelInfo(name="asm_reduce", opts_to_apply=())))
|
||||
|
||||
out = Tensor.zeros(1,).contiguous().realize()
|
||||
eval_harness("RDNA3 assembly kernel", lambda x: out.custom_kernel(x, fxn=asm_sum)[0], check=correct)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from tinygrad import UOp, getenv
|
||||
from tinygrad import Device, UOp, getenv
|
||||
from tinygrad.uop.ops import AxisType, KernelInfo, Ops
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
|
||||
@@ -13,18 +13,23 @@ assert N % BLOCK_N == 0 and M % BLOCK_M == 0 and K % BLOCK_K == 0
|
||||
|
||||
use_wmma = getenv("WMMA")
|
||||
if use_wmma:
|
||||
is_rdna4 = Device[Device.DEFAULT].renderer.target.arch.startswith("gfx12")
|
||||
|
||||
WAVES_M, WAVES_N = 2, 2
|
||||
LANES_PER_WAVE_M, LANES_PER_WAVE_N = 2, 16
|
||||
UNROLL_M, UNROLL_N = 1, 1
|
||||
|
||||
# wmma params
|
||||
WMMA_M, WMMA_N, WMMA_K = 16, 16, 16
|
||||
WMMA_ACC = WMMA_M // LANES_PER_WAVE_M
|
||||
UNROLL_M, UNROLL_N = (WMMA_ACC, 1) if is_rdna4 else (1, 1)
|
||||
else:
|
||||
WAVES_M, WAVES_N = 4, 1
|
||||
LANES_PER_WAVE_M, LANES_PER_WAVE_N = 4, 8
|
||||
UNROLL_M, UNROLL_N = 4, 4
|
||||
|
||||
# total lanes must be the warp size
|
||||
assert LANES_PER_WAVE_M*LANES_PER_WAVE_N == WARP_SIZE
|
||||
|
||||
# WARP_SIZE * total waves
|
||||
THREADS_PER_BLOCK = WARP_SIZE * WAVES_M * WAVES_N
|
||||
|
||||
@@ -71,7 +76,10 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0,2,1)[tile_m, tile_n]
|
||||
a_frag = A_local.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_K // WMMA_K, WMMA_K)[wave_m, tile_m, lane_n, k]
|
||||
b_frag = B_local.reshape(WAVES_N, TN, WMMA_N, BLOCK_K // WMMA_K, WMMA_K)[wave_n, tile_n, lane_n, k]
|
||||
|
||||
if is_rdna4:
|
||||
# NOTE: since this is part of K, these 2 can be anywhere in the frags and long as a and b match
|
||||
a_frag = a_frag.reshape(2, 8)[lane_m, :]
|
||||
b_frag = b_frag.reshape(2, 8)[lane_m, :]
|
||||
wmma = UOp(Ops.SHAPED_WMMA, dtypes.float, (a_frag, b_frag, acc_frag.after(k)), arg=((16, 16, 16), 'AMD', 32))
|
||||
acc_store = acc_frag.store(wmma).end(tile_m, tile_n)
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@ import numpy as np
|
||||
from tinygrad.helpers import BEAM, Timing, CI, prod
|
||||
from tinygrad import Variable, Device, Tensor
|
||||
from tinygrad.nn import Conv2d
|
||||
from tinygrad.uop.ops import AxisType
|
||||
from tinygrad.uop.ops import AxisType, Ops
|
||||
from tinygrad.codegen.opt import Opt, OptOps
|
||||
from tinygrad.codegen.opt.postrange import Scheduler
|
||||
from tinygrad.codegen.opt.search import get_kernel_actions
|
||||
@@ -85,6 +85,7 @@ class TestBeamSearch(unittest.TestCase):
|
||||
size = max(tc.dims[0], tc.dims[1]) * 8
|
||||
a, b = Tensor.rand(size, size, dtype=tc.dtype_in), Tensor.rand(size, size, dtype=tc.dtype_in)
|
||||
ast = a.matmul(b, dtype=tc.dtype_out).schedule()[-1].ast
|
||||
if ast.op is Ops.BEAM: ast = ast.src[0]
|
||||
s = Scheduler(ast, Device[Device.DEFAULT].renderer)
|
||||
s.apply_opt(Opt(OptOps.TC, 0, (-1, 0, 1)))
|
||||
up = prod([x for x, t in zip(s.full_shape, s.axis_types) if t in (AxisType.UPCAST, AxisType.UNROLL)])
|
||||
@@ -95,6 +96,7 @@ class TestBeamSearch(unittest.TestCase):
|
||||
def test_max_up(self):
|
||||
a = Tensor.rand(16, 16)
|
||||
ast = a.schedule()[-1].ast
|
||||
if ast.op is Ops.BEAM: ast = ast.src[0]
|
||||
s = Scheduler(ast, Device[Device.DEFAULT].renderer)
|
||||
for max_up in (2, 4):
|
||||
actions = get_kernel_actions(s, include_0=False, max_up=max_up)
|
||||
|
||||
+7
-3
@@ -24,7 +24,7 @@ List all codegen steps for a kernel: `--rewrites -s E_3`
|
||||
Get source code: `--rewrites -s E_3 -i "View Source"`
|
||||
Inspect a graph rewrite: `--rewrites -s E_3 -i "initial symbolic"`
|
||||
|
||||
# SQTT tracing
|
||||
## SQTT tracing
|
||||
|
||||
Supported on AMD for RDNA3 and RDNA4 (best) and CDNA (developing).
|
||||
|
||||
@@ -38,8 +38,12 @@ You can select a specific trace with --source, Example workflow:
|
||||
VIZ=-2 python extra/gemm/amd_asm_matmul.py
|
||||
|
||||
# View barriers
|
||||
extra/viz/cli.py --profile -s "SQTT kernel PKTS SE:0" | rg BARRIER | head -10
|
||||
extra/viz/cli.py --profile -s "kernel SQTT SE:0 PKTS" | rg BARRIER | head -10
|
||||
|
||||
# Get bank conflicts from performance counters
|
||||
|
||||
python extra/viz/cli.py -p -s "kernel PMC" -i "SQC_LDS_BANK_CONFLICT"
|
||||
|
||||
# Find the EXEC corresponding to a DISPATCH at cycle 410
|
||||
extra/viz/cli.py --profile -s "SQTT kernel PKTS SE:0" | awk '/EXEC/ && $1 - $5 == 410'
|
||||
extra/viz/cli.py --profile -s "kernel SQTT SE:0 PKTS" | awk '/EXEC/ && $1 - $5 == 410'
|
||||
```
|
||||
|
||||
+19
-3
@@ -47,7 +47,9 @@ def decode_profile(data:bytes) -> dict:
|
||||
def get(data:dict, key:str):
|
||||
for k,v in data.items():
|
||||
if ansistrip(k) == key: return v
|
||||
raise RuntimeError(f'item "{key}" not found in list')
|
||||
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.trace = viz.load_pickle(args.rewrites_path, default=RewriteTrace([], [], {}))
|
||||
@@ -59,8 +61,8 @@ def main(args) -> None:
|
||||
events:list = viz.load_pickle(args.profile_path, default=[])
|
||||
if (profile_bytes:=viz.get_profile(events)) is None: raise RuntimeError(f"empty profile in {args.profile_path}")
|
||||
profile = decode_profile(profile_bytes)
|
||||
profile["layout"].update([(f'{c["name"]} {s["name"]}', s["data"]) for c in viz.ctxs if c["name"].startswith("SQTT") for s in c["steps"]
|
||||
if "PKTS" in s["name"]])
|
||||
profile["layout"].update([(f'{c["name"][5:]}{" SQTT" if s["name"].endswith("PKTS") else ""} {s["name"]}', s["data"]) for c in viz.ctxs
|
||||
if c["name"].startswith("SQTT") for s in c["steps"] if s["name"].endswith(("PMC", "PKTS"))])
|
||||
if args.src is None:
|
||||
for k in profile["layout"]:
|
||||
print(f" {format_colored(k)}")
|
||||
@@ -99,6 +101,20 @@ def main(args) -> None:
|
||||
print(f"{int(e.st)-inst_st:<12} {unit:<20} {op_str}{' '*(22-ansilen(op_str))} {int(unwrap(e.en)-e.st):<4} {str(delay or ''):<4} {info}")
|
||||
return None
|
||||
|
||||
# ** PMC printer
|
||||
if "PMC" in args.src:
|
||||
table = viz.unpack_pmc(data[0])
|
||||
cols = table["cols"]
|
||||
rows:list = []
|
||||
for r in table["rows"]:
|
||||
if args.item is None: rows.append(r[:2])
|
||||
elif args.item == r[0]:
|
||||
rows = r[2]["rows"] if len(r) > 2 else [r[:2]]
|
||||
cols = r[2]["cols"] if len(r) > 2 else cols
|
||||
from tabulate import tabulate
|
||||
print(tabulate(rows, headers=cols, tablefmt="github"))
|
||||
return None
|
||||
|
||||
# ** Profiler printer
|
||||
agg:dict[str, tuple[float, int]] = {}
|
||||
total = 0
|
||||
|
||||
@@ -24,6 +24,10 @@ class TestArange(unittest.TestCase):
|
||||
self.assertEqual(self._get_flops(Tensor.arange(256), np.arange(256)), 0)
|
||||
self.assertEqual(self._get_flops(Tensor.arange(2560), np.arange(2560)), 0)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "CL", "TODO: fails on CI CL")
|
||||
def test_arange_cumsum(self):
|
||||
np.testing.assert_equal(Tensor.arange(513).cumsum(0).numpy(), np.arange(513).cumsum())
|
||||
|
||||
def test_arange_cat(self):
|
||||
t = Tensor.arange(2, dtype=dtypes.int)+Tensor([3])
|
||||
self.assertEqual(t.cat(t).tolist(), [3, 4, 3, 4])
|
||||
|
||||
@@ -510,7 +510,7 @@ class TestSchedule(unittest.TestCase):
|
||||
np.testing.assert_allclose(out[1].numpy(), np.sqrt(np.square(y.numpy() - np_mu).sum(-1)/y.shape[-1]), atol=1e-4, rtol=1e-4)
|
||||
|
||||
def test_cumsum_parallel_reduce_fused(self):
|
||||
# two-stage cumsum + ops triggers parallel REDUCEs in one kernel that must share an END
|
||||
# two-stage cumsum + ops triggers parallel REDUCEs in one kernel that must share an END (same nesting context = should merge)
|
||||
step, num_steps = 513, 10
|
||||
t = Tensor.arange(step).float().realize()
|
||||
phase = t.cumsum()
|
||||
@@ -521,6 +521,12 @@ class TestSchedule(unittest.TestCase):
|
||||
expected = (expected * np.array([1,0,0,1,0,0,0,0,1,0]).reshape(num_steps, 1)).flatten()
|
||||
np.testing.assert_allclose(out.numpy(), expected, atol=1e-4, rtol=1e-4)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "CL", "TODO: fails on CI CL")
|
||||
def test_reduce_different_nesting_depth(self):
|
||||
# two REDUCEs sharing the same RANGE at different nesting depths must NOT merge
|
||||
x = Tensor.arange(768).reshape(3, 256).float()
|
||||
np.testing.assert_allclose((x.sum(axis=1) + x.sum(axis=1).sum()).numpy(), x.numpy().sum(axis=1) + x.numpy().sum(axis=1).sum())
|
||||
|
||||
def test_multimatmul_fusion(self):
|
||||
Tensor.manual_seed(0)
|
||||
a,b = Tensor.randn(4, 64).realize(), Tensor.rand(64,8).realize()
|
||||
|
||||
@@ -12,6 +12,51 @@ class TestC(unittest.TestCase):
|
||||
subprocess.check_output(('clang', '-x', 'c', '-fPIC', '-shared', '-', '-o', f.name), input=src.encode())
|
||||
return DLL("test", f.name)
|
||||
|
||||
def test_struct_array_init(self):
|
||||
@record
|
||||
class Foo:
|
||||
SIZE = 12
|
||||
a: Annotated[ctypes.c_int * 3, 0]
|
||||
init_records()
|
||||
|
||||
f = Foo((1,2,3))
|
||||
assert f.a[0] == 1
|
||||
assert f.a[1] == 2
|
||||
assert f.a[2] == 3
|
||||
f = Foo((ctypes.c_int * 3)(1,2,3))
|
||||
assert f.a[0] == 1
|
||||
assert f.a[1] == 2
|
||||
assert f.a[2] == 3
|
||||
|
||||
def test_field_ranges(self):
|
||||
@record
|
||||
class Foo:
|
||||
SIZE = 2
|
||||
s: Annotated[ctypes.c_int8, 0]
|
||||
u: Annotated[ctypes.c_uint8, 1]
|
||||
init_records()
|
||||
|
||||
f = Foo()
|
||||
f.s = -1
|
||||
f.u = -1
|
||||
assert f.s == -1
|
||||
assert f.u == 255
|
||||
|
||||
# this syntax is inherited from ctypes, but it seems a bit nonsensical?
|
||||
def test_voidp_none(self):
|
||||
@record
|
||||
class Foo:
|
||||
SIZE = 8
|
||||
p: Annotated[ctypes.c_void_p, 0]
|
||||
init_records()
|
||||
|
||||
f = Foo(None)
|
||||
assert f.p is None
|
||||
f.p = ctypes.c_void_p(0xDEADBEEF)
|
||||
assert f.p == 0xDEADBEEF
|
||||
f.p = None
|
||||
assert f.p is None
|
||||
|
||||
def test_packed_struct(self):
|
||||
@record
|
||||
class Baz:
|
||||
|
||||
@@ -21,7 +21,7 @@ from tinygrad.schedule.rangeify import pm_add_buffers_local, rangeify_codegen, p
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
|
||||
from tinygrad.renderer.amd.elf import do_assemble_amd
|
||||
|
||||
def full_rewrite_to_sink(sink:UOp, ren:Renderer|None=None, optimize:bool=True) -> UOp:
|
||||
def full_rewrite_to_sink(sink:UOp, ren:Renderer|None=None, optimize:bool=True, beam:int=0) -> UOp:
|
||||
if ren is None: ren = Renderer(Target())
|
||||
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Base AST")
|
||||
@@ -46,7 +46,7 @@ def full_rewrite_to_sink(sink:UOp, ren:Renderer|None=None, optimize:bool=True) -
|
||||
sink = graph_rewrite(sink, pm_flatten_range+pm_simplify_ranges, ctx={}, name="simplify ranges")
|
||||
|
||||
# do postrange optimization, BEAM or hand_coded_optimizations
|
||||
sink = apply_opts(sink, ren)
|
||||
sink = apply_opts(sink, ren, beam=beam)
|
||||
|
||||
# ** expander (expand_rewrite) **
|
||||
sink = graph_rewrite(sink, sym+pm_move_where_on_load, name="postopt symbolic")
|
||||
@@ -164,14 +164,15 @@ def get_program(ast:UOp, renderer:Renderer, opts:list[Opt]|None=None) -> Program
|
||||
"""
|
||||
|
||||
if ast.op is Ops.PROGRAM: prg = ast
|
||||
elif ast.op is Ops.SINK:
|
||||
elif ast.op is Ops.SINK or ast.op is Ops.BEAM:
|
||||
beam, ast = (ast.arg, ast.src[0]) if ast.op is Ops.BEAM else (0, ast)
|
||||
# rewrite to prg
|
||||
assert isinstance(ast.arg, KernelInfo), "requires KernelInfo on arg to get_program"
|
||||
if opts is not None:
|
||||
# TODO: should this be here?
|
||||
assert ast.arg.opts_to_apply is None, "can't apply opts if there's already opts to apply"
|
||||
ast = ast.replace(arg=replace(ast.arg, opts_to_apply=tuple(opts)))
|
||||
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None)
|
||||
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None, beam=beam)
|
||||
prg = UOp(Ops.PROGRAM, src=(full_sink, UOp(Ops.DEVICE, arg=renderer.target.device)))
|
||||
else:
|
||||
raise RuntimeError(f"can't call get_program on {ast.op}")
|
||||
|
||||
@@ -328,11 +328,23 @@ def reduce_to_acc(ctx:ReduceContext, red:UOp):
|
||||
return acc.after(end).index(UOp.const(dtypes.int, 0))
|
||||
|
||||
def merge_reduce_ends(ctx:ReduceContext, sink:UOp):
|
||||
# merge ENDs that share the same range (only those created by reduce_to_acc)
|
||||
# merge ENDs that share the same range and nesting context (only those created by reduce_to_acc)
|
||||
# ENDs at different nesting depths get cloned RANGEs so each RANGE maps to one END
|
||||
range_to_ends: dict[tuple[UOp, ...], list[UOp]] = {}
|
||||
for u in sink.backward_slice:
|
||||
if u.op is Ops.END and u.tag == "mergeable": range_to_ends.setdefault(u.src[1:], []).append(u)
|
||||
subs = {e: UOp.group(*(e.src[0] for e in ends)).end(*r) for r, ends in range_to_ends.items() if len(ends) > 1 for e in ends}
|
||||
subs: dict[UOp, UOp] = {}
|
||||
next_axis = max((u.arg[0] for u in sink.backward_slice if u.op is Ops.RANGE), default=-1) + 1
|
||||
for r, ends in range_to_ends.items():
|
||||
if len(ends) <= 1: continue
|
||||
by_ctx: dict[frozenset[UOp], list[UOp]] = {}
|
||||
for e in ends: by_ctx.setdefault(frozenset(e.ranges), []).append(e)
|
||||
for i, group in enumerate(by_ctx.values()):
|
||||
tr = r if i == 0 else tuple(rr.replace(arg=(next_axis + j, *rr.arg[1:])) for j, rr in enumerate(r))
|
||||
if i > 0: next_axis += len(r)
|
||||
mapped = [e.substitute(dict(zip(r, tr))) if i > 0 else e for e in group]
|
||||
merged = mapped[0] if len(mapped) == 1 else UOp.group(*(e.src[0] for e in mapped)).end(*tr)
|
||||
for e in group: subs[e] = merged
|
||||
return sink.substitute(subs) if subs else None
|
||||
|
||||
pm_reduce = PatternMatcher([
|
||||
|
||||
@@ -6,7 +6,7 @@ from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, KernelInfo, graph_r
|
||||
from tinygrad.uop.ops import axis_letters, axis_colors, axis_to_pos
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import colored, BEAM, getenv, DEBUG, to_function_name, NOOPT, argsort, round_up, prod, merge_dicts, get_single_element, flatten
|
||||
from tinygrad.helpers import colored, getenv, DEBUG, to_function_name, NOOPT, argsort, round_up, prod, merge_dicts, get_single_element, flatten
|
||||
from tinygrad.helpers import ALLOW_TF32, count, Context
|
||||
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError, check
|
||||
from tinygrad.codegen.simplify import pm_flatten_range
|
||||
@@ -334,18 +334,18 @@ def bufs_from_ast(ast:UOp, dname:str) -> list[Buffer]:
|
||||
glbls = sorted([x for x in ast.backward_slice if x.op is Ops.PARAM], key=lambda x: x.arg)
|
||||
return [Buffer(dname, x.ptrdtype.size, x.dtype.base) for x in glbls]
|
||||
|
||||
def apply_opts(ast:UOp, ren:Renderer) -> UOp:
|
||||
def apply_opts(ast:UOp, ren:Renderer, beam:int=0) -> UOp:
|
||||
if ast.tag is not None: return ast
|
||||
k = Scheduler(ast, ren)
|
||||
k.convert_loop_to_global()
|
||||
if ast.arg is not None and ast.arg.opts_to_apply is not None:
|
||||
for opt in ast.arg.opts_to_apply: k.apply_opt(opt)
|
||||
elif BEAM >= 1:
|
||||
elif beam >= 1:
|
||||
from tinygrad.codegen.opt.search import beam_search
|
||||
rawbufs = bufs_from_ast(ast, ren.target.device)
|
||||
# beam search may open devices
|
||||
with Context(ALLOW_DEVICE_USAGE=1):
|
||||
k = beam_search(k, rawbufs, BEAM.value, bool(getenv("BEAM_ESTIMATE", 1)))
|
||||
k = beam_search(k, rawbufs, beam, bool(getenv("BEAM_ESTIMATE", 1)))
|
||||
elif not NOOPT and (ast.arg is None or ast.arg.applied_opts == ()):
|
||||
from tinygrad.codegen.opt.heuristic import hand_coded_optimizations
|
||||
# NOTE: hand_coded_optimizations doesn't support multiblock opts yet
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import cast, Callable
|
||||
import time, pprint, random, itertools, math
|
||||
from dataclasses import dataclass, replace, field
|
||||
from tinygrad.helpers import all_same, colored, DEBUG, GlobalCounters, ansilen, BEAM, NOOPT, all_int, Metadata, TRACEMETA, TracingKey
|
||||
from tinygrad.helpers import DEVECTORIZE, time_to_str, VALIDATE_WITH_CPU, cpu_profile, PROFILE, ProfilePointEvent, cpu_events, prod, Context, unwrap
|
||||
from tinygrad.helpers import all_same, colored, DEBUG, GlobalCounters, ansilen, NOOPT, all_int, Metadata, TRACEMETA, TracingKey
|
||||
from tinygrad.helpers import DEVECTORIZE, time_to_str, VALIDATE_WITH_CPU, cpu_profile, PROFILE, ProfilePointEvent, cpu_events, prod, unwrap
|
||||
from tinygrad.helpers import EMULATED_DTYPES
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, sym_infer
|
||||
from tinygrad.device import Device, Buffer
|
||||
@@ -130,7 +130,7 @@ class EncDec(Runner):
|
||||
method_cache: dict[tuple[str, type, bytes, tuple, bool], CompiledRunner] = {}
|
||||
def get_runner(device:str, ast:UOp) -> CompiledRunner:
|
||||
# TODO: this should be all context relevant to rendering
|
||||
context = (BEAM.value, NOOPT.value, DEVECTORIZE.value, EMULATED_DTYPES.value)
|
||||
context = (NOOPT.value, DEVECTORIZE.value, EMULATED_DTYPES.value)
|
||||
ckey = (device, type(Device[device].compiler), ast.key, context, False)
|
||||
if cret:=method_cache.get(ckey): return cret
|
||||
bkey = (device.split(":")[0], type(Device[device].compiler), ast.key, context, True)
|
||||
@@ -145,7 +145,7 @@ def get_runner(device:str, ast:UOp) -> CompiledRunner:
|
||||
|
||||
# NOTE: ctx is the buffers
|
||||
si_lowerer = PatternMatcher([
|
||||
(UPat((Ops.SINK, Ops.PROGRAM), name="sink"), lambda ctx,sink: get_runner(ctx[0].device, sink)),
|
||||
(UPat((Ops.SINK, Ops.PROGRAM, Ops.BEAM), name="sink"), lambda ctx,sink: get_runner(ctx[0].device, sink)),
|
||||
(UPat(Ops.BUFFER_VIEW), lambda ctx: ViewOp(ctx[0])),
|
||||
(UPat(Ops.COPY), lambda ctx: (BufferXfer(ctx[0].nbytes, ctx[0].device, ctx[1].device) \
|
||||
if hasattr(alc:=Device[ctx[0].device].allocator, '_transfer') and alc.supports_transfer and all_same([x.device.split(":")[0] for x in ctx]) \
|
||||
@@ -198,7 +198,8 @@ capturing: list = [] # put classes with an add_linear method in here
|
||||
def run_schedule(schedule:list[ExecItem], var_vals:dict[str, int]|None=None, do_update_stats=True):
|
||||
while len(schedule):
|
||||
ei = schedule.pop(0).lower()
|
||||
if VALIDATE_WITH_CPU and ei.ast.op is Ops.SINK:
|
||||
sink = ei.ast.src[0] if ei.ast.op is Ops.BEAM else ei.ast
|
||||
if VALIDATE_WITH_CPU and sink.op is Ops.SINK:
|
||||
# copy in allocated buffers from the GPU
|
||||
bufs = [b for b in ei.bufs if b is not None]
|
||||
nb: list[Buffer|None] = [Buffer("CPU", b.size, b.dtype) for b in bufs]
|
||||
@@ -209,7 +210,7 @@ def run_schedule(schedule:list[ExecItem], var_vals:dict[str, int]|None=None, do_
|
||||
ei.run(var_vals, do_update_stats=do_update_stats)
|
||||
|
||||
# validate the output buffers match (NOTE: this is assuming the output is buffer 0)
|
||||
with Context(BEAM=0): ExecItem(ei.ast, nb, ei.metadata, ei.fixedvars).run(var_vals, do_update_stats=do_update_stats)
|
||||
ExecItem(sink, nb, ei.metadata, ei.fixedvars).run(var_vals, do_update_stats=do_update_stats)
|
||||
import numpy as np
|
||||
assert nb[0] is not None
|
||||
np.testing.assert_allclose(bufs[0].numpy(), nb[0].numpy(), rtol=1e-3, atol=1e-3)
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections import deque
|
||||
from tinygrad.uop.ops import UOp, Ops, buffers, UOpMetaClass, track_rewrites, graph_rewrite, gate_kernel_sink, KernelInfo
|
||||
from tinygrad.uop.spec import type_verify, tensor_spec
|
||||
from tinygrad.device import Buffer, MultiBuffer
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, flatten
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, flatten, BEAM
|
||||
from tinygrad.engine.realize import ExecItem
|
||||
|
||||
# **** schedule linearizer
|
||||
@@ -72,6 +72,8 @@ def linear_to_schedule(linear:UOp) -> list[ExecItem]:
|
||||
base = buf_uops[1].buffer
|
||||
assert isinstance(base, Buffer), "base can't be MultiBuffer"
|
||||
buffers[buf_uops[0]] = base.view(buf_uops[0].arg, ast.dtype, ast.arg[1]*base.dtype.itemsize)
|
||||
# wrap SINK with BEAM UOp when beam search is enabled
|
||||
if ast.op is Ops.SINK and BEAM >= 1: ast = UOp(Ops.BEAM, src=(ast,), arg=BEAM.value)
|
||||
ubufs = [b.buffer for b in buf_uops if b.op is not Ops.BIND]
|
||||
metadata = si.arg.metadata
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "graph":
|
||||
|
||||
@@ -13,7 +13,8 @@ ARCHS = {
|
||||
"rdna4": {"xml": "amdgpu_isa_rdna4.xml", "pdf": "https://docs.amd.com/api/khub/documents/uQpkEvk3pv~kfAb2x~j4uw/content"},
|
||||
"cdna": {"xml": "amdgpu_isa_cdna4.xml", "pdf": "https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/instruction-set-architectures/amd-instinct-cdna4-instruction-set-architecture.pdf"},
|
||||
}
|
||||
XML_URL = "https://gpuopen.com/download/machine-readable-isa/latest/"
|
||||
# Pin the September 2025 XML bundle because newer `latest` changed WMMA format bit sizes across archs and breaks generation.
|
||||
XML_URL = "https://gpuopen.com/download/AMD_GPU_MR_ISA_XML_2025_09_05.zip"
|
||||
# Map XML encoding names to codebase names
|
||||
NAME_MAP = {"VOP3_SDST_ENC": "VOP3SD", "VOP3_SDST_ENC_LIT": "VOP3SD_LIT", "VOP3_SDST_ENC_DPP16": "VOP3SD_DPP16",
|
||||
"VOP3_SDST_ENC_DPP8": "VOP3SD_DPP8", "VOPDXY": "VOPD", "VOPDXY_LIT": "VOPD_LIT", "VDS": "DS"}
|
||||
|
||||
@@ -389,6 +389,7 @@ class AM_GFX(AM_IP):
|
||||
self._grbm_select(me=1, pipe=0, queue=q, inst=xcc)
|
||||
if self.adev.regCP_HQD_ACTIVE.read(inst=xcc) & 1:
|
||||
self.adev.regCP_HQD_DEQUEUE_REQUEST.write(0x2, inst=xcc) # 1 - DRAIN_PIPE; 2 - RESET_WAVES
|
||||
self.adev.regSPI_COMPUTE_QUEUE_RESET.write(0x1, inst=xcc)
|
||||
if not self.adev.is_err_state: wait_cond(lambda: self.adev.regCP_HQD_ACTIVE.read(inst=xcc) & 1, value=0, msg="HQD dequeue timeout")
|
||||
self._grbm_select()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, functools, os, pathlib, re, sys, sysconfig
|
||||
from tinygrad.helpers import ceildiv, getenv, unwrap, DEBUG, OSX, WIN
|
||||
from _ctypes import Array as _CArray, _SimpleCData, _Pointer
|
||||
from typing import TYPE_CHECKING, get_type_hints, get_args, get_origin, overload, Annotated, Any, Generic, Iterable, ParamSpec, TypeVar
|
||||
|
||||
def _do_ioctl(__idir, __base, __nr, __struct, __fd, *args, __payload=None, **kwargs):
|
||||
@@ -34,22 +33,22 @@ if TYPE_CHECKING:
|
||||
from _ctypes import _CData
|
||||
class Array(Generic[T, U], _CData):
|
||||
@overload
|
||||
def __getitem__(self: Array[_SimpleCData[V], Any], key: int) -> V: ...
|
||||
def __getitem__(self: Array[ctypes._SimpleCData[V], Any], key: int) -> V: ...
|
||||
@overload
|
||||
def __getitem__(self: Array[T, Any], key: slice) -> list[T]: ...
|
||||
@overload
|
||||
def __getitem__(self: Array[T, Any], key: int) -> T: ...
|
||||
def __getitem__(self, key) -> Any: ...
|
||||
@overload
|
||||
def __setitem__(self: Array[_SimpleCData[V], Any], key: int, val: V): ...
|
||||
def __setitem__(self: Array[ctypes._SimpleCData[V], Any], key: int, val: V): ...
|
||||
@overload
|
||||
def __setitem__(self: Array[T, Any], key: int, val: T): ...
|
||||
@overload
|
||||
def __setitem__(self: Array[T, Any], key: slice, val: Iterable[T]): ...
|
||||
def __setitem__(self, key, val): ...
|
||||
class POINTER(Generic[T], _Pointer): ...
|
||||
class POINTER(Generic[T], ctypes._Pointer): ...
|
||||
class CFUNCTYPE(Generic[T, P], _CFunctionType): ...
|
||||
class Enum(_SimpleCData):
|
||||
class Enum(ctypes._SimpleCData):
|
||||
@classmethod
|
||||
def get(cls, val:int, default="unknown") -> str: ...
|
||||
@classmethod
|
||||
@@ -80,14 +79,9 @@ else:
|
||||
return val
|
||||
def pointer(obj): return ctypes.pointer(obj)
|
||||
|
||||
def i2b(i:int, sz:int) -> bytes: return i.to_bytes(sz, sys.byteorder)
|
||||
def b2i(b:bytes) -> int: return int.from_bytes(b, sys.byteorder)
|
||||
def mv(st) -> memoryview: return memoryview(st).cast('B')
|
||||
|
||||
class Struct(ctypes.Structure):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ctypes.Structure.__init__(self)
|
||||
self._objects_ = {}
|
||||
for f,v in [*zip((rf[0] for rf in self._real_fields_), args), *kwargs.items()]: setattr(self, f, v)
|
||||
|
||||
def record(cls) -> type[Struct]:
|
||||
@@ -98,38 +92,38 @@ def record(cls) -> type[Struct]:
|
||||
def init_records() -> None:
|
||||
for cls, struct, ns in _pending_records:
|
||||
setattr(struct, '_real_fields_', [])
|
||||
for nm, t in get_type_hints(cls, globalns=ns, include_extras=True).items():
|
||||
if t.__origin__ in (bool, bytes, str, int, float): setattr(struct, nm, Field(*(f:=t.__metadata__)))
|
||||
else: setattr(struct, nm, Field(*(f:=(del_an(t.__origin__), *t.__metadata__))))
|
||||
struct._real_fields_.append((nm,) + f) # type: ignore
|
||||
for i, (nm, t) in enumerate(get_type_hints(cls, globalns=ns, include_extras=True).items()):
|
||||
struct._real_fields_.append((nm, *(f:=(del_an(t.__origin__), *t.__metadata__) if isinstance(t.__metadata__[0], int) else t.__metadata__))) # type: ignore
|
||||
setattr(struct, nm, Field(nm, i, *f))
|
||||
_pending_records.clear()
|
||||
|
||||
class Field(property):
|
||||
def __init__(self, typ, off:int, bit_width=None, bit_off=0):
|
||||
if bit_width is not None:
|
||||
sl, set_mask = slice(off,off+(sz:=ceildiv(bit_width+bit_off, 8))), ~((mask:=(1 << bit_width) - 1) << bit_off)
|
||||
class Field:
|
||||
def __init__(self, nm, idx, typ, off, bit_width=None, bit_off=0):
|
||||
self.nm, self.idx, self.typ, self.off, self.bit_width, self.bit_off = nm, idx, typ, off, bit_width, bit_off
|
||||
|
||||
# lazily resolve field descriptors
|
||||
def _resolve(self, cls):
|
||||
if self.bit_width: # handle bitfields ourselves
|
||||
sl, set_mask = slice(self.off, self.off+(sz:=ceildiv(self.bit_width+self.bit_off, 8))), ~((mask:=(1 << self.bit_width) - 1) << self.bit_off)
|
||||
def b2i(obj): return int.from_bytes(memoryview(obj).cast("B")[sl], sys.byteorder)
|
||||
def bset(obj, v): memoryview(obj).cast("B")[sl] = ((b2i(obj) & set_mask) | v << self.bit_off).to_bytes(sz, sys.byteorder)
|
||||
# FIXME: signedness
|
||||
super().__init__(lambda self: (b2i(mv(self)[sl]) >> bit_off) & mask,
|
||||
lambda self,v: mv(self).__setitem__(sl, i2b((b2i(mv(self)[sl]) & set_mask) | (v << bit_off), sz)))
|
||||
else:
|
||||
sl = slice(off, off + ctypes.sizeof(typ))
|
||||
def set_with_objs(f):
|
||||
def wrapper(self, v):
|
||||
if hasattr(v, '_objects') and hasattr(self, '_objects_'): self._objects_[off] = {'_self_': v, **(v._objects or {})}
|
||||
mv(self).__setitem__(sl, bytes(v if isinstance(v, typ) else f(v)))
|
||||
return wrapper
|
||||
if issubclass(typ, _CArray):
|
||||
getter = (lambda self: typ.from_buffer(mv(self)[sl]).value) if typ._type_ is ctypes.c_char else (lambda self: typ.from_buffer(mv(self)[sl]))
|
||||
super().__init__(getter, set_with_objs(lambda v: typ(*v)))
|
||||
else: super().__init__(lambda self: v.value if isinstance(v:=typ.from_buffer(mv(self)[sl]), _SimpleCData) else v, set_with_objs(typ))
|
||||
self.offset = off
|
||||
cf = property(lambda obj: b2i(obj) >> self.bit_off & mask, bset)
|
||||
# pull the CField descriptor from a dummy class, zero length arrays are so ctypes manages references to child objects for us
|
||||
else: cf = type(self.nm, (ctypes.Structure,), {"_layout_": "ms", "_pack_": 1, "_fields_": [(str(i), ctypes.c_byte * 0) for i in range(self.idx)] +
|
||||
[("_", ctypes.c_byte * self.off), ("v", self.typ)]}).v # type: ignore
|
||||
setattr(cls, self.nm, cf)
|
||||
return cf
|
||||
|
||||
def __get__(self, obj, objtype=None): return self._resolve(objtype).__get__(obj, objtype) if objtype else self
|
||||
def __set__(self, obj, value): self._resolve(obj.__class__).__set__(obj, value)
|
||||
|
||||
@functools.cache
|
||||
def init_c_struct_t(sz:int, fields: tuple[tuple, ...]):
|
||||
CStruct = type("CStruct", (Struct,), {'_fields_': [('_mem_', ctypes.c_byte * sz)], '_real_fields_': []})
|
||||
for nm,ty,*args in fields:
|
||||
setattr(CStruct, nm, Field(*(f:=(del_an(ty), *args))))
|
||||
CStruct._real_fields_.append((nm,) + f) # type: ignore
|
||||
for i,(nm,ty,*args) in enumerate(fields):
|
||||
CStruct._real_fields_.append((nm, *(f:=(del_an(ty), *args)))) # type: ignore
|
||||
setattr(CStruct, nm, Field(nm, i, *f))
|
||||
return CStruct
|
||||
def init_c_var(ty, creat_cb): return (creat_cb(v:=del_an(ty)()), v)[1]
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Ops(FastEnum):
|
||||
|
||||
# AFTER passes src[0] through and promises in the toposort that any consumers of the AFTER run after src[1:]
|
||||
# GROUP is a NOOP that just merges things together
|
||||
SINK = auto(); AFTER = auto(); GROUP = auto()
|
||||
SINK = auto(); AFTER = auto(); GROUP = auto(); BEAM = auto()
|
||||
|
||||
# vector creation / item selection
|
||||
GEP = auto(); VECTORIZE = auto()
|
||||
|
||||
+3
-6
@@ -633,7 +633,7 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
case Ops.PERMUTE | Ops.FLIP: return self.arg
|
||||
case _: raise RuntimeError(f"{self.op} is not a MovementOp")
|
||||
|
||||
def _mop(self, op:Ops, arg, same_shape_noop:bool=False) -> UOp:
|
||||
def _mop(self, op:Ops, arg) -> UOp:
|
||||
# early NOOP
|
||||
if op in {Ops.SHRINK, Ops.PAD, Ops.EXPAND} and len(arg) == 0:
|
||||
assert len(self.shape) == 0, "0 len arg only valid on zero length shape"
|
||||
@@ -644,11 +644,8 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
case Ops.PERMUTE | Ops.FLIP: src_args = []
|
||||
case _: raise RuntimeError(f"{op} is not a MovementOp")
|
||||
usrcs = [shape_to_shape_arg(arg) for arg in src_args]
|
||||
if len(usrcs) == 0: ret = UOp(op, self.dtype, (self,), arg)
|
||||
else: ret = UOp(op, self.dtype, (self,)+UOp.sink(*usrcs).simplify().src)
|
||||
# for all movement ops, we check shape property to validity check the movement op
|
||||
if ret.shape == self.shape and same_shape_noop: return self
|
||||
return ret
|
||||
if len(usrcs) == 0: return UOp(op, self.dtype, (self,), arg)
|
||||
return UOp(op, self.dtype, (self,)+UOp.sink(*usrcs).simplify().src)
|
||||
|
||||
# *** uop UNIQUE ***
|
||||
|
||||
|
||||
@@ -298,6 +298,8 @@ full_spec = PatternMatcher([
|
||||
(UPat(Ops.DEFINE_VAR, dtype=dtypes.floats), lambda: True),
|
||||
# allow any AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(),), allow_any_len=True), lambda: True),
|
||||
# BEAM wraps a SINK for beam search
|
||||
(UPat(Ops.BEAM, src=(UPat(Ops.SINK),)), lambda: True),
|
||||
])+_tensor_spec+kernel_spec+program_spec+shared_spec
|
||||
|
||||
# ***** uop helpers *****
|
||||
|
||||
+10
-6
@@ -288,18 +288,22 @@ metrics:dict[str, Callable[[dict[str, tuple[int, int, int]]], str]] = {
|
||||
|
||||
def unpack_pmc(e) -> dict:
|
||||
agg_cols = ["Name", "Sum"]
|
||||
sample_cols = ["XCC", "INST", "SE", "SA", "WGP", "Value"]
|
||||
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), range(s.wgp)):
|
||||
row[1] += (val:=int(view[ptr]))
|
||||
max_val, cnt = max(max_val, val), cnt + 1
|
||||
row[2]["rows"].append(sample+(val,))
|
||||
ptr += 1
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user