Compare commits

..
Author SHA1 Message Date
geohot 303c5d3259 we don't float anymore 2026-07-21 18:33:34 -07:00
geohot 048f510b51 cleanups 2026-07-21 18:29:13 -07:00
geohot cacba3f4d5 cleanups 2026-07-21 18:11:20 -07:00
geohot 01c6f396b1 upd 2026-07-21 17:36:05 -07:00
geohot d51003bb61 LOOP is srcless RANGE (kimi) 2026-07-21 17:18:17 -07:00
chenyuandGitHub 9433790adb move _broadcasted to ElementwiseMixin [PR] (#17128) 2026-07-21 19:42:07 -04:00
chenyuandGitHub 92f9c850b4 fix pow(int, float) (#17126)
* fix pow(int, float)

* onnx
2026-07-21 18:48:24 -04:00
chenyuandGitHub b1060ca708 don't promote dtype in _pad_constant [pr] (#17125) 2026-07-21 18:13:50 -04:00
chenyuandGitHub b1a72299ab more _broadcast_to cleanup [PR] (#17124) 2026-07-21 17:46:04 -04:00
sirhcmandGitHub 8fa5993923 replace pytest-timeout with SIGABRT hook (#17122) 2026-07-21 17:42:58 -04:00
nimlgenandGitHub f41e4a758f drop Ops.WAIT (#17121) 2026-07-22 00:42:45 +03:00
chenyuandGitHub f19a2ad771 single where mixin [pr] (#17118)
* single where mixin [pr]

no shape broadcasting in ufix and _broadcasted anymore

* QCOM vectorized bool is broken
2026-07-21 17:38:36 -04:00
nimlgenandGitHub 787b2f2db2 hcq2: use ins for hcq ir (#17120) 2026-07-21 23:42:34 +03:00
Armand du Parc LocmariaandGitHub ef37830d13 allow freeing buffers when pickling/unpickling (#16799)
* allow pickling out of band buffers

* also need to release when loading

* test peak ram

* lint

* sync before yielding next buffer for backends with async copy in

* skip on mock devices

* reason

* or always bytearray, always free?

* Revert "or always bytearray, always free?"

This reverts commit a017bb68742985a5b7431e0b4e973c2997c92b6a.

* one less copy
2026-07-21 16:11:23 -04:00
chenyuandGitHub 5244d3cd2a fix test_u32_to_f16 (#17119) 2026-07-21 16:06:05 -04:00
b764599d87 add Ops.LOOP + conditional Ops.END (kimi) (#17117)
* add Ops.LOOP + conditional Ops.END (kimi)

* c

* x

---------

Co-authored-by: George Hotz <[email protected]>
2026-07-21 22:54:14 +03:00
chenyuandGitHub 46b82d4755 don't auto cast cond for WHERE (#17115)
no or_casted all WHEREs with single mixin, matched torch
2026-07-21 13:00:07 -04:00
chenyuandGitHub 76dade5a11 implicit broadcast gradient based on shape only [pr] (#17114)
fixed gradient for shape () UOp, enabled unify WHERE mixin
2026-07-21 12:51:54 -04:00
chenyuandGitHub f64f96ec59 broadcast_axes [PR] (#17112)
prerequisite to simplify broadcasting logic and make it implicit
2026-07-21 11:53:12 -04:00
qazalandGitHub 7b05caf5c5 viz: do not crash on sym_infer err (#17109) 2026-07-21 13:04:49 +09:00
chenyuandGitHub 34bcc5ad63 logcumsumexp mask is bool (#17108) 2026-07-20 22:42:58 -04:00
chenyuandGitHub 40f0d4af14 clean up _broadcasted [PR] (#16974)
no more ptr issue
2026-07-20 22:28:19 -04:00
chenyuandGitHub 2864036e8e correct more spelling of coalesce (#17107) 2026-07-20 21:55:37 -04:00
sirhcmandGitHub f3a5337825 correct spelling of coalesce (#17103) 2026-07-20 21:44:04 -04:00
chenyuandGitHub 95f5c85bf3 some realize and corealize for slow tests (#17106) 2026-07-20 21:43:23 -04:00
George HotzandGitHub 2a81616492 update rules for INDEX mops (#17105) 2026-07-20 18:43:00 -07:00
chenyuandGitHub 13ca9bd8a6 remove dtypes.index again (#17104)
also reverted some dtype change, the split made things needlessly complicated
2026-07-20 20:30:04 -04:00
George HotzandGitHub 636a43722d add END and GROUP to addrspace (#17102) 2026-07-20 17:26:02 -07:00
sirhcmandGitHub 980748ccfc add multiple_of to ParamArg (#17101) 2026-07-20 20:11:54 -04:00
George HotzandGitHub f7ce7f330d llm: minor fixes + tests (#17099)
* llm: minor fixes + tests

* error
2026-07-20 14:31:20 -07:00
chenyuandGitHub 4b8db13e01 rdna int8 wmma (#17098)
nice to fix _wmma_name, also more generic tests
2026-07-20 17:00:50 -04:00
76 changed files with 683 additions and 491 deletions
+10
View File
@@ -0,0 +1,10 @@
import os, pytest, signal, threading
@pytest.hookimpl(wrapper=True)
def pytest_runtest_call(item):
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 300)), os.kill, args=(os.getpid(), signal.SIGABRT))
t.start()
try: yield
finally:
t.cancel()
t.join()
+2 -2
View File
@@ -5,10 +5,10 @@ def bit_extract(x: Tensor, e: int, s: int) -> Tensor:
return (x >> s) & mask
def u16_to_f16(x: Tensor) -> Tensor:
sign = bit_extract(x, 15, 15).float()
sign = bit_extract(x, 15, 15).bool()
exponent = bit_extract(x, 14, 10).float()
fraction = bit_extract(x, 9, 0).float()
return sign.where(-1, 1) * exponent.where((exponent - 15.0).exp2() * (1 + fraction / 1024.0), 6.103515625e-5 * (fraction / 1024.0))
return sign.where(-1, 1) * exponent.bool().where((exponent - 15.0).exp2() * (1 + fraction / 1024.0), 6.103515625e-5 * (fraction / 1024.0))
def u32_to_f16(oo: Tensor) -> Tensor:
f1 = u16_to_f16(oo>>16)
+3 -3
View File
@@ -174,10 +174,10 @@ def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
m = UOp.range(M, 1, AxisType.LOOP)
n = UOp.range(N, 2, AxisType.LOOP)
k = UOp.range(K, 0, AxisType.REDUCE)
mul = (A.flatten().index((m*UOp.const(dtypes.index, K)+k))*
B.flatten().index((k*UOp.const(dtypes.index, N)+n))).cast(dtypes.float32)
mul = (A.flatten().index((m*UOp.const(dtypes.weakint, K)+k))*
B.flatten().index((k*UOp.const(dtypes.weakint, N)+n))).cast(dtypes.float32)
red = mul.reduce(k, arg=Ops.ADD, dtype=dtypes.float32).cast(C.dtype)
store = C.flatten().index((m*UOp.const(dtypes.index, N)+n)).store(red).end(m, n)
store = C.flatten().index((m*UOp.const(dtypes.weakint, N)+n)).store(red).end(m, n)
return store.sink(arg=KernelInfo(name=f'uop_gemm_{M}_{N}_{K}'))
# ** bf16 A @ B.T kernel in C
+11 -10
View File
@@ -49,7 +49,7 @@ def make_getaddr(u, device=None):
return UOp(Ops.GETADDR, dtypes.uint64, src=(u,), arg=device or to_tuple(u.device)[0])
def make_ins(op, *srcs):
return UOp(Ops.INS, dtypes.void, tuple(UOp.const(dtypes.uint32, s) if isinstance(s, int) else s.cast(dtypes.uint32) for s in srcs), op)
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(dtypes.uint32, s) if isinstance(s, int) else s.cast(dtypes.uint32) for s in srcs))
def make_placeholder(devs, size:int, dtype, name=None, unique=True) -> UOp:
return UOp.param(next(UOp.unique_num) if unique else 0, dtype, shape=(size,), device=devs).rtag(name or "temp")
@@ -133,7 +133,7 @@ def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str,
for (ddevs, dqueue, dtag), lanes in deps.items():
sig = make_mstack([make_signal(d if dl is None else ddevs[dl], queue=dqueue, sentinel=dl is None) for dl, d in zip(lanes, devices)])
val = make_mstack([make_signal_value(d if dl is None else ddevs[dl], queue=dqueue) for dl, d in zip(lanes, devices)])
waits.append((sig.index(zero:=UOp.const(dtypes.int, 0)).load() >= val.index(zero) + dtag).wait())
waits.append(UOp(Ops.INS, arg="wait", src=(sig, val.index(UOp.const(dtypes.int, 0)) + dtag)))
return waits, {dtag for _, _, dtag in deps}
def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[tuple[tuple[str, ...], str]],
@@ -154,7 +154,8 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
waited |= cur_waited
# wait the syncs, store the device epoch; value bumps are a separate call: no lane may bump until every lane has patched its waits
submit = make_submit(*waits, make_signal(devs).store((tl:=make_signal_value(devs)).index(zero)), devs=devs, queue="COMPUTE:0")
store = UOp(Ops.INS, arg="store", src=(make_signal(devs), (tl:=make_signal_value(devs)).index(zero)))
submit = make_submit(*waits, store, devs=devs, queue="COMPUTE:0")
upd = [(tl, 1)] + [(make_signal_value(devs, queue=qn), n) for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])]
bump = UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])
finalizers += [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for b in (submit, bump)]
@@ -181,15 +182,15 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
for tag, ((call, _), (devices, queue), cmds) in enumerate(zip(batch, batch_info, call_waits)):
# first queue use, sync prior device work with main signal
if batch_info.index((devices, queue)) == tag:
epoch = (make_signal(devices).index(0).load() >= make_signal_value(devices).index(0) - 1).wait()
cmds = [UOp(Ops.BARRIER), epoch] + cmds
# signal queue timeline if someone waits for us
store = make_signal(devices, queue=queue).store(make_signal_value(devices, queue=queue).index(0) + tag) if tag in waited else None
epoch = UOp(Ops.INS, arg="wait", src=(make_signal(devices), make_signal_value(devices).index(0) - 1))
cmds = [UOp(Ops.INS, arg="barrier", src=()), epoch] + cmds
# and make hcq call
info = HCQInfo(get_call_name(call, get_call_arg_uops(call)), estimate_uop(call), devices, queue)
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))] + ([store] if store is not None else [])
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))]
# signal queue timeline if someone waits for us
if tag in waited: cmds += [UOp(Ops.INS, arg="store", src=(make_signal(devices, queue), make_signal_value(devices, queue).index(0) + tag))]
src.append(UOp.custom_function("hcq", make_submit(*cmds, devs=devices, queue=queue).sink()).call(name="hcq", aux=info))
return src + finalizers
@@ -353,7 +354,7 @@ def pack_hcq_placeholders(call:UOp) -> UOp|None:
sizes[b.tag] = offs[b] + b.max_numel()
counts = collections.Counter(b.tag for b in bufs)
bases = {b.tag:make_placeholder(b.device, sizes[b.tag], b.dtype, b.tag) for b in bufs if counts[b.tag] > 1}
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.index, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.weakint, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
return call.replace(src=(call.src[0].substitute(subs, walk=True), *call.src[1:])) if subs else None
pm_pack_placeholders = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
+11 -11
View File
@@ -90,7 +90,7 @@ def memory_barrier(ctx):
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
acquire_mem(ctx)))
def pm4_wait(ctx, x, y): return wait_reg_mem(ctx, y, mem=make_getaddr(x.buf_uop, ctx.devs))
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=make_getaddr(dst, ctx.devs))
def pm4_barrier(ctx): return memory_barrier(ctx)
@@ -138,10 +138,10 @@ def pm4_program(ctx, call, prg):
pm_pm4_opsel = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), pm4_program),
(UPat(Ops.WAIT, src=(UPat.var("x") >= UPat.var("y"),)), pm4_wait),
(UPat(Ops.BARRIER), pm4_barrier),
(UPat(Ops.CUSTOM_FUNCTION, arg="timestamp", src=(UPat(name="dst"),)), pm4_timestamp),
(UPat(Ops.STORE, src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), pm4_wait),
(UPat(Ops.INS, arg="barrier"), pm4_barrier),
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), pm4_timestamp),
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
])
def pm4_submit(cmdbuf, devs):
@@ -184,10 +184,10 @@ def sdma_copy(ctx, call):
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz - off, ctx.max_copy_size) - 1), 0,
*data64_le(src_addr + off), *data64_le(dst_addr + off)) for off in range(0, sz, ctx.max_copy_size)]))
def sdma_wait(ctx, x, y):
def sdma_wait(ctx, dst, val):
op = ctx.sdma.SDMA_OP_POLL_REGMEM | ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(WAIT_REG_MEM_FUNCTION_GEQ) \
| ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(x.buf_uop, ctx.devs)), y, 0xffffffff,
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(dst, ctx.devs)), val, 0xffffffff,
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))
def sdma_store(ctx, dst, val):
@@ -202,10 +202,10 @@ def sdma_timestamp(ctx, dst):
pm_sdma_opsel = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
(UPat(Ops.BARRIER), lambda: UOp(Ops.NOOP, dtypes.void, ())),
(UPat(Ops.WAIT, src=(UPat.var("x") >= UPat.var("y"),)), sdma_wait),
(UPat(Ops.CUSTOM_FUNCTION, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
(UPat(Ops.STORE, src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), sdma_wait),
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
])
def sdma_submit(cmdbuf, devs):
+2 -2
View File
@@ -16,7 +16,7 @@ def _custom_fused_ce_loss_fwd(loss_out:UOp, max_out:UOp, lse_out:UOp, logits:UOp
row_lse = (logits[b, s, v_lse].cast(dtypes.float) - row_max).exp().reduce(v_lse, arg=Ops.ADD).log() + row_max
v_smooth = UOp.range(vocab, 3, axis_type=AxisType.REDUCE)
target = logits[b, s, targets[row].cast(dtypes.index)].cast(dtypes.float)
target = logits[b, s, targets[row].cast(dtypes.weakint)].cast(dtypes.float)
mean_logits = logits[b, s, v_smooth].cast(dtypes.float).reduce(v_smooth, arg=Ops.ADD) / vocab
loss = row_lse - (1.0 - label_smoothing) * target - label_smoothing * mean_logits
stores = UOp.group(loss_out[row].store(loss), max_out[row].store(row_max), lse_out[row].store(row_lse))
@@ -32,7 +32,7 @@ def _custom_fused_ce_loss_bwd(d_logits:UOp, logits:UOp, lse:UOp, targets:UOp, sc
s = row % seq
prob = (logits[b, s, v].cast(dtypes.float) - lse[row]).exp()
target = v.eq(targets[row].cast(dtypes.index)).where(1.0 - label_smoothing, 0.0)
target = v.eq(targets[row].cast(dtypes.weakint)).where(1.0 - label_smoothing, 0.0)
smooth = label_smoothing / vocab
grad = (prob - target - smooth) * scale[0]
@@ -48,7 +48,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
device = device[0].split(":")[0] if isinstance(device, tuple) else device.split(":")[0]
if device in {"AMD", "NULL"}: atomic_arg = "if ({2} > {3}) __hip_atomic_fetch_max((int*){0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);"
else: raise NotImplementedError(f"no atomic max for device {device}")
amax_idx = amax_out.reshape((1,)).index(UOp.const(dtypes.index, 0))
amax_idx = amax_out.reshape((1,)).index(UOp.const(dtypes.weakint, 0))
max_val = lds[0].load()
atomic = UOp(Ops.CUSTOM, dtypes.void, (amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=atomic_arg)
return atomic.end(tid, wg).sink(arg=KernelInfo(f"quantize_fp8_with_amax_{n_elems}", opts_to_apply=()))
-3
View File
@@ -74,7 +74,6 @@ testing_minimal = [
"torch==2.9.1",
"pytest",
"pytest-xdist",
"pytest-timeout",
"pytest-split",
"hypothesis>=6.148.9",
"z3-solver<4.15.4", # 4.15.4 has a segfault when creating many z3.Context()
@@ -160,8 +159,6 @@ norecursedirs = [
".hypothesis",
".git",
]
timeout = 300
timeout_func_only = true
testpaths = ["test"]
filterwarnings = [
# Ignore SWIG warnings from importlib
+1 -1
View File
@@ -36,7 +36,7 @@ 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.index, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
var = UOp.param(2, dtypes.weakint, 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
+1 -1
View File
@@ -290,7 +290,7 @@ class TestLinearizer(unittest.TestCase):
@unittest.skipIf(MOCKGPU and isinstance(Device[Device.DEFAULT].renderer, (PTXRenderer, CUDARenderer)), "PTX indexes differently. might be ok?")
def test_where_fold(self):
a = Tensor.ones(4, 4).contiguous().realize()
b = a.shrink(((1, 2), None)).pad(((1, 2), None))
b = a.shrink(((1, 2), None)).pad(((1, 2), None)).bool()
a.assign(b.where(2, a))
linear, var_vals = a.linear_with_vars()
assert len(linear.src) == 1
+9 -9
View File
@@ -12,18 +12,18 @@ class TestLinearizerFailure(unittest.TestCase):
@unittest.skipUnless(Device.DEFAULT == "METAL", "only tested on METAL")
def test_failure_beam_mnist(self):
c0 = UOp.param(0, dtypes.uchar, (4014080,))
c1 = UOp.range(UOp.const(dtypes.index, 512), 0, AxisType.GLOBAL)
c2 = UOp.range(UOp.const(dtypes.index, 784), 1, AxisType.GLOBAL)
c3 = UOp.range(UOp.const(dtypes.index, 10), 3, AxisType.GLOBAL)
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 0, AxisType.GLOBAL)
c2 = UOp.range(UOp.const(dtypes.weakint, 784), 1, AxisType.GLOBAL)
c3 = UOp.range(UOp.const(dtypes.weakint, 10), 3, AxisType.GLOBAL)
c4 = UOp.param(1, dtypes.int, (512,))
c5 = c4.index(c1.valid(UOp.const(dtypes.bool, True)))
c6 = UOp.range(UOp.const(dtypes.index, 6000), 1004, AxisType.REDUCE)
c7 = UOp.range(UOp.const(dtypes.index, 3750), 2006, AxisType.REDUCE)
c8 = UOp.range(UOp.const(dtypes.index, 16), 2007, AxisType.GROUP_REDUCE)
c6 = UOp.range(UOp.const(dtypes.weakint, 6000), 1004, AxisType.REDUCE)
c7 = UOp.range(UOp.const(dtypes.weakint, 3750), 2006, AxisType.REDUCE)
c8 = UOp.range(UOp.const(dtypes.weakint, 16), 2007, AxisType.GROUP_REDUCE)
c9 = UOp.param(2, dtypes.uchar, (47040000,))
c10 = c9.index((((c3*UOp.const(dtypes.index, 4704000))+c2)+(c6*UOp.const(dtypes.index, 784))).valid(UOp.const(dtypes.bool, True)))
c11 = c5.alu(Ops.CMPNE, ((((c3*UOp.const(dtypes.index, 6000))+c6)+((c7*UOp.const(dtypes.index, 16))+c8)).alu(Ops.CMPLT, UOp.const(dtypes.index, 59999)).where(UOp.const(dtypes.int, 0), UOp.const(dtypes.int, 1)).reduce(c7, c8, arg=Ops.ADD)+UOp.const(dtypes.int, -1))).where(UOp.const(dtypes.uchar, 0), c10).reduce(c6, arg=Ops.ADD)
c12 = c0.index((((c1*UOp.const(dtypes.index, 7840))+(c2*UOp.const(dtypes.index, 10)))+c3).valid(UOp.const(dtypes.bool, True))).store(c11).end(c1, c2, c3)
c10 = c9.index((((c3*UOp.const(dtypes.weakint, 4704000))+c2)+(c6*UOp.const(dtypes.weakint, 784))).valid(UOp.const(dtypes.bool, True)))
c11 = c5.alu(Ops.CMPNE, ((((c3*UOp.const(dtypes.weakint, 6000))+c6)+((c7*UOp.const(dtypes.weakint, 16))+c8)).alu(Ops.CMPLT, UOp.const(dtypes.weakint, 59999)).where(UOp.const(dtypes.int, 0), UOp.const(dtypes.int, 1)).reduce(c7, c8, arg=Ops.ADD)+UOp.const(dtypes.int, -1))).where(UOp.const(dtypes.uchar, 0), c10).reduce(c6, arg=Ops.ADD)
c12 = c0.index((((c1*UOp.const(dtypes.weakint, 7840))+(c2*UOp.const(dtypes.weakint, 10)))+c3).valid(UOp.const(dtypes.bool, True))).store(c11).end(c1, c2, c3)
ast = c12.sink(arg=KernelInfo(name='test', axis_types=(), dont_use_locals=False, applied_opts=(Opt(op=OptOps.GROUP, axis=1, arg=16),), opts_to_apply=None))
_ = to_program(ast, Device["METAL"].renderer)
+5 -12
View File
@@ -5,7 +5,6 @@ import torch
from tinygrad.helpers import getenv, DEBUG, DEV, IMAGE, Context
from tinygrad import Tensor, Device, dtypes
from tinygrad.tensor import _to_np_dtype
from tinygrad.renderer.cstyle import QCOMCLRenderer
from tinygrad.renderer.nir import NIRRenderer
TINY_BACKEND = getenv("TINY_BACKEND")
@@ -450,7 +449,6 @@ class TestOps(unittest.TestCase):
helper_test_op([(45,35), (45,35), (45,35)], lambda x,y,z: x.lerp(y,z))
helper_test_op(None, lambda x,y,z: x.lerp(y,z), vals=[[1.,2.,3.], [4.,5.,6.], 0.5])
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_tril(self):
helper_test_op([(3,3)], lambda x: x.tril())
helper_test_op([(3,3)], lambda x: x.tril(1))
@@ -468,7 +466,6 @@ class TestOps(unittest.TestCase):
helper_test_op([(5,3,3)], lambda x: x.tril(1))
helper_test_op(None, lambda x: x.tril(), vals=[[[True] * 3] * 3], forward_only=True)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_triu(self):
helper_test_op([(3,3)], lambda x: x.triu())
helper_test_op([(3,3)], lambda x: x.triu(1))
@@ -771,6 +768,11 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: torch.tensor([2], dtype=torch.int) ** torch.tensor(-2, dtype=torch.int),
lambda: Tensor([2]) ** Tensor(-2), forward_only=True)
def test_pow_int_base_float_exponent(self):
for exponent in (0.5, 1.5, 2.0, -1.0, 0.0):
helper_test_op([], lambda: torch.tensor([1, 2, 3, 4], dtype=torch.int) ** exponent,
lambda: Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent, forward_only=True)
def test_sqrt(self):
helper_test_op([(45,65)], lambda x: x.sqrt())
helper_test_op(None, lambda x: x.sqrt(), vals=[[0.0]])
@@ -791,7 +793,6 @@ class TestOps(unittest.TestCase):
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_xor(y), expected=RuntimeError)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_and(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -809,7 +810,6 @@ class TestOps(unittest.TestCase):
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_and(y), expected=RuntimeError)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_or(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -1229,7 +1229,6 @@ class TestOps(unittest.TestCase):
helper_test_op(None, lambda x: x.type(torch.int32).argmax().type(torch.int32), lambda x: x.argmax(), forward_only=True, vals=[[False, True]])
helper_test_op(None, lambda x: x.type(torch.int32).argmax().type(torch.int32), lambda x: x.argmax(), forward_only=True, vals=[[True, False]])
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_argmin(self):
# check if it returns the first index for multiple occurrences
helper_test_op(None, lambda x: x.argmin().type(torch.int32), lambda x: x.argmin(), forward_only=True, vals=[[2, 2]])
@@ -1535,7 +1534,6 @@ class TestOps(unittest.TestCase):
def test_prod_dtype_arg(self):
with self.assertRaises(AttributeError): Tensor([1.0, 2.0]).prod(dtype="")
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_min(self):
helper_test_op([(3,3)], lambda x: x.min())
helper_test_op([(45,3)], lambda x: x.min())
@@ -1575,7 +1573,6 @@ class TestOps(unittest.TestCase):
def test_any_zero_axis(self):
helper_test_op([(1,0,3,0,5)], lambda x: x.any(axis=(1,3)), forward_only=True)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_all(self):
helper_test_op([(3,4,5,6)], lambda x: x.all(), forward_only=True)
helper_test_op(None, lambda x: x.all(), vals=[[True, True]], forward_only=True)
@@ -2953,7 +2950,6 @@ class TestOps(unittest.TestCase):
helper_test_op([(2,5,6,5,3,4)], lambda x: x[...,c,:,e], lambda x: x[...,k,:,p])
@slow_test
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_slice_fancy_indexing_dim_collapse_int(self):
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
# dim collapse from int
@@ -2964,7 +2960,6 @@ class TestOps(unittest.TestCase):
helper_test_op([(2,5,6,5,3,4)], lambda x: x[1,:,3:11:2,d,0:2], lambda x: x[1,:,3:11:2,o,0:2])
@slow_test
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_slice_fancy_indexing_dim_inject_none(self):
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
# dim injection from None
@@ -2999,7 +2994,6 @@ class TestOps(unittest.TestCase):
lambda x: x[Tensor([[0,1,-1],[-1,-2,0]]), Tensor([2,1,-1])])
@slow_test
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_slice_fancy_indexing_list_indices(self):
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
helper_test_op([(2,5,6,5,3,4)], lambda x: x[((0,),)])
@@ -3011,7 +3005,6 @@ class TestOps(unittest.TestCase):
helper_test_op([(2,5,6,5,3,4)], lambda x: x[a,(2,1,0),c,(-2,1,0),e], lambda x: x[i,(2,1,0),k,(-2,1,0),p])
@slow_test
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, QCOMCLRenderer), "QCOM CL vectorized bool bug")
def test_slice_fancy_indexing_tuple_indices(self):
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
helper_test_op([(2,5,6,5,3,4)], lambda x: x[(((0,),),)], lambda x: x[(((0,),),)])
+19 -3
View File
@@ -1,7 +1,7 @@
import unittest, pickle, types
import unittest, pickle, types, tracemalloc
import numpy as np
from tinygrad import Tensor, TinyJit, Variable, dtypes
from tinygrad.helpers import GlobalCounters, ContextVar, Context
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
from tinygrad.uop.ops import PatternMatcher, UPat, UOp
class TestPickle(unittest.TestCase):
@@ -78,6 +78,22 @@ class TestPickle(unittest.TestCase):
a2:UOp = pickle.loads(s)
self.assertListEqual(a2.base.realized.as_memoryview().cast("I").tolist(), [0, 1, 2, 3])
@unittest.skipIf(DEV.interface.startswith("MOCK"), "mock device buffers live in host RAM, not VRAM")
def test_pickle_oob_ram(self):
N, M = 8, 10**6
ts = [Tensor.rand(M, dtype='float32').realize() for _ in range(N)]
tracemalloc.start()
st = pickle.dumps(ts, protocol=5, buffer_callback=lambda pb: pb.release())
self.assertLess(tracemalloc.get_traced_memory()[1], N*M*4)
tracemalloc.reset_peak()
def make_fake_buffers():
for _ in range(N):
Device[Device.DEFAULT].synchronize()
yield pickle.PickleBuffer(bytearray(M*4))
pickle.loads(st, buffers=make_fake_buffers())
self.assertLess(tracemalloc.get_traced_memory()[1], N*M*4)
tracemalloc.stop()
def test_pickle_unrealized_tensor(self):
t = Tensor.ones(10, 10)
st = pickle.dumps(t)
+104
View File
@@ -0,0 +1,104 @@
import unittest
from tinygrad import Tensor, UOp
from tinygrad.device import Device
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.renderer.nir import NIRRenderer
from tinygrad.renderer.isa.x86 import X86Renderer
from tinygrad.uop.ops import KernelInfo
def wait_loop_kernel(C:UOp) -> UOp:
N = 10
# a RANGE with no src is a bound-less loop header: a jump target with no induction variable.
# the compare and conditional backedge are expanded by the renderers from the loop RANGE/END
l = UOp.loop(0)
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
# i = 0
i = i.after(i[0].store(0))
# i + 1, read loop-carried through after(l)
inc = i.after(l)[0].load() + 1
# i = inc; END(store, l, cond): conditional backedge, loop again while inc < N (do-while)
# NOTE: the cond uses the computed value, not a reload of the register
st = i[0].store(inc)
i = i.after(st.end(l, inc < N))
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="wait_loop"))
def nested_loop_kernel(C:UOp) -> UOp:
r = UOp.range(4, 0)
l = UOp.loop(1)
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
i = i.after(i[0].store(0))
inc = i.after(l, r)[0].load() + 1
st = i[0].store(inc)
lend = st.end(l, inc < (r.cast(dtypes.int)+1)*3)
i = i.after(lend.end(r))
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="nested_loop", opts_to_apply=()))
def two_loops_kernel(C:UOp) -> UOp:
# two sequential loops on the same counter: ++ until 10, then ++ until 25
l1, l2 = UOp.loop(0), UOp.loop(1)
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
i = i.after(i[0].store(0))
inc1 = i.after(l1)[0].load() + 1
i = i.after(i[0].store(inc1).end(l1, inc1 < 10))
inc2 = i.after(l2)[0].load() + 1
i = i.after(i[0].store(inc2).end(l2, inc2 < 25))
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="two_loops", opts_to_apply=()))
def loop_in_loop_kernel(C:UOp) -> UOp:
# outer loop while i < 12, inner loop increments until i % 4 == 0 -> 12
l1, l2 = UOp.loop(0), UOp.loop(1)
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
i = i.after(i[0].store(0))
inc = i.after(l1, l2)[0].load() + 1
st = i[0].store(inc)
# the outer END closes the inner END, and its cond reloads the register after the inner loop (in scope at the outer level)
e2 = st.end(l2, inc % 4 != 0)
oc = i.after(e2)[0].load()
i = i.after(e2.end(l1, oc < 12))
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="loop_in_loop", opts_to_apply=()))
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, (NIRRenderer, X86Renderer)), "loops are not supported in LVP and X86")
class TestWaitLoop(unittest.TestCase):
def test_wait_loop(self):
c = Tensor.empty(1, dtype=dtypes.int)
c = Tensor.custom_kernel(c, fxn=wait_loop_kernel)[0]
c.realize()
self.assertEqual(c.item(), 10)
def test_nested_loop_in_range(self):
c = Tensor.empty(1, dtype=dtypes.int)
c = Tensor.custom_kernel(c, fxn=nested_loop_kernel)[0]
c.realize()
self.assertEqual(c.item(), 12)
def test_two_sequential_loops(self):
c = Tensor.empty(1, dtype=dtypes.int)
c = Tensor.custom_kernel(c, fxn=two_loops_kernel)[0]
c.realize()
self.assertEqual(c.item(), 25)
def test_loop_in_loop(self):
c = Tensor.empty(1, dtype=dtypes.int)
c = Tensor.custom_kernel(c, fxn=loop_in_loop_kernel)[0]
c.realize()
self.assertEqual(c.item(), 12)
if __name__ == "__main__": unittest.main()
+2 -2
View File
@@ -24,7 +24,7 @@ def vision_conv_143():
c32 = ((c27<3)!=True)&(c27<67)
c34 = UOp.param(1, dtypes.half, shape=(32, 1024, 4))
c38 = c5//2
c45 = (c32&c24).where((c27*64+c38+c17*4096+-12480), UOp.const(dtypes.index, Invalid))
c45 = (c32&c24).where((c27*64+c38+c17*4096+-12480), UOp.const(dtypes.weakint, Invalid))
c48 = (c24&c32).where(c34.index(c45), UOp.const(dtypes.float, 0.0))
c49 = UOp.param(2, dtypes.half, shape=(64, 49, 4))
c61 = c48*c49.index((c26*4+c5%2+c16*28+c38*196))
@@ -50,7 +50,7 @@ def vision_conv_153():
c32 = ((c27<3)!=True)&(c27<35)
c34 = UOp.param(1, dtypes.half, shape=(16, 1024, 4))
c38 = c5//2
c45 = (c32&c24).where((c27*128+c38+c17*4096+-12672), UOp.const(dtypes.index, Invalid))
c45 = (c32&c24).where((c27*128+c38+c17*4096+-12672), UOp.const(dtypes.weakint, Invalid))
c48 = (c24&c32).where(c34.index(c45), UOp.const(dtypes.float, 0.0))
c49 = UOp.param(2, dtypes.half, shape=(128, 49, 4))
c61 = c48*c49.index((c26*4+c5%2+c16*28+c38*196))
+3 -3
View File
@@ -40,7 +40,7 @@ def random_int_expr(depth=10):
def random_bool_expr(depth=10, expr1=None):
if depth == 0: return True
if expr1 is None: expr1 = random_int_expr(depth-1)
expr2 = random.choice([random_or_sub_expression_int(depth-1, expr1), UOp.const(dtypes.index, random.randint(-10, 10))])
expr2 = random.choice([random_or_sub_expression_int(depth-1, expr1), UOp.const(dtypes.weakint, random.randint(-10, 10))])
return random.choice(comp_ops)(expr1, expr2)
@@ -82,8 +82,8 @@ if __name__ == "__main__":
f"v2=Variable(\"{u2.arg[0]}\", {u2.arg[1]}, {u2.arg[2]})\n" +\
f"v3=Variable(\"{u3.arg[0]}\", {u3.arg[1]}, {u3.arg[2]})\n" +\
f"expr = {expr}\n" +\
f"v1_val, v2_val, v3_val = UOp.const(dtypes.index, {n1.as_long()}), UOp.const(dtypes.index, {n2.as_long()})," +\
f"UOp.const(dtypes.index, {n3.as_long()})\n" +\
f"v1_val, v2_val, v3_val = UOp.const(dtypes.weakint, {n1.as_long()}), UOp.const(dtypes.weakint, {n2.as_long()})," +\
f"UOp.const(dtypes.weakint, {n3.as_long()})\n" +\
"num = expr.simplify().substitute({v1:v1_val, v2:v2_val, v3:v3_val}).ssimplify()\n" +\
"rn = expr.substitute({v1:v1_val, v2:v2_val, v3:v3_val}).ssimplify()\n" +\
"assert num==rn, f\"{num} != {rn}\"\n"
+1 -1
View File
@@ -40,7 +40,7 @@ class TestWeakConstFolding(unittest.TestCase):
self.assertEqual((out.op, out.dtype, out.arg), (Ops.CONST, dtypes.weakint, 2**41))
def test_float_unaries(self):
for dtype in (dtypes.weakint, dtypes.weakfloat):
for dtype in (dtypes.weakfloat,):
for op in (Ops.SIN, Ops.LOG2, Ops.EXP2, Ops.SQRT, Ops.RECIPROCAL):
out = UOp.const(dtype, 4).alu(op).simplify()
self.assertEqual((out.op, out.dtype), (Ops.CONST, dtypes.weakfloat))
+20 -18
View File
@@ -224,30 +224,16 @@ class TestTypePromotion(unittest.TestCase):
assert least_upper_dtype(dtypes.fp8e5m2, dtypes.uint64) == dtypes.fp8e5m2
def test_weakint_promo(self):
# weakint with itself is weakint
assert least_upper_dtype(dtypes.weakint, dtypes.weakint) == dtypes.weakint
# weakint is above bool
assert least_upper_dtype(dtypes.weakint, dtypes.bool) == dtypes.weakint
# weakint defers to any concrete int type
assert least_upper_dtype(dtypes.weakint, dtypes.int8) == dtypes.int8
assert least_upper_dtype(dtypes.weakint, dtypes.uint8) == dtypes.uint8
assert least_upper_dtype(dtypes.weakint, dtypes.int16) == dtypes.int16
assert least_upper_dtype(dtypes.weakint, dtypes.int32) == dtypes.int32
assert least_upper_dtype(dtypes.weakint, dtypes.int64) == dtypes.int64
assert least_upper_dtype(dtypes.weakint, dtypes.uint64) == dtypes.uint64
# weakint defers to any float type
assert least_upper_dtype(dtypes.weakint, dtypes.float16) == dtypes.float16
assert least_upper_dtype(dtypes.weakint, dtypes.float32) == dtypes.float32
assert least_upper_dtype(dtypes.weakint, dtypes.float64) == dtypes.float64
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.weakint)
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.int8)
def test_weakfloat_promo(self):
# weakfloat is a float, but like weakint it is not one of dtypes.floats
# weakfloat is a float, but is not one of dtypes.floats
assert dtypes.is_float(dtypes.weakfloat) and dtypes.weakfloat not in dtypes.floats
# weakfloat with itself is weakfloat
assert least_upper_dtype(dtypes.weakfloat, dtypes.weakfloat) == dtypes.weakfloat
# weakfloat is above bool, weakint and any concrete int (they defer up to it)
# weakfloat is above bool and any concrete int (they defer up to it)
assert least_upper_dtype(dtypes.weakfloat, dtypes.bool) == dtypes.weakfloat
assert least_upper_dtype(dtypes.weakfloat, dtypes.weakint) == dtypes.weakfloat
assert least_upper_dtype(dtypes.weakfloat, dtypes.int32) == dtypes.weakfloat
assert least_upper_dtype(dtypes.weakfloat, dtypes.uint64) == dtypes.weakfloat
# weakfloat defers to any concrete float type
@@ -329,6 +315,19 @@ class TestAutoCastType(unittest.TestCase):
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
assert (Tensor.ones(4, 4, dtype=dt) + True).dtype == dt
@given(strat.sampled_from(core_dtypes))
def test_pad_scalar(self, dt):
t = Tensor.ones(4, dtype=dt)
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
assert t.pad(((1, 1),), value=True).dtype == dt
@given(strat.sampled_from(core_dtypes))
def test_sort(self, dt):
# sort pads with dtype.min/max, a scalar of its own dtype
assert Tensor([3, 1, 2], dtype=dt).sort()[0].dtype == dt
assert Tensor([3, 1, 2], dtype=dt).sort(descending=True)[0].dtype == dt
@given(strat.sampled_from(dtype_floats))
def test_int_div_int(self, default_float):
dtypes.default_float = default_float
@@ -429,6 +428,9 @@ class TestAutoCastType(unittest.TestCase):
self.check_where_alternate_input_other(3.1, True, dtypes.default_float)
self.check_where_alternate_input_other(3, 2, dtypes.default_int)
self.check_where_alternate_input_other(3, True, dtypes.default_int)
def test_where_non_bool_cond_raises(self):
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0)
self.check_where_alternate_input_other(False, True, dtypes.bool)
@given(strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes))
+1 -1
View File
@@ -24,7 +24,7 @@ class TestGroupedDims(unittest.TestCase):
total = math.prod(dims)
specials = sorted(dedup(flatten([[y for y in x.toposort() if y.op is Ops.SPECIAL] for x in idxs])), key=lambda u: u.arg)
# build flat index and primed flat (same expression with renamed SPECIALs)
flat = UOp.const(dtypes.index, 0)
flat = UOp.const(dtypes.weakint, 0)
for i, idx in enumerate(idxs):
flat = flat + idx * int(math.prod(dims[i+1:]))
flat_p = flat.substitute({s: UOp(Ops.SPECIAL, src=s.src, arg=s.arg+"_p") for s in specials})
+4 -4
View File
@@ -107,21 +107,21 @@ class TestFoldingAndReduction(unittest.TestCase):
class TestModuloAndDivisionFolding(unittest.TestCase):
def test_full_graph_rewrite_modulo_folding_with_define_var(self):
# index dtype because div-mod rules only work on index
x_var_uop = UOp.variable('x', 0, 100).cast(dtypes.index)
x_var_uop = UOp.variable('x', 0, 100).cast(dtypes.weakint)
optimized_mod_uop = apply_rewrite(((x_var_uop * 4) + 2) % 4)
self.assertEqual(optimized_mod_uop.op, Ops.CONST)
self.assertEqual(optimized_mod_uop.arg, 2)
def test_full_graph_rewrite_division_folding_with_define_var(self):
# index dtype because div-mod rules only work on index
n_var_uop = UOp.variable('n', 1, 1000).cast(dtypes.index)
n_var_uop = UOp.variable('n', 1, 1000).cast(dtypes.weakint)
optimized_div_uop = apply_rewrite((n_var_uop * 6) // 3)
self.assertEqual(optimized_div_uop.op, Ops.MUL)
self.assertEqual(optimized_div_uop.src[1].arg, 2)
def test_full_graph_rewrite_complex_mod_div_folding(self):
# index dtype because div-mod rules only work on index
k_var_uop = UOp.variable('k', 0, 50).cast(dtypes.index)
k_var_uop = UOp.variable('k', 0, 50).cast(dtypes.weakint)
optimized_div_uop = apply_rewrite(((k_var_uop * 12 + 8) % 6) // 2)
self.assertEqual(optimized_div_uop.op, Ops.CONST)
self.assertEqual(optimized_div_uop.arg, 1)
@@ -140,7 +140,7 @@ class TestModuloAndDivisionFolding(unittest.TestCase):
def test_full_graph_rewrite_modulo_large_divisor(self):
# index dtype because div-mod rules only work on index
x_var_uop = UOp.variable('x', 1, 5)
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.index) % 10).render(simplify=False), x_var_uop.render(simplify=False))
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.weakint) % 10).render(simplify=False), x_var_uop.render(simplify=False))
def test_full_graph_rewrite_division_with_remainder(self):
x_var_uop = UOp.variable('x', 7, 9)
+5 -5
View File
@@ -8,12 +8,12 @@ from tinygrad.codegen import to_program
class TestLinearizerFailures(unittest.TestCase):
def test_fail_1(self):
c0 = UOp.param(0, dtypes.float, (64,))
c1 = UOp.range(UOp.const(dtypes.index, 2), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.index, 32), 2, AxisType.LOOP)
c3 = ((c1*UOp.const(dtypes.index, 32))+c2)
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.LOOP)
c3 = ((c1*UOp.const(dtypes.weakint, 32))+c2)
c4 = UOp.param(1, dtypes.float, (163840,))
c5 = UOp.range(UOp.const(dtypes.index, 2560), 0, AxisType.REDUCE)
c6 = c4.index(((((((c5//UOp.const(dtypes.index, 8))%UOp.const(dtypes.index, 8))*UOp.const(dtypes.index, 8))+(c5%UOp.const(dtypes.index, 8)))+(((c2*UOp.const(dtypes.index, 40))+(c5//UOp.const(dtypes.index, 64)))*UOp.const(dtypes.index, 64)))+(c1*UOp.const(dtypes.index, 81920))))
c5 = UOp.range(UOp.const(dtypes.weakint, 2560), 0, AxisType.REDUCE)
c6 = c4.index(((((((c5//UOp.const(dtypes.weakint, 8))%UOp.const(dtypes.weakint, 8))*UOp.const(dtypes.weakint, 8))+(c5%UOp.const(dtypes.weakint, 8)))+(((c2*UOp.const(dtypes.weakint, 40))+(c5//UOp.const(dtypes.weakint, 64)))*UOp.const(dtypes.weakint, 64)))+(c1*UOp.const(dtypes.weakint, 81920))))
c7 = UOp.param(2, dtypes.float, (64,))
c8 = c7.index(c3)
c9 = ((((c6+(c8*UOp.const(dtypes.float, -1.0)))*(c6+(c8*UOp.const(dtypes.float, -1.0)))).reduce(c5, arg=Ops.ADD)*UOp.const(dtypes.float, 0.000390625))+UOp.const(dtypes.float, 1e-05)).sqrt().reciprocal()
+14 -14
View File
@@ -1,6 +1,6 @@
import unittest, itertools
from tinygrad.codegen.late.coalese import indexing_simplify
from tinygrad.codegen.late.coalesce import indexing_simplify
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import UOp, Ops, graph_rewrite
from tinygrad.uop.symbolic import simplify_valid, sym, pm_move_where_on_load
@@ -23,7 +23,7 @@ def get_load_image_uop(image_shape:tuple[int, ...], valid:UOp, idx:tuple[UOp, UO
UOp.param(0, dtypes.float, image_shape).index(idx[1].valid(valid), idx[0].valid(valid)),
))
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(dtypes.index, nmax),), arg=expr)
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(dtypes.weakint, nmax),), arg=expr)
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax)
def Range(n, nmax): return UOp.range(nmax, n)
@@ -455,7 +455,7 @@ class TestImageSimplification(unittest.TestCase):
A1 = lidx0*32 + r0*32 + lidx1*4 - 99
valid = ((lidx1 < 1).ne(True)) & ((lidx0 + r0) < 3).ne(True) & ((lidx0 + r0) < 19)
alu0 = gidx0 + (A1 % 32)*32 + (A1 // 32 % 16)*1024
load = get_load_image_uop((1, 16384, 4), valid, (alu0, UOp.const(dtypes.index, 0)))
load = get_load_image_uop((1, 16384, 4), valid, (alu0, UOp.const(dtypes.weakint, 0)))
try:
self.check(load, None, "(gidx0+lidx0*1024+r0*1024+lidx1*128+-3168)", "0")
except AssertionError:
@@ -474,7 +474,7 @@ class TestImageSimplification(unittest.TestCase):
A1 = lidx0*16 + r0*16 + lidx1*4 - 51
valid = ((lidx1 < 1).ne(True)) & ((lidx0 + r0) < 3).ne(True) & ((lidx0 + r0) < 11)
alu0 = lidx2 + gidx0*4 + (A1 % 16)*64 + (A1 // 16 % 8)*1024
load = get_load_image_uop((1, 8192, 4), valid, (alu0, UOp.const(dtypes.index, 0)))
load = get_load_image_uop((1, 8192, 4), valid, (alu0, UOp.const(dtypes.weakint, 0)))
try:
self.check(load, None, "(lidx2+gidx0*4+lidx0*1024+r0*1024+lidx1*256+-3264)", "0")
except AssertionError:
@@ -488,18 +488,18 @@ class TestImageSimplification(unittest.TestCase):
gidx0 = Special("gidx0", 1064)
r12 = Range(12, 3)
valid = ((gidx0 < 645).ne(True)) & (gidx0 < 653)
idx = (r12*4 + (gidx0+3)%4 + (gidx0+3)//4*24 - 3888, UOp.const(dtypes.index, 0))
idx = (r12*4 + (gidx0+3)%4 + (gidx0+3)//4*24 - 3888, UOp.const(dtypes.weakint, 0))
load = get_load_image_uop((1, 48, 4), valid, idx)
self.check(load, None, "(r12*4+(gidx0+3)%4+(gidx0+3)//4*24+-3888)", "0")
class TestDropTrueGate(unittest.TestCase):
def test_drop_true_gate_on_index(self):
# test that INDEX with a constant True valid gets simplified to drop the valid
from tinygrad.codegen.late.coalese import indexing_simplify
from tinygrad.codegen.late.coalesce import indexing_simplify
from tinygrad.uop.ops import graph_rewrite
from tinygrad.uop.symbolic import sym
buf = UOp.param(0, dtypes.int, (1,))
idx = UOp.const(dtypes.index, 0)
idx = UOp.const(dtypes.weakint, 0)
true_gate = UOp.const(dtypes.bool, True)
index_with_gate = UOp(Ops.INDEX, src=(buf, idx.valid(true_gate)))
# apply the optimization
@@ -516,7 +516,7 @@ class TestRangeShrink(unittest.TestCase):
def test_range_shrink_single_guard(self):
# range 0..203 guarded by r < 4 everywhere -> shrink to 0..3
r = Range(0, 204)
load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
ranges = self.get_ranges(load.sink())
self.assertEqual(len(ranges), 1)
self.assertEqual(ranges[0].src[0].arg, 4)
@@ -524,8 +524,8 @@ class TestRangeShrink(unittest.TestCase):
def test_range_shrink_picks_max_guard(self):
# two loads guard the same range with r < 4 and r < 8 -> shrink to max(4, 8) = 8
r = Range(0, 204)
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
load2 = get_gated_load_uop(r < UOp.const(dtypes.index, 8), r)
load1 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
load2 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 8), r)
ranges = self.get_ranges(UOp.sink(load1, load2))
self.assertEqual(len(ranges), 1)
self.assertEqual(ranges[0].src[0].arg, 8)
@@ -533,7 +533,7 @@ class TestRangeShrink(unittest.TestCase):
def test_range_no_shrink_guard_ge_max(self):
# guard r < 300 with range max 204 -> no shrink (guard doesn't constrain)
r = Range(0, 204)
load = get_gated_load_uop(r < UOp.const(dtypes.index, 300), r)
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 300), r)
ranges = self.get_ranges(load.sink())
self.assertEqual(len(ranges), 1)
self.assertEqual(ranges[0].src[0].arg, 204)
@@ -541,7 +541,7 @@ class TestRangeShrink(unittest.TestCase):
def test_range_no_shrink_when_unguarded_elsewhere(self):
# one load guards r < 4, but another load uses r without a gate -> no shrink
r = Range(0, 204)
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
load1 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
load2 = UOp(Ops.LOAD, src=(UOp.param(1, dtypes.float, (204,)).index(r),))
ranges = self.get_ranges(UOp.sink(load1, load2))
self.assertEqual(len(ranges), 1)
@@ -550,7 +550,7 @@ class TestRangeShrink(unittest.TestCase):
def test_range_no_shrink_when_used_in_reduce(self):
# range used in both a gated load AND directly in the reduce expression -> no shrink
r = Range(0, 204)
gated_load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
gated_load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
red = (r.cast(dtypes.float) + gated_load).reduce(r, arg=Ops.ADD)
ranges = self.get_ranges(red.sink())
self.assertEqual(len(ranges), 1)
@@ -559,7 +559,7 @@ class TestRangeShrink(unittest.TestCase):
def test_range_shrink_to_single_iteration(self):
# guard r < 1 shrinks range to 1 -> single iteration, range eliminated entirely
r = Range(0, 204)
load = get_gated_load_uop(r < UOp.const(dtypes.index, 1), r)
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 1), r)
ranges = self.get_ranges(load.sink())
self.assertEqual(len(ranges), 0)
+1 -1
View File
@@ -382,7 +382,7 @@ class TestTensorUOpStack(unittest.TestCase):
self.assertIs(_t(2, 3).uop.stack(w.uop).dtype, dtypes.float32)
def test_stack_index_dtype(self):
# index is outside the promotion lattice, equal dtypes bypass promotion
self.assertEqual(UOp.const(dtypes.index, 1).stack(UOp.const(dtypes.index, 2)).shape, (2,))
self.assertEqual(UOp.const(dtypes.weakint, 1).stack(UOp.const(dtypes.weakint, 2)).shape, (2,))
class TestTensorUOpConv2d(unittest.TestCase):
def test_conv2d_basic(self):
+25 -14
View File
@@ -2,7 +2,7 @@ import unittest, pytest
from tinygrad import dtypes, Variable
from tinygrad.dtype import AddrSpace
from tinygrad.helpers import DEBUG, Context
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType, broadcast_axes
from tinygrad.uop.symbolic import sym
from test.helpers import to_uops_list
@@ -202,7 +202,7 @@ class TestUOpGraph(unittest.TestCase):
def test_where_same_fold(self):
v = UOp.variable('tmp', 0, 1)
c0 = UOp.const(dtypes.index, 0)
c0 = UOp.const(dtypes.weakint, 0)
vc = v != c0
c1 = UOp.const(dtypes.float, 1.0)
out = vc.where(c1, c1)
@@ -424,16 +424,16 @@ class TestUOpGraph(unittest.TestCase):
# mnist indexing with split reduceop
# Make sure we are not doign math on the loaded index, which would promote it to long
c0 = UOp.param(0, dtypes.uchar, (128000,))
c1 = UOp.range(UOp.const(dtypes.index, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.index, 250), 2, AxisType.LOOP)
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
c3 = UOp.param(1, dtypes.int, (512,))
c4 = c3.index(c1)
c5 = UOp.range(UOp.const(dtypes.index, 240), 0, AxisType.REDUCE)
c6 = ((c2*UOp.const(dtypes.index, 240))+c5)
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
c6 = ((c2*UOp.const(dtypes.weakint, 240))+c5)
c7 = UOp.param(2, dtypes.uchar, (60000,))
c8 = c7.index(c6)
c9 = ((c4<0).where((c4+60000), c4)!=c6.cast(dtypes.int)).where(0, c8.cast(dtypes.uint).cast(dtypes.uchar)).reduce(c5, arg=Ops.ADD)
c10 = c0.index(((c1*UOp.const(dtypes.index, 250))+c2)).store(c9).end(c1, c2)
c10 = c0.index(((c1*UOp.const(dtypes.weakint, 250))+c2)).store(c9).end(c1, c2)
uops = to_uops_list([c10])
for u in uops:
self.assertNotEqual(u.dtype, dtypes.long)
@@ -441,19 +441,19 @@ class TestUOpGraph(unittest.TestCase):
def test_load_idx_no_math_on_loaded(self):
# test the (x+y)<c pattern where x has loads - we shouldn't do math on loaded indices
c0 = UOp.param(0, dtypes.uchar, (128000,))
c1 = UOp.range(UOp.const(dtypes.index, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.index, 250), 2, AxisType.LOOP)
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
c3 = UOp.param(1, dtypes.int, (512,))
c4 = c3.index(c1) # c4 is a load
c5 = UOp.range(UOp.const(dtypes.index, 240), 0, AxisType.REDUCE)
c6 = ((c2*UOp.const(dtypes.index, 240))+c5)
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
c6 = ((c2*UOp.const(dtypes.weakint, 240))+c5)
c7 = UOp.param(2, dtypes.uchar, (60000,))
c8 = c7.index(c6)
# (loaded + range) < const pattern - loaded value shouldn't be promoted to long
loaded_idx = c4.cast(dtypes.index)
comparison = (loaded_idx + c5) < UOp.const(dtypes.index, 60000)
loaded_idx = c4.cast(dtypes.weakint)
comparison = (loaded_idx + c5) < UOp.const(dtypes.weakint, 60000)
c9 = comparison.where(c8.cast(dtypes.uint).cast(dtypes.uchar), 0).reduce(c5, arg=Ops.ADD)
c10 = c0.index(((c1*UOp.const(dtypes.index, 250))+c2)).store(c9).end(c1, c2)
c10 = c0.index(((c1*UOp.const(dtypes.weakint, 250))+c2)).store(c9).end(c1, c2)
uops = to_uops_list([c10])
for u in uops:
self.assertNotEqual(u.dtype, dtypes.long)
@@ -707,5 +707,16 @@ class TestUOpBroadcast(unittest.TestCase):
c = a + b
self.assertEqual(c.op, Ops.ADD)
def test_broadcast_axes(self):
t = Variable("t", 1, 10)
self.assertEqual(broadcast_axes((4, 8), (4, 8)), ())
self.assertEqual(broadcast_axes((8,), (4, 8)), (0,))
self.assertEqual(broadcast_axes((), (4, 8)), (0, 1))
self.assertEqual(broadcast_axes((3, 1), (4, 3, 8)), (0, 2))
self.assertEqual(broadcast_axes((1, 8), (1, 8)), ())
self.assertEqual(broadcast_axes((t, 8), (t, 8)), ())
self.assertEqual(broadcast_axes((1, 8), (t, 8)), (0,))
with self.assertRaises(RuntimeError): broadcast_axes((4, 8), (8,))
if __name__ == '__main__':
unittest.main(verbosity=2)
+12 -12
View File
@@ -11,13 +11,13 @@ from tinygrad.uop.validate import uops_to_z3
def check_uop_against_string(self, v:UOp, s:str):
sym_vars = {v.render():v for v in v.toposort() if v.op in (Ops.RANGE, Ops.SPECIAL, Ops.PARAM)}
s_eval = eval(s, sym_vars)
if isinstance(s_eval, int) and v.dtype==dtypes.index: s_eval = UOp.const(dtypes.index, s_eval)
if isinstance(s_eval, int) and v.dtype==dtypes.weakint: s_eval = UOp.const(dtypes.weakint, s_eval)
elif isinstance(s_eval, (bool, int, float)): s_eval = UOp.const(dtypes.from_py(s_eval), s_eval)
s_eval = graph_rewrite(s_eval, commutative, name="cannonicalize eval")
self.assertIs(s_eval, v, f"eval did not match simplified: {s_eval} != {v.render()} for {s}")
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.index): return UOp.variable(name,min_val,max_val,dtype)
def uconst(val): return UOp.const(dtypes.index, val)
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint): return UOp.variable(name,min_val,max_val,dtype)
def uconst(val): return UOp.const(dtypes.weakint, val)
def usum(ops): return functools.reduce(lambda x,y: x+y, ops)
def uand(ops): return functools.reduce(lambda x,y: x*y, ops)
@@ -247,12 +247,12 @@ class TestSymbolic(unittest.TestCase):
self.assertEqual((Variable("x", -10, 0)%Variable("y", 1, 10))._min_max, (0, 9))
def test_range_div_its_symbolic_bound(self):
a = Variable("a", 1, 10, dtypes.index)
a = Variable("a", 1, 10, dtypes.weakint)
ridx0 = UOp.range(a+2, 0)
self.helper_test_variable(ridx0//(a+2), 0, 0, "0")
def test_range_mod_its_symbolic_bound(self):
a = Variable("a", 1, 10, dtypes.index)
a = Variable("a", 1, 10, dtypes.weakint)
ridx = UOp.range(a+2, 0)
self.helper_test_variable(ridx%(a+2), 0, 11, "r0")
@@ -919,8 +919,8 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(cond.cast(dtypes.int).ne(2), 1, 1, "True")
self.helper_test_variable(cond.cast(dtypes.int).ne(-1), 1, 1, "True")
# CAST(bool -> index) folds too
self.helper_test_variable(cond.cast(dtypes.index).ne(0), 0, 1, "(a<2)")
self.helper_test_variable(cond.cast(dtypes.index).ne(1), 0, 1, "((a<2)!=True)")
self.helper_test_variable(cond.cast(dtypes.weakint).ne(0), 0, 1, "(a<2)")
self.helper_test_variable(cond.cast(dtypes.weakint).ne(1), 0, 1, "((a<2)!=True)")
def test_where_removal(self):
cond = Variable("a", 0, 3) < 2
@@ -1021,7 +1021,7 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable((numerator//denominator)<=0, 1, 1, "True")
def test_symbolic_range_doesnt_collapse(self):
r0 = UOp.range((Variable("a", 1, 10)<5).cast(dtypes.index), 0)
r0 = UOp.range((Variable("a", 1, 10)<5).cast(dtypes.weakint), 0)
self.helper_test_variable(r0, 0, 0, "r0")
def test_const_reciprocal(self):
@@ -1289,16 +1289,16 @@ class TestInvalidIndex(unittest.TestCase):
self.assertIs((UOp.invalid()<Variable("a",0,10)).simplify().dtype, dtypes.bool)
def test_alu_invalid_vconst(self):
c1 = UOp.const(dtypes.index, (1, 1, Invalid, Invalid))
c2 = UOp.const(dtypes.index, (1, Invalid, 1, 1))
self.assertIs((c1+c2).simplify(), UOp.const(dtypes.index, (2, Invalid, Invalid, Invalid)))
c1 = UOp.const(dtypes.weakint, (1, 1, Invalid, Invalid))
c2 = UOp.const(dtypes.weakint, (1, Invalid, 1, 1))
self.assertIs((c1+c2).simplify(), UOp.const(dtypes.weakint, (2, Invalid, Invalid, Invalid)))
class TestStoreLoadFolding(unittest.TestCase):
"""Tests for store(index, load(index)) -> NOOP rule. This rule matches patterns that EMERGE during simplification."""
def test_store_load_folding(self):
# store(idx, load(idx)) -> NOOP, including emergent patterns like store(idx, load(idx) + 0)
buf = UOp.param(0, dtypes.int, (1,))
index = buf.index(UOp.const(dtypes.index, 0))
index = buf.index(UOp.const(dtypes.weakint, 0))
# Direct: store(idx, load(idx)) -> NOOP
self.assertEqual(graph_rewrite(index.store(index.load()), sym).op, Ops.NOOP)
# Emergent: store(idx, load(idx) + 0) -> store(idx, load(idx)) -> NOOP
+15 -1
View File
@@ -167,7 +167,7 @@ class TestVminVmaxProperties(unittest.TestCase):
self.assertNotEqual(i.vmin, i.vmax)
def test_vmin_vmax_invalid_vconst(self):
x = UOp.const(dtypes.index, (0, 4, Invalid, Invalid))
x = UOp.const(dtypes.weakint, (0, 4, Invalid, Invalid))
self.assertLess(x.vmin, 0)
self.assertGreater(x.vmax, 4)
@@ -371,6 +371,10 @@ class TestConstFactor(unittest.TestCase):
uop = (x * 3) * 5
self.assertEqual(uop.const_factor(), 15) # Constant multipliers are combined (3 * 5 = 15)
def test_const_factor_variable_multiple_of(self):
x = UOp.variable('x', 16, 32, multiple_of=4)
self.assertEqual(x.const_factor(), 4)
class TestDivides(unittest.TestCase):
def test_divides_constant_exact(self):
# Divides a constant by an exact divisor
@@ -409,5 +413,15 @@ class TestDivides(unittest.TestCase):
result = uop.divides(3)
self.assertIsNone(result) # Cannot divide by 3, since 4 is not divisible by 3
def test_divides_variable_multiple_of_exact(self):
x = UOp.variable('x', 16, 32, multiple_of=4)
result = x.divides(4)
self.assertIsNotNone(result)
def test_divides_variable_multiple_of_factor(self):
x = UOp.variable('x', 16, 32, multiple_of=4)
result = x.divides(2)
self.assertIsNotNone(result)
if __name__ == '__main__':
unittest.main()
+11 -13
View File
@@ -14,28 +14,26 @@ class TestDTypeFromUOp(unittest.TestCase):
def test_broadcastable_promotion(self):
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
self.assertEqual(dtype_from_uop(Ops.MUL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.int32, 1)), None), dtypes.int32)
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None), dtypes.int8)
with self.assertRaises(KeyError): dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None)
def test_same_dtype_fast_path(self):
src = (UOp.const(dtypes.index, 1), UOp.const(dtypes.index, 2))
self.assertEqual(dtype_from_uop(Ops.ADD, src, None), dtypes.index)
src = (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.weakint, 2))
self.assertEqual(dtype_from_uop(Ops.ADD, src, None), dtypes.weakint)
def test_where_promotion(self):
cond = UOp.const(dtypes.bool, True)
self.assertEqual(dtype_from_uop(Ops.WHERE, (cond, UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
idx = UOp.range(4, 0)
self.assertEqual(idx.valid(idx < 4).dtype, dtypes.index)
self.assertEqual(idx.valid(idx < 4).dtype, dtypes.weakint)
def test_const_dtype_from_value(self):
self.assertEqual(dtype_from_uop(Ops.CONST, (), True), dtypes.bool)
self.assertEqual(dtype_from_uop(Ops.CONST, (), 3), dtypes.weakint)
self.assertEqual(dtype_from_uop(Ops.CONST, (), ConstFloat(3.0)), dtypes.weakfloat)
self.assertEqual(dtype_from_uop(Ops.CONST, (), Invalid), dtypes.bool)
self.assertRaises(TypeError, dtype_from_uop, Ops.CONST, (), (1, 2))
@Context(SPEC=2)
def test_const_default_dtype_is_derived(self):
self.assertEqual(UOp(Ops.CONST, arg=3).dtype, dtypes.weakint)
self.assertEqual(UOp(Ops.CONST, arg=ConstFloat(3.0)).dtype, dtypes.weakfloat)
self.assertEqual(UOp(Ops.CONST, arg=True).dtype, dtypes.bool)
self.assertEqual(UOp(Ops.CONST, arg=Invalid).dtype, dtypes.bool)
@@ -81,7 +79,7 @@ class TestExecALU(unittest.TestCase):
# Invalid poisons any binary op regardless of result dtype: a comparison must not fold to a boolean
self.assertIs(exec_alu(Ops.CMPLT, dtypes.bool, (Invalid, 1)), Invalid)
self.assertIs(exec_alu(Ops.CMPNE, dtypes.bool, (Invalid, 1)), Invalid)
self.assertIs(exec_alu(Ops.ADD, dtypes.index, (Invalid, 1)), Invalid)
self.assertIs(exec_alu(Ops.ADD, dtypes.weakint, (Invalid, 1)), Invalid)
def test_div(self):
self.assertEqual(exec_alu(Ops.CDIV, dtypes.int8, (8, 2)), 4)
@@ -155,8 +153,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
def test_tiny_gate_store(self):
gmem = UOp.param(0, dtypes.float, (8,))
gidx0 = UOp.special(4, 'gidx0')
gate = gidx0<UOp.const(dtypes.index, 1)
idx = UOp(Ops.INDEX, src=(gmem, (gidx0 * UOp.const(dtypes.index, 2)).valid(gate)))
gate = gidx0<UOp.const(dtypes.weakint, 1)
idx = UOp(Ops.INDEX, src=(gmem, (gidx0 * UOp.const(dtypes.weakint, 2)).valid(gate)))
val = UOp.const(dtypes.float, 42.0)
store = UOp(Ops.STORE, src=(idx, val))
uops = to_uops_list([store])
@@ -172,8 +170,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
gmem0 = UOp.param(0, dtypes.float, (8,))
gmem1 = UOp.param(1, dtypes.float, (8,))
gidx0 = UOp.special(4, 'gidx0')
idx = gidx0 * UOp.const(dtypes.index, 2)
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gidx0<UOp.const(dtypes.index, 1))))
idx = gidx0 * UOp.const(dtypes.weakint, 2)
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gidx0<UOp.const(dtypes.weakint, 1))))
idx1 = UOp(Ops.INDEX, src=(gmem1, idx))
val = UOp.const(dtypes.float, 42.0)
stores = [UOp.store(idx0, val), UOp.store(idx1, val)]
@@ -192,8 +190,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
gmem0 = UOp.param(0, dtypes.float, (8,))
gmem1 = UOp.param(1, dtypes.float, (8,))
gidx0 = UOp.special(4, 'gidx0')
idx = gidx0*UOp.const(dtypes.index, 2)
gate = gidx0<UOp.const(dtypes.index, 1)
idx = gidx0*UOp.const(dtypes.weakint, 2)
gate = gidx0<UOp.const(dtypes.weakint, 1)
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gate)))
idx1 = UOp(Ops.INDEX, src=(gmem1, idx.valid(gate)))
val = UOp.const(dtypes.float, 42.0)
+2 -2
View File
@@ -126,7 +126,7 @@ class TestValidateOOB(unittest.TestCase):
buf0 = UOp.param(0, dtypes.int, (16,))
buf1 = UOp.param(1, dtypes.int, (64,))
r = UOp.range(42, 0, AxisType.GLOBAL)
ld0 = buf0.index(r.valid(r < 8)).load(dtype=dtypes.int).cast(dtypes.index)
ld0 = buf0.index(r.valid(r < 8)).load(dtype=dtypes.int).cast(dtypes.weakint)
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 32))).load(dtype=dtypes.int)]) # valid
with self.assertRaises(RuntimeError):
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 64))).load(dtype=dtypes.int)]) # oob
@@ -135,7 +135,7 @@ class TestValidateOOB(unittest.TestCase):
with Context(CHECK_OOB=1, SPEC=2):
buf_bool = UOp.param(0, dtypes.bool, (16,))
buf_int = UOp.param(1, dtypes.int, (8,))
gidx = UOp(Ops.SPECIAL, src=(UOp.const(dtypes.index, 16),), arg="gidx0")
gidx = UOp(Ops.SPECIAL, src=(UOp.const(dtypes.weakint, 16),), arg="gidx0")
ld_bool = buf_bool.index(gidx).load()
with self.assertRaises(RuntimeError):
to_uops_list([buf_int.index(gidx.valid(ld_bool)).load()]) # gidx 0..15, buf_int size 8
+27 -1
View File
@@ -1,5 +1,5 @@
import unittest
from tinygrad import Device, Tensor, dtypes
from tinygrad import Device, Tensor, Variable, dtypes
from tinygrad.uop.ops import UOp, Ops
from tinygrad.codegen import to_program
from tinygrad.codegen.opt import Opt, OptOps
@@ -114,5 +114,31 @@ class TestFloat4(unittest.TestCase):
assert TestFloat4.count_float4(uops) == (1, 1)
def test_float4_aligned_variable(self):
x = Variable('x', 0, 4, multiple_of=4).bind(4)
a = Tensor.empty(4).realize()
b = Tensor.empty(12).realize().shrink(((x, x+4),))
c = a + b
# should float4 both
s = c.linear_with_vars()[0].src[0]
uops = tuple(to_program(replace_opts(s.src[0], [Opt(op=OptOps.UPCAST, axis=0, arg=4)]), renderer=Device[Device.DEFAULT].renderer).src[1].src)
assert TestFloat4.count_float4(uops) == (2, 1)
def test_float4_unaligned_variable(self):
x = Variable('x', 0, 4, multiple_of=2).bind(4)
a = Tensor.empty(4).realize()
b = Tensor.empty(12).realize().shrink(((x, x+4),))
c = a + b
# should float4 a but not b
s = c.linear_with_vars()[0].src[0]
uops = tuple(to_program(replace_opts(s.src[0], [Opt(op=OptOps.UPCAST, axis=0, arg=4)]), renderer=Device[Device.DEFAULT].renderer).src[1].src)
assert TestFloat4.count_float4(uops) == (1, 1)
if __name__ == '__main__':
unittest.main()
+9 -4
View File
@@ -18,6 +18,9 @@ from test.backend.test_linearizer import helper_realized_ast, helper_linearizer_
# NOTE: to_program always passes in Device[Device.DEFAULT].renderer explicitly for process_replay!!!
def _tc_rand(*shape, dtype:DType) -> Tensor:
return Tensor.randint(*shape, low=dtype.min, high=dtype.max+1, dtype=dtype) if dtypes.is_int(dtype) else Tensor.rand(*shape, dtype=dtype)
def run_program(prg:UOp, bufs:list[Buffer]):
buf_uops = [UOp.new_buffer(b.device, b.size, b.dtype) for b in bufs]
for u,b in zip(buf_uops, bufs): buffers[u] = b
@@ -25,7 +28,7 @@ def run_program(prg:UOp, bufs:list[Buffer]):
def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0,
ensure_triggered:bool=True):
a, b = Tensor.rand(M, K, dtype=dtype_in), Tensor.rand(K, N, dtype=dtype_in)
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
r = a.matmul(b, dtype=dtype_out)
sched = r.schedule_linear()
realized_ast = sched.src[-1].src[0]
@@ -44,7 +47,7 @@ def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, d
except KernelOptError: pass
def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0, use_tensor_cores:int=1):
a, b = Tensor.rand(M, K, dtype=dtype_in), Tensor.rand(K, N, dtype=dtype_in)
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
np_a, np_b = a.numpy(), b.numpy()
r = a.matmul(b, dtype=dtype_out)
if dtype_in == dtypes.bfloat16: r = r.float()
@@ -57,9 +60,11 @@ def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axi
run_program(ast, bufs)
if dtype_in == dtypes.half: tc_atol, tc_rtol = 1e-2, 1e-3
elif dtype_in == dtypes.bfloat16: tc_atol, tc_rtol = (1e-1, 2e-2) if dtype_out == dtypes.bfloat16 else (1e-2, 1e-2)
elif not dtypes.is_float(dtype_in): tc_atol, tc_rtol = 0, 0
else: tc_atol, tc_rtol = 5e-3, 1e-4
c = bufs[0].numpy().reshape((M,N))
np.testing.assert_allclose(c, np_a @ np_b, atol=tc_atol, rtol=tc_rtol)
ref = (np_a.astype(np.int32) @ np_b.astype(np.int32)) if not dtypes.is_float(dtype_in) else (np_a @ np_b)
np.testing.assert_allclose(c, ref, atol=tc_atol, rtol=tc_rtol)
class TestTensorCores(unittest.TestCase):
# TODO: don't skip bf16 for real device (METAL, AMD)
@@ -75,7 +80,7 @@ class TestTensorCores(unittest.TestCase):
def test_tensor_cores_codegen(self):
for tc in Device[Device.DEFAULT].renderer.tensor_cores:
n, m, k = tc.dims
a, b = Tensor.rand(m, k, dtype=tc.dtype_in), Tensor.rand(k, n, dtype=tc.dtype_in)
a, b = _tc_rand(m, k, dtype=tc.dtype_in), _tc_rand(k, n, dtype=tc.dtype_in)
r = a.matmul(b, dtype=tc.dtype_out)
prg = to_program(replace_opts(r.schedule_linear().src[-1].src[0],
[Opt(op=OptOps.TC, axis=0, arg=(-1, 2, 1))]), Device[Device.DEFAULT].renderer)
+8 -65
View File
@@ -1,11 +1,7 @@
import pathlib, tempfile, unittest
from unittest.mock import patch
import tempfile, unittest
from tinygrad import Tensor, dtypes
from tinygrad.uop import Ops
from tinygrad.uop.ops import UOp
from tinygrad.uop.spec import spec_tensor
from tinygrad.nn.state import safe_save
class TestWeakPromotion(unittest.TestCase):
@@ -15,27 +11,20 @@ class TestWeakPromotion(unittest.TestCase):
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).randn_like()
def test_sum_stays_weak(self):
for weak, value in ((dtypes.weakint, 1), (dtypes.weakfloat, 1.0)):
for weak, value in ((dtypes.weakfloat, 1.0),):
self.assertEqual(Tensor.const(weak, value).expand(3).sum().dtype, weak)
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float16)
def test_storage_width(self):
t = Tensor.const(dtypes.weakint, 2)
for fn in (lambda: t.bitcast(dtypes.int32), lambda: Tensor.const(dtypes.int32, 2).bitcast(dtypes.weakint), t.element_size, t.nbytes):
with self.assertRaises(RuntimeError): fn()
def test_materialize_at_default_dtype(self):
for weak, value, strong in ((dtypes.weakint, 3, dtypes.default_int), (dtypes.weakfloat, 0.5, dtypes.default_float)):
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
t = Tensor.const(weak, value)
self.assertEqual(t.dtype, weak)
self.assertEqual(t.data().itemsize, strong.itemsize)
self.assertEqual(t.numpy().dtype.itemsize, strong.itemsize)
with self.assertRaises(RuntimeError): t.clone("CPU")
with patch.object(dtypes, "default_int", dtypes.int64):
self.assertEqual(Tensor.const(dtypes.weakint, 3).numpy().dtype.itemsize, dtypes.int64.itemsize)
def test_uop_scalar_const_unchanged(self):
for dtype, value in ((dtypes.index, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
for dtype, value in ((dtypes.weakint, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
out = UOp.variable("x", 0.0 if dtype == dtypes.float32 else 0, 10.0 if dtype == dtypes.float32 else 10, dtype) + value
self.assertEqual((out.dtype, out.src[1].dtype), (dtype, dtype))
@@ -49,7 +38,6 @@ class TestWeakPromotion(unittest.TestCase):
self.assertEqual(((t_bool + 1) + t_i8).dtype, dtypes.int8)
self.assertEqual(((t_bool + 1) + t_u16).dtype, dtypes.uint16)
self.assertEqual((Tensor(3) + t_i8).dtype, dtypes.int8)
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
# zeros/ones are full with a python fill value, so they are weak too (jnp.zeros pins float32; deliberate divergence)
self.assertEqual((Tensor.zeros(3) + t_f16).dtype, dtypes.float16)
@@ -58,23 +46,13 @@ class TestWeakPromotion(unittest.TestCase):
self.assertEqual((t_i8 + 1).dtype, dtypes.int8)
self.assertEqual((t_f16 + 0.5).dtype, dtypes.float16)
self.assertEqual((t_f32 + t_f16).dtype, dtypes.float32)
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
@unittest.expectedFailure # TODO: dot of a weak const tensor defers to the other operand once python scalars are weak consts
def test_dot_defers_weak(self):
weak = Tensor([True, False]).where(Tensor(1), 2)
self.assertEqual(weak.dot(Tensor([1, 1], dtype=dtypes.int8)).dtype, dtypes.int8)
@unittest.expectedFailure # TODO: Tensor(3).uop becomes CONST(weakint); Tensor.dtype is always uop.dtype; buffers lower to the default
def test_dtype_is_uop_dtype(self):
for value, weak, lowered in ((3, dtypes.weakint, dtypes.default_int), (0.5, dtypes.weakfloat, dtypes.default_float)):
t = Tensor(value)
self.assertEqual((t.uop.dtype, t.dtype), (weak, weak))
self.assertEqual(t.numpy().dtype.itemsize, lowered.itemsize)
realized = t.clone("CPU").realize()
self.assertEqual((realized.dtype, realized.uop.buffer.dtype), (lowered, lowered))
with patch.object(dtypes, "default_int", dtypes.int64):
self.assertEqual(Tensor(3).clone("CPU").realize().uop.buffer.dtype, dtypes.int64)
def test_integer_values(self):
x = Tensor.full((1,), 1, dtype=dtypes.int64, device="CPU")
self.assertEqual((x + 2**40).item(), 2**40 + 1)
@@ -94,15 +72,6 @@ class TestWeakPromotion(unittest.TestCase):
for out in (Tensor(2).exp(), Tensor(2).cos(), Tensor(2).sigmoid()):
self.assertEqual((out.dtype, (out + t_f16).dtype), (dtypes.weakfloat, dtypes.float16))
@unittest.expectedFailure # TODO: where of weak consts stays weak and resolves per consumer
def test_where_and_shared_literal(self):
gate, weak = Tensor([True, False], device="CPU"), Tensor(2)
weak_where = gate.where(weak, 3)
self.assertEqual(weak_where.dtype, dtypes.weakint)
self.assertEqual((weak_where + Tensor([1, 1], dtype=dtypes.int64, device="CPU")).tolist(), [3, 4])
self.assertEqual((weak + Tensor([1], dtype=dtypes.int32, device="CPU")).item(), 3)
self.assertEqual((weak + Tensor([1], dtype=dtypes.int64, device="CPU")).item(), 3)
def test_null_lowering(self):
for t in (Tensor.full((1,), 1, dtype=dtypes.int64, device="NULL") + 2**40,
Tensor.full((1,), 1.0, dtype=dtypes.float64, device="NULL") + (1.0 + 2**-40)):
@@ -113,9 +82,8 @@ class TestWeakPromotion(unittest.TestCase):
class TestWeakStorageBoundary(unittest.TestCase):
# weak has no storage: a weak assignment source casts when it defers to the destination, everything else raises
def test_weak_source(self):
w3, w05 = Tensor.const(dtypes.weakint, 3).reshape(1).expand(2), Tensor.const(dtypes.weakfloat, 0.5).reshape(1)
w05 = Tensor.const(dtypes.weakfloat, 0.5).reshape(1)
dst = Tensor.zeros(2, dtype=dtypes.int8, device="CPU").contiguous().realize()
self.assertEqual(dst.assign(w3).realize().tolist(), [3, 3]) # weakint defers to int8
with self.assertRaises(RuntimeError): dst.assign(w05.expand(2)) # weakfloat into int does not defer
with self.assertRaises(RuntimeError): dst[0:1] = w05
fdst = Tensor.zeros(2, dtype=dtypes.float32, device="CPU").contiguous().realize()
@@ -123,33 +91,17 @@ class TestWeakStorageBoundary(unittest.TestCase):
self.assertEqual(fdst.tolist(), [0.5, 0.0])
with tempfile.TemporaryDirectory() as td: # the DISK path checks the same
ddst = Tensor.empty(2, dtype=dtypes.int32, device=f"DISK:{td}/t")
self.assertEqual(ddst.assign(w3).tolist(), [3, 3])
with self.assertRaises(RuntimeError): ddst.assign(w05.expand(2))
def test_weak_has_no_storage(self):
w = Tensor.const(dtypes.weakint, 3)
with self.assertRaises(RuntimeError): w.assign(Tensor([1], device="CPU"))
with self.assertRaises(RuntimeError): w.reshape(1)[0] = 1
with tempfile.TemporaryDirectory() as td:
with self.assertRaises(ValueError): safe_save({"x": w.reshape(1).expand(2)}, f"{td}/w.safetensors")
with self.assertRaises(RuntimeError): Tensor.empty(2, dtype=dtypes.weakint)
with self.assertRaises(RuntimeError): UOp.new_buffer("CPU", 2, dtypes.weakint) # the one storage boundary
with self.assertRaises(RuntimeError): Tensor([1], dtype=dtypes.weakint)
import numpy as np
with self.assertRaises(RuntimeError): Tensor(np.ones(2, dtype=np.int32), dtype=dtypes.weakint)
self.assertEqual(Tensor(np.array(3), dtype=dtypes.weakint).dtype, dtypes.weakint) # a 0-D ndarray is a const, not storage
with self.assertRaises(RuntimeError): Tensor(np.ones(2, dtype=np.float32), dtype=dtypes.weakfloat)
with self.assertRaises(RuntimeError): Tensor(bytes(8), dtype=dtypes.weakfloat)
with self.assertRaises(RuntimeError): Tensor(bytes(8), dtype=dtypes.weakint)
with tempfile.NamedTemporaryFile(suffix=".bin") as f:
f.write(bytes(8))
f.flush()
with self.assertRaises(RuntimeError): Tensor(pathlib.Path(f.name), dtype=dtypes.weakint)
class TestWeakMaterializationEntries(unittest.TestCase):
# everything that creates storage from a weak value raises
def test_reads_commit_storage_raises(self):
for weak, value, strong in ((dtypes.weakint, 3, dtypes.default_int), (dtypes.weakfloat, 0.5, dtypes.default_float)):
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
def weak_val():
return Tensor([True], device="CPU").where(Tensor.const(weak, value), Tensor.const(weak, value))
self.assertEqual(weak_val().dtype, weak)
@@ -163,21 +115,12 @@ class TestWeakMaterializationEntries(unittest.TestCase):
with self.assertRaises(RuntimeError): entry(weak_val())
def test_empty_reads_commit(self):
for weak, strong in ((dtypes.weakint, dtypes.default_int), (dtypes.weakfloat, dtypes.default_float)):
for weak, strong in ((dtypes.weakfloat, dtypes.default_float),):
empty = Tensor.const(weak, 0).reshape(1).shrink(((0, 0),))
self.assertEqual(empty.data().format, strong.fmt)
self.assertEqual(empty.numpy().dtype.itemsize, strong.itemsize)
self.assertEqual(empty.tolist(), [])
class TestWeakSpec(unittest.TestCase):
def test_weak_operand_allowed(self):
x = UOp.variable("x", 0, 10, dtypes.int64)
weak = UOp.const(dtypes.weakint, 3)
for u in (x.alu(Ops.ADD, weak), x.alu(Ops.CMPLT, weak), x.alu(Ops.SHL, weak)):
self.assertIs(spec_tensor.rewrite(u), True)
gate = UOp.variable("gate", False, True, dtypes.bool)
self.assertIs(spec_tensor.rewrite(UOp(Ops.WHERE, dtypes.int8, (gate, UOp.const(dtypes.int8, 1), weak))), True)
if __name__ == "__main__":
unittest.main()
+29 -2
View File
@@ -1,8 +1,8 @@
import unittest
import unittest, math
import numpy as np
from tinygrad import Tensor
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import UOp, KernelInfo
from tinygrad.uop.ops import UOp, KernelInfo, Ops
class TestTensorGradient(unittest.TestCase):
def test_example(self):
@@ -98,6 +98,33 @@ class TestTensorGradient(unittest.TestCase):
x = Tensor.randn(4, 4)
np.testing.assert_allclose(x.pad(((1,0),(0,0))).gradient(x, gradient=g2)[0].numpy(), np.zeros((4, 4)))
def test_implicit_broadcast_where_gradient(self):
# WHERE with a bare ()-shape branch: the scalar's gradient counts the positions where it is selected
cond, x, w = Tensor([True, False, True]), Tensor([1.0, 2.0, 3.0]), Tensor(4.0)
dw = Tensor(cond.uop.alu(Ops.WHERE, x.uop, w.uop)).sum().gradient(w)[0]
self.assertEqual(dw.shape, ())
self.assertEqual(dw.item(), 1.0)
dw = Tensor(cond.uop.alu(Ops.WHERE, w.uop, x.uop)).sum().gradient(w)[0]
self.assertEqual(dw.item(), 2.0)
def test_implicit_broadcast_alu_gradient(self):
# MUL with a bare ()-shape src, no EXPAND in the graph
x, w = Tensor([1.0, 2.0, 3.0]), Tensor(2.0)
m = x.uop.alu(Ops.MUL, w.uop)
self.assertIs(m.src[1], w.uop)
dw = Tensor(m).sum().gradient(w)[0]
self.assertEqual(dw.shape, ())
self.assertEqual(dw.item(), 6.0)
def test_implicit_broadcast_intermediate_accumulation(self):
# s is used directly and through an implicit broadcast edge, each edge's gradient reduces to s's shape before they sum
x, p = Tensor([1.0, 2.0, 3.0]), Tensor(0.5)
s = p.sin()
z = Tensor(x.uop.alu(Ops.MUL, s.uop)).sum() + s
dp = z.gradient(p)[0]
self.assertEqual(dp.shape, ())
self.assertAlmostEqual(dp.item(), 7*math.cos(0.5), places=5)
def test_bare_const_skipped_by_backward(self):
Tensor.manual_seed(0)
w = Tensor(1.0)
+1 -1
View File
@@ -71,7 +71,7 @@ class TestKeccak(unittest.TestCase):
def test_variable_bs(self):
data = Tensor([b"abc", b"abc", b"def"], dtype=dtypes.uint8).repeat(2048, 1)
bs = UOp.variable("bs", 1, 4096).bind(3)
out = data.shrink_to(bs, data.shape[-1]).keccak().shrink_to(3, 32)
out = data.shrink_to(bs, data.shape[-1]).keccak().shrink_to(3, 32).realize()
self.assertEqual(bytes(out[0].tolist()), bytearray.fromhex("3a985da74fe225b2 045c172d6bd390bd 855f086e3e9d525b 46bfe24511431532"))
self.assertEqual(bytes(out[1].tolist()), bytearray.fromhex("3a985da74fe225b2 045c172d6bd390bd 855f086e3e9d525b 46bfe24511431532"))
self.assertEqual(bytes(out[2].tolist()), bytearray.fromhex("8e0d8f672252acb0 ffc5093db8653b18 1513bf9a2097e737 b4f73533dcaf46df"))
+11
View File
@@ -3,6 +3,7 @@ from tinygrad import Tensor
from tinygrad.device import Buffer
from tinygrad.dtype import Invalid, dtypes
from tinygrad.engine.realize import run_linear
from tinygrad.uop.ops import Ops, UOp
class TestInvalidTensor(unittest.TestCase):
def _invalid_test_helper(self, out, expected):
@@ -132,5 +133,15 @@ class TestInvalidTensor(unittest.TestCase):
out = Tensor([1.0, 2.0, 3.0, 4.0])[idx]
self._invalid_test_helper(out, [1.0, 2.0, None, None])
def test_uop_where_keeps_invalid_bare(self):
cond = UOp.const(dtypes.weakint, 0) < UOp.const(dtypes.weakint, 1)
idx = UOp(Ops.STACK, src=tuple(UOp.const(dtypes.weakint, x) for x in range(3)))
out = cond.where(idx, UOp.invalid())
self.assertIs(cond.op, Ops.CMPLT)
self.assertIs(idx.op, Ops.STACK)
self.assertIs(out.op, Ops.WHERE)
self.assertIs(out.src[2].op, Ops.CONST)
self.assertIs(out.src[2].arg, Invalid)
if __name__ == '__main__':
unittest.main()
+6 -1
View File
@@ -17,6 +17,7 @@ class TestLinAlg(unittest.TestCase):
for size in sizes:
a = Tensor.randn(size).realize()
U,S,V = a.svd()
Tensor.realize(U,S,V)
b_shape,m,n = size[0:-2],size[-2],size[-1]
k = min(m,n)
s_diag = (S.unsqueeze(-2) * Tensor.eye(k).reshape((1,) * len(b_shape) + (k,k)))
@@ -29,6 +30,7 @@ class TestLinAlg(unittest.TestCase):
with Context(CHECK_OOB=0): # sometimes this is slow in CI
a = Tensor.randn(size).realize()
U,S,V = a.svd(full_matrices=False)
Tensor.realize(U,S,V)
b_shape,m,n = size[0:-2],size[-2],size[-1]
k = min(m,n)
s_diag = (S.unsqueeze(-2) * Tensor.eye(k).reshape((1,) * len(b_shape) + (k,k)).expand(b_shape + (k,k)))
@@ -61,6 +63,7 @@ class TestLinAlg(unittest.TestCase):
for size in sizes:
a = Tensor.randn(size).realize()
Q,R = a.qr()
Tensor.realize(Q,R)
orthogonality_helper(Q)
reconstruction_helper([Q,R],a)
@@ -73,9 +76,10 @@ class TestLinAlg(unittest.TestCase):
reconstruction_helper([Q,R], a)
def test_svd_identity(self):
for a in (Tensor.eye(2), Tensor.zeros(2, 2)):
for a in (Tensor.eye(2).clone(), Tensor.zeros(2, 2)):
a = a.realize()
U,S,V = a.svd()
Tensor.realize(U,S,V)
assert not np.isnan(U.numpy()).any()
assert not np.isnan(S.numpy()).any()
assert not np.isnan(V.numpy()).any()
@@ -85,6 +89,7 @@ class TestLinAlg(unittest.TestCase):
def test_svd_identity_4x4(self):
a = Tensor.eye(4).clone()
U,S,V = a.svd()
Tensor.realize(U,S,V)
assert not np.isnan(U.numpy()).any()
assert not np.isnan(S.numpy()).any()
assert not np.isnan(V.numpy()).any()
+1 -1
View File
@@ -17,7 +17,7 @@ class TestMetalGraph(unittest.TestCase):
buf.op = Ops.SLICE
src = MagicMock()
src.dtype = dtypes.uint8
buf.src = (src, UOp.const(dtypes.index, offset))
buf.src = (src, UOp.const(dtypes.weakint, offset))
buf.dtype = dtypes.uint8
else:
buf.op = Ops.BUFFER
+9 -2
View File
@@ -63,6 +63,13 @@ class TestMultiTensor(unittest.TestCase):
np.testing.assert_equal((s + Tensor(UOp.const(dtypes.float, 1.0))).numpy(), [2, 3, 4, 5])
np.testing.assert_equal((s + Tensor(UOp.const(dtypes.float, 1.0)).reshape((1,)).expand((4,))).numpy(), [2, 3, 4, 5])
def test_add_rank_expand_shard(self):
# a sharded src keeps its own rank under implicit broadcast, its shard axis right-aligns into the output
a = Tensor([1.,2.,3.,4.]).shard(devices_2, 0)
b = Tensor([[10.,20.,30.,40.]]).shard(devices_2, None)
self.assertEqual((a+b).uop.axis, 1)
np.testing.assert_equal((a+b).numpy(), [[11.,22.,33.,44.]])
def test_shard_reduce(self):
self._test_shard_op(lambda t:t.reshape(2, 3).sum(axis=1), [3.,3.], n=6)
self._test_shard_op(lambda t:t.reshape(2, 3).sum(axis=0), [2.,2.,2.], n=6)
@@ -602,8 +609,8 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
np.testing.assert_allclose((a+a).numpy(), (b+b).numpy(), rtol=1e-7, atol=1e-3)
np.testing.assert_equal((a+1).numpy(), (b+1).numpy())
np.testing.assert_equal((1+a).numpy(), (1+b).numpy())
np.testing.assert_allclose((a.where(a+a, a)).numpy(), (b.where(b+b, b)).numpy(), rtol=1e-7, atol=1e-3)
np.testing.assert_allclose((a.where(1, 0)).numpy(), (b.where(1, 0)).numpy(), rtol=1e-7, atol=1e-3)
np.testing.assert_allclose((a.bool().where(a+a, a)).numpy(), (b.bool().where(b+b, b)).numpy(), rtol=1e-7, atol=1e-3)
np.testing.assert_allclose((a.bool().where(1, 0)).numpy(), (b.bool().where(1, 0)).numpy(), rtol=1e-7, atol=1e-3)
# reduce
np.testing.assert_allclose(a.max().numpy(), b.max().numpy(), rtol=1e-7, atol=1e-3)
+1 -1
View File
@@ -119,7 +119,7 @@ class TestRandomness(unittest.TestCase):
self.assertRaises(AssertionError, lambda: Tensor(2).multinomial(1, replacement=False))
self.assertRaises(AssertionError, lambda: Tensor([1, 9]).multinomial(0, replacement=False))
def _check_with_torch(w, num_samples, replacement):
tiny_res = Tensor(w).multinomial(num_samples, replacement=replacement)
tiny_res = Tensor(w).multinomial(num_samples, replacement=replacement).realize()
torch_res = torch.tensor(w).multinomial(num_samples, replacement=replacement)
self.assertEqual(tiny_res.shape, torch_res.shape)
if torch_res.ndim == 1:
+5 -4
View File
@@ -61,7 +61,7 @@ def _make_buffer_view(src:UOp) -> UOp|None:
buf = buf.src[0]
if byte_offset % buf.dtype.itemsize != 0: return None
offset = byte_offset // buf.dtype.itemsize
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.index, offset)), src.numel())
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.weakint, offset)), src.numel())
def contiguous_mops_to_view(c:UOp, src:UOp):
"""MOPS(BUFFER) → SLICE when movement ops collapse to a contiguous range."""
@@ -180,8 +180,9 @@ def finalize_after(ctx:AllocCtx, x:UOp):
def replace_input_buffer(ctx:AllocCtx, b:UOp):
ctx.replacements.append(b)
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
b._min_max if b.op is Ops.BIND else None, b.src[0].expr if b.op is Ops.BIND else None,
b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
b._min_max if b.op is Ops.BIND else None, name=b.src[0].expr if b.op is Ops.BIND else None,
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL,
multiple_of=b.src[0].arg.multiple_of if b.op is Ops.BIND else None)
pm_finalize_call = PatternMatcher([
(UPat(Ops.AFTER, name="x"), finalize_after),
@@ -193,7 +194,7 @@ pm_replace_buf = PatternMatcher([
(UPat(Ops.BUFFER, src=(UPat(),), name="b"), lambda ctx,b:
replace_input_buffer(ctx, b) if isinstance(b.arg, ParamArg) and b.addrspace is AddrSpace.GLOBAL else None),
# replace SLICE with PARAM. this rewrite is bottom up so BUFFERs we don't need won't be in the input
(UPat(Ops.SLICE, src=(UPat(Ops.BUFFER), UPat(Ops.CONST, dtype=dtypes.index)), name="b"), replace_input_buffer),
(UPat(Ops.SLICE, src=(UPat(Ops.BUFFER), UPat(Ops.CONST, dtype=dtypes.weakint)), name="b"), replace_input_buffer),
# strip value from BIND for cache key normalization, so different values hit same cache
(UPat(Ops.BIND, src=(UPat(Ops.PARAM), UPat(Ops.CONST)), name="b"), replace_input_buffer),
])
+11 -14
View File
@@ -17,14 +17,14 @@ from tinygrad.uop.movement import mop_cleanup
from tinygrad.codegen.decomp.dtype import pm_dtype_decomps
from tinygrad.codegen.decomp.op import get_late_rewrite_patterns, get_simplifying_rewrite_patterns
from tinygrad.codegen.decomp.transcendental import get_transcendental_patterns
from tinygrad.codegen.late.coalese import indexing_simplify
from tinygrad.codegen.late.coalesce import indexing_simplify
from tinygrad.codegen.opt.postrange import apply_opts
from tinygrad.codegen.late.gater import pm_move_gates_from_index
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
from tinygrad.schedule.rangeify import pm_mops
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
from tinygrad.codegen.late.coalese import memory_coalesing, pm_simplify_add_image
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
from tinygrad.helpers import all_same, flatten, argsort, partition
from tinygrad.uop.ops import _align_left, _broadcast_shape, identity_element
from tinygrad.schedule.rangeify import BufferizeOpts
@@ -39,8 +39,8 @@ pm_number_params = PatternMatcher([
])
pm_no_index = PatternMatcher([
(UPat(GroupOp.ALU.union({Ops.CONST}), dtype=dtypes.index, name="x"), lambda x: x.replace(dtype=dtypes.int)),
(UPat(Ops.CAST, dtype=dtypes.index, src=(UPat.var("x"),)), lambda x: x.cast(dtypes.int)),
(UPat(GroupOp.ALU.union({Ops.CONST}), dtype=dtypes.weakint, name="x"), lambda x: x.replace(dtype=dtypes.int)),
(UPat(Ops.CAST, dtype=dtypes.weakint, src=(UPat.var("x"),)), lambda x: x.cast(dtypes.int)),
])
def build_range_map(sink:UOp) -> dict[int, int]:
@@ -110,7 +110,7 @@ def broadcast_and_devec_wmma(b:UOp):
for u,shp in zip(b.src, shaped_aligned)]
src = []
for idx in itertools.product(*[range(i) for i in b.shape[:-1]]):
idx_c = [UOp.const(dtypes.index, i) for i in idx]
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
src.append(b.replace(src=tuple([x.index(*idx_c) for x in src_reshaped])))
return UOp.stack(*src).reshape(b.shape)
@@ -135,7 +135,7 @@ def do_devectorize(b:UOp):
if not all_same([x.shape for x in b.src]): return None
src = []
for idx in itertools.product(*[range(x) for x in b.shape]):
idx_c = [UOp.const(dtypes.index, i) for i in idx]
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
src.append(b.replace(src=tuple([x.index(*idx_c) for x in b.src])))
return UOp.stack(*src).reshape(b.shape) if b.op is not Ops.STORE else UOp.group(*src)
@@ -145,7 +145,7 @@ def do_stack_wmma(u:UOp):
src = []
for b in u.src:
if b.op != Ops.STACK:
src.append(UOp.stack(*[b.index(UOp.const(dtypes.index, i)) for i in range(b.max_numel())]))
src.append(UOp.stack(*[b.index(UOp.const(dtypes.weakint, i)) for i in range(b.max_numel())]))
else:
src.append(b)
return u.replace(src=tuple(src))
@@ -171,13 +171,10 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
# RESHAPE a void is removed (hack for AFTER)
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
# reshape of a single element shaped value to scalar is an index
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.index, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
# EXPAND on scalar -> STACK
(UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
lambda x,out: UOp.stack(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
# INDEX on INDEX is INDEX
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"),
lambda idx1, idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:])),
])
def fix_group_for_reduce(x:UOp):
@@ -318,11 +315,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# simplify indexing
sink = graph_rewrite(sink, indexing_simplify, name="simplify load/store indexing")
# some coalesing misses without this
# some coalescing misses without this
sink = graph_rewrite(sink, sym, name="early symbolic")
# do memory coalesing (late)
sink = memory_coalesing(sink, ren)
# do memory coalescing (late)
sink = memory_coalescing(sink, ren)
sink = graph_rewrite(sink, symbolic_simple+ew_devectorizer+pm_simplify_add_image, name="add images", ctx=({}, ren), bottom_up=True)
# extra symbolic before decomp. crashes without this?
+1 -1
View File
@@ -57,7 +57,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
# get the idxs
ki: KernelInfo = s.arg
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.index)]
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.weakint)]
elif ki.dont_use_locals:
assert not local_dims, "can't use locals if there's no local dims"
idxs = get_grouped_dims("idx", global_shape, ctx.global_max, reverse=True)
@@ -97,16 +97,16 @@ pm_simplify_add_image = PatternMatcher([
(UPat.var("x", dtype=dtypes.float).cast(dtypes.half).cast(dtypes.float), lambda x: x),
])
def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
if getenv("DMC"): return sink
# collect
memory: defaultdict[tuple[Ops, UOp, UOp|str, UOp], dict[int, list[UOp]]] = defaultdict(dict)
for u in sink.toposort():
# TODO: this should handle images too, it's just memory coalesing
# TODO: this should handle images too, it's just memory coalescing
if u.op in {Ops.LOAD, Ops.STORE}:
assert len(u.src) == (2 if u.op is Ops.STORE else 1), "memory coalesing does not support gated loads/stores"
assert u.src[0].op is Ops.INDEX, f"memory coalesing should be on INDEX, not {u.src[0].op}"
assert len(u.src) == (2 if u.op is Ops.STORE else 1), "memory coalescing does not support gated loads/stores"
assert u.src[0].op is Ops.INDEX, f"memory coalescing should be on INDEX, not {u.src[0].op}"
buf, idx_u = u.src[0].src
if buf.addrspace == AddrSpace.REG: continue
idx, valid = idx_u.get_idx(), idx_u.get_valid()
@@ -141,12 +141,12 @@ def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
grouped_offsets = [[x for _,x in group] for _,group in itertools.groupby(enumerate(sorted(offsets.keys())), lambda x: x[1]-x[0])]
for full_grp in grouped_offsets:
while len(full_grp):
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(dtypes.index, full_grp[0])
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(dtypes.weakint, full_grp[0])
length = [l for l in lengths if l <= len(full_grp) and (not must_divide or offset.divides(l) is not None)][0]
grp = full_grp[:length]
# NOTE: we apply the valid again after we determine the length
offset = offset.valid(valid) if valid is not None else offset
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(dtypes.index, len(grp)))) if len(grp) > 1 else buf.index(offset)
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(dtypes.weakint, len(grp)))) if len(grp) > 1 else buf.index(offset)
if op == Ops.STORE:
datas = []
for i,g in enumerate(grp):
@@ -158,8 +158,8 @@ def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
ld = idx.load()
for i,g in enumerate(grp):
for oo in offsets[g]:
replacements[oo] = ld.index(UOp.const(dtypes.index, i)) if len(grp) > 1 else ld
replacements[oo] = ld.index(UOp.const(dtypes.weakint, i)) if len(grp) > 1 else ld
full_grp = full_grp[length:]
# apply
return sink.substitute(replacements, name="memory coalesing")
return sink.substitute(replacements, name="memory coalescing")
+4 -4
View File
@@ -2,7 +2,7 @@ import heapq
from typing import Any
from collections import defaultdict
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat, multirange_str
from tinygrad.dtype import AddrSpace
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.helpers import prod, getenv, TUPLE_ORDER
def linearize(sink:UOp) -> list[UOp]:
@@ -85,9 +85,9 @@ pm_add_control_flow = PatternMatcher([
])
def do_split_ends(e:UOp):
ret = e.src[0]
for r in sorted(UOp.sink(*e.src[1:]).ranges, key=lambda x: x.arg, reverse=True): ret = ret.end(r)
return ret
ret, backedge = e.src[0], tuple(x for x in e.src[1:] if x.dtype in (dtypes.void, dtypes.bool))
for r in sorted(UOp.sink(*[x for x in e.src[1:] if x not in backedge]).ranges, key=lambda x: x.arg, reverse=True): ret = ret.end(r)
return ret.end(*backedge) if len(backedge) else ret
pm_split_ends = PatternMatcher([
# split the ends
+1 -1
View File
@@ -2,7 +2,7 @@ import itertools
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError
from tinygrad.helpers import getenv, DEBUG, prod, NOLOCALS, TC_OPT, TC_SELECT, USE_TC, IMAGE
from tinygrad.uop.ops import Ops, resolve, AxisType
from tinygrad.codegen.late.coalese import image_valid_dims
from tinygrad.codegen.late.coalesce import image_valid_dims
from tinygrad.codegen.opt.postrange import Scheduler
def hand_coded_optimizations(k:Scheduler) -> Scheduler:
+3 -2
View File
@@ -21,8 +21,9 @@ class Scheduler:
@property
def rngs(self):
# always in order by axistype
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.vmax > 0], key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
# always in order by axistype. void RANGEs are loops, not opt axes
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.dtype is not dtypes.void and u.vmax > 0],
key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
@property
def shape_len(self) -> int: return len(self.rngs)
@property
+1 -1
View File
@@ -103,7 +103,7 @@ amd_rdna3 = [TensorCore(dims=(16,16,16), threads=32, elements_per_thread=(16,16,
opts=("l0","l0","l0","l0","l1","u1","u1","u1"),
swizzle=((('l4', 'u0', 'u1', 'u2', 'l0'), ('r1', 'r2', 'r3'), ('l1', 'l2', 'l3', 'r0')),
(('l0', 'l1', 'l2', 'l3', 'l4'), ('r1', 'r2', 'r3'), ('u0', 'u1', 'u2', 'r0'))))
for di,do in [(dtypes.half,dtypes.float),(dtypes.half,dtypes.half),(dtypes.bfloat16,dtypes.float)]]
for di,do in [(dtypes.half,dtypes.float),(dtypes.half,dtypes.half),(dtypes.bfloat16,dtypes.float),(dtypes.int8,dtypes.int32)]]
amd_rdna4 = [TensorCore(dims=(16,16,16), threads=32, elements_per_thread=(8,8,8), dtype_in=di, dtype_out=do,
opts=("l0","l0","l0","l0","u1","u1","u1","l1"),
swizzle=((('u0', 'u1', 'u2', 'l4', 'r2'), ('r0', 'r1', 'r3'), ('l0', 'l1', 'l2', 'l3')),
+5 -2
View File
@@ -9,7 +9,9 @@ def flatten_range(r:UOp) -> UOp|None:
off = range_start[r.op]
rngs = r.src[off:]
if not len(rngs): return None
return r.replace(src=r.src[:off]+tuple(UOp.sink(*rngs).ranges))
# ranges in the cond should not be ended
backedge = tuple(x for x in rngs if x.dtype in (dtypes.void, dtypes.bool))
return r.replace(src=r.src[:off]+tuple(UOp.sink(*[x for x in rngs if x not in backedge]).ranges)+backedge)
pm_flatten_range = PatternMatcher([
# real ranges only
@@ -19,6 +21,7 @@ pm_flatten_range = PatternMatcher([
# index/range arithmetic uses FLOORDIV/FLOORMOD prior to late rewrite
def count_divmod(x:UOp) -> int: return sum(u.op in {Ops.FLOORDIV, Ops.FLOORMOD} for u in x.backward_slice)
def simplify_merge_adjacent(u:UOp) -> UOp|None:
if not all(r.op is Ops.RANGE for r in u.ended_ranges): return None
reduce_ranges = [x.ranges for x in u.backward_slice_with_self if x.op is Ops.REDUCE]
# on END we only want to merge adjacent ranges, on REDUCE we want to try all combinations
for r0, r1 in (zip(u.ended_ranges, u.ended_ranges[1:]) if u.op is Ops.END else itertools.permutations(u.ended_ranges, 2)):
@@ -149,5 +152,5 @@ def no_load(u:UOp) -> bool: return not any(x.op is Ops.INDEX for x in u.backward
pm_load_collapse = PatternMatcher([
(UPat(Ops.REDUCE, arg=(Ops.ADD, 0), src=(UPat.var("u"), UPat()), name="red"), reduce_load_collapse),
# we want to make sure we dont do math on a loaded index since that can cause overflow, this undoes the rule in pm_reduce_load_collapse
((UPat.var("x", dtypes.index)+UPat.var("y"))<UPat.var("c"), lambda x,y,c: x < c-y if no_load(y) and no_load(c) and not no_load(x) else None),
((UPat.var("x", dtypes.weakint)+UPat.var("y"))<UPat.var("c"), lambda x,y,c: x < c-y if no_load(y) and no_load(c) and not no_load(x) else None),
])
+7 -5
View File
@@ -100,8 +100,8 @@ class MultiBuffer:
class Buffer:
profile_events:list[ProfileEvent] = []
def __init__(self, device:str, size:int, dtype:DType, opaque:Any=None, options:BufferSpec|None=None, initial_value:bytes|None=None,
uop_refcount=0, base:Buffer|None=None, offset:int=0, preallocate=False):
def __init__(self, device:str, size:int, dtype:DType, opaque:Any=None, options:BufferSpec|None=None,
initial_value:bytes|pickle.PickleBuffer|None=None, uop_refcount=0, base:Buffer|None=None, offset:int=0, preallocate=False):
assert isinstance(dtype, DType)
self.device, self.size, self.dtype, self.options, self.offset, self.allocated_views = device, size, dtype, options, offset, 0
self._bufs: dict[str, Any] = {}
@@ -113,6 +113,7 @@ class Buffer:
if initial_value is not None:
self.allocate()
self.copy_from(Buffer("PYTHON", self.size, self.dtype, opaque=memoryview(bytearray(initial_value))))
if isinstance(initial_value, pickle.PickleBuffer): initial_value.release()
else:
assert base._base is None, "base can't have a base"
assert device == base.device, "base must have the same device"
@@ -171,12 +172,13 @@ class Buffer:
self.allocator.free(self._buf, self.nbytes, self.options)
elif self._base is not None: self._base.allocated_views -= 1
self._bufs.clear()
def __reduce__(self):
buf = None
def __reduce_ex__(self, protocol):
buf:bytearray|pickle.PickleBuffer|None = None
if self._base is not None:
return self.__class__, (self.device, self.size, self.dtype, None, None, None, 0, self.base, self.offset, self.is_allocated())
if self.device == "NPY": return self.__class__, (self.device, self.size, self.dtype, self._buf, self.options, None, self.uop_refcount)
if self.is_allocated(): buf = bytearray(self.as_memoryview())
if self.is_allocated():
buf = pickle.PickleBuffer(self.as_memoryview()) if protocol >= 5 else bytearray(self.as_memoryview())
return self.__class__, (self.device, self.size, self.dtype, None, self.options, buf, self.uop_refcount)
@property
def trace_num(self) -> int:
+10 -10
View File
@@ -89,7 +89,7 @@ class dtypes:
def is_float(x: DType) -> bool: return x in (dtypes.floats + (dtypes.weakfloat,))
@staticmethod # static methods on top, or bool in the type info will refer to dtypes.bool
@functools.cache
def is_int(x: DType) -> bool: return x in (dtypes.ints + (dtypes.weakint, dtypes.index))
def is_int(x: DType) -> bool: return x in (dtypes.ints + (dtypes.weakint,))
@staticmethod
@functools.cache
def is_unsigned(x: DType) -> bool: return x in dtypes.uints
@@ -111,8 +111,7 @@ class dtypes:
return {dtypes.float16: (5, 10), dtypes.bfloat16: (8, 7), dtypes.float32: (8, 23), dtypes.float64: (11, 52),
dtypes.fp8e4m3: (4, 3), dtypes.fp8e5m2: (5, 2), dtypes.fp8e4m3fnuz: (4, 3), dtypes.fp8e5m2fnuz: (5, 2)}[dtype]
void: Final[DType] = DType.new(-1, 0, "void", None)
weakint: Final[DType] = DType.new(0, 800, "weakint", None)
index: Final[DType] = DType.new(0, 800, "index", None) # NOTE: not in the promo lattice: index math never mixes dtypes
weakint: Final[DType] = DType.new(0, 800, "weakint", None) # NOTE: not in the promo lattice: index math never mixes dtypes
bool: Final[DType] = DType.new(0, 1, "bool", '?')
int8: Final[DType] = DType.new(1, 8, "signed char", 'b')
uint8: Final[DType] = DType.new(2, 8, "unsigned char", 'B')
@@ -154,7 +153,7 @@ class dtypes:
uints = (uint8, uint16, uint32, uint64)
sints = (int8, int16, int32, int64)
ints = uints + sints
weaks = (weakint, weakfloat)
weaks = (weakfloat,)
all = floats + ints + (bool,) # noqa: A003
if (env_default_float := getenv("DEFAULT_FLOAT", "")):
@@ -164,12 +163,13 @@ if (env_default_float := getenv("DEFAULT_FLOAT", "")):
DTypeLike = str|DType
def to_dtype(dtype:DTypeLike) -> DType: return dtype if isinstance(dtype, DType) else getattr(dtypes, dtype.lower())
def strong_dtype(dtype:DType) -> DType:
return dtypes.default_int if dtype == dtypes.weakint else dtypes.default_float if dtype == dtypes.weakfloat else dtype
# TODO: weakint
return dtypes.default_float if dtype == dtypes.weakfloat else dtype
# https://jax.readthedocs.io/en/latest/jep/9407-type-promotion.html
# we don't support complex type
promo_lattice = { dtypes.bool: [dtypes.weakint], dtypes.weakint: [dtypes.int8, dtypes.uint8],
dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
# TODO: weakint
promo_lattice = { dtypes.bool: [dtypes.int8, dtypes.uint8], dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
dtypes.int64: [dtypes.uint64], dtypes.uint8: [dtypes.int16, dtypes.uint16], dtypes.uint16: [dtypes.int32, dtypes.uint32],
dtypes.uint32: [dtypes.int64, dtypes.uint64], dtypes.uint64: [dtypes.weakfloat],
dtypes.weakfloat: [dtypes.fp8e4m3, dtypes.fp8e5m2, dtypes.fp8e4m3fnuz, dtypes.fp8e5m2fnuz],
@@ -185,8 +185,8 @@ def least_upper_dtype(*ds:DType) -> DType:
return min(set.intersection(*[_get_recursive_parents(d) for d in ds]))
def least_upper_float(dt:DType) -> DType: return dt if dtypes.is_float(dt) else least_upper_dtype(dt, dtypes.default_float)
DTYPES_DICT = {k: v for k, v in dtypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "index", "_"))}
INVERSE_DTYPES_DICT = {**{v.name:k for k,v in DTYPES_DICT.items()}, "void": "void", "weakint":"weakint", "index":"index", "weakfloat":"weakfloat"}
DTYPES_DICT = {k: v for k, v in dtypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "_"))}
INVERSE_DTYPES_DICT = {**{v.name:k for k,v in DTYPES_DICT.items()}, "void": "void", "weakint":"weakint", "weakfloat":"weakfloat"}
@functools.cache
def can_lossless_cast(dt0:DType, dt1:DType) -> bool:
@@ -194,7 +194,7 @@ def can_lossless_cast(dt0:DType, dt1:DType) -> bool:
# similar to https://numpy.org/doc/stable/reference/generated/numpy.can_cast.html
if dt0 == dt1 or dt0 == dtypes.bool: return True
match dt1:
case dtypes.weakint | dtypes.index: return dt0 in dtypes.ints
case dtypes.weakint: return dt0 in dtypes.ints
case dtypes.double: return dt0 in (dtypes.float, dtypes.half, dtypes.bfloat16, *dtypes.fp8s,
dtypes.uint32, dtypes.uint16, dtypes.uint8, dtypes.int32, dtypes.int16, dtypes.int8)
case dtypes.float: return dt0 in (dtypes.half, dtypes.bfloat16, *dtypes.fp8s, dtypes.uint16, dtypes.uint8, dtypes.int16, dtypes.int8)
+19 -10
View File
@@ -6,7 +6,7 @@ from tinygrad.helpers import argfix, polyN
from tinygrad.mixin.creation import CreationMixin
if TYPE_CHECKING:
from tinygrad.uop.ops import UOp
from tinygrad.uop.ops import UOp, sint
class ElementwiseMixin(CreationMixin):
@@ -18,9 +18,11 @@ class ElementwiseMixin(CreationMixin):
def ufix(self, x: 'Self|ConstType|UOp') -> Self:
return x if isinstance(x, type(self)) else self._wrap_uop(self._uop.ufix(x))
# implemented in OpMixin, broadcasting needs the movement ops
def _broadcasted(self, y: 'Self|ConstType|UOp', reverse: bool = False) -> tuple[Self, Self]:
raise NotImplementedError
y = self.ufix(y)
x, y = (self, y) if not reverse else (y, self)
if x.dtype == y.dtype: return x, y
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
def _binop(self, op: Ops, x: Self | ConstType, reverse: bool) -> Self:
lhs, rhs = self._broadcasted(x, reverse)
@@ -414,10 +416,19 @@ class ElementwiseMixin(CreationMixin):
m = a.maximum(b)
return ((a-m).exp() + (b-m).exp()).log() + m
def where(self, x: Self | ConstType, y: Self | ConstType) -> Self:
ref: Self = x if isinstance(x, type(self)) else y if isinstance(y, type(self)) else \
self.cast(least_upper_dtype(dtypes.from_py(x), dtypes.from_py(y)))
return self.alu(Ops.WHERE, ref.ufix(x), ref.ufix(y))
def where(self, x: 'Self | ConstType | sint', y: 'Self | ConstType | sint') -> Self:
"""
Returns a tensor of elements selected from either `x` or `y`, depending on `self`.
`output_i = x_i if self_i else y_i`.
```python exec="true" source="above" session="tensor" result="python"
cond = Tensor([[True, True, False], [True, False, False]])
print(cond.where(1, 3).numpy())
```
"""
ref = x if isinstance(x, type(self)) else y if isinstance(y, type(self)) else self
x, y = ref.ufix(x)._broadcasted(y)
return self.alu(Ops.WHERE, x, y)
def masked_fill(self, mask:Self, value:Self|PyConst) -> Self:
"""
@@ -548,9 +559,7 @@ class ElementwiseMixin(CreationMixin):
# TODO: int pow
if not base.is_floating_point() and isinstance(x, ConstType) and not (isinstance(x, int) and x >= 0):
raise RuntimeError("base needs to be float")
ret = base.alu(Ops.POW, exponent)
# NOTE: pow(int, float) -> int
return ret.round().cast(self.dtype) if not reverse and not dtypes.is_float(self.dtype) and dtypes.is_float(exponent.dtype) else ret
return base.alu(Ops.POW, exponent)
def __pow__(self, x: Self | ConstType) -> Self:
return self.pow(x)
+8 -12
View File
@@ -1,18 +1,13 @@
from typing import cast
import math, dataclasses
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
from tinygrad.helpers import argsort
from tinygrad.dtype import sum_acc_dtype
def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
def broadcast_to_input(x:UOp) -> UOp: return x._broadcast_to(ret.src[0].shape)
if op == Ops.ADD: return (broadcast_to_input(ctx),)
if op == Ops.MAX:
assert ret.op is Ops.REDUCE, "only works on REDUCE"
mask = ret.src[0].eq(broadcast_to_input(ret)).cast(ctx.dtype)
count = mask._rop(Ops.ADD, tuple(range(ret.arg[1])))
return ((mask/broadcast_to_input(count)) * broadcast_to_input(ctx),)
if op == Ops.MUL: return (broadcast_to_input(ctx * ret) / ret.src[0],)
if op == Ops.ADD: return (ctx._broadcast_to(ret.src[0].shape),)
if op == Ops.MAX: return (((mask:=ret.src[0].eq(ret).cast(ctx.dtype))/mask._rop(Ops.ADD, tuple(range(ret.arg[1])))) * ctx,)
if op == Ops.MUL: return (ctx * ret / ret.src[0],)
def _compact_params(body:UOp, all_args:tuple[UOp, ...]) -> tuple[UOp, tuple[UOp, ...]]:
"""Remove unused PARAMs from body and return compacted (body, args)."""
@@ -67,9 +62,7 @@ pm_gradient = PatternMatcher([
(UPat(Ops.CONTIGUOUS), lambda ctx: (ctx,)),
(UPat(Ops.CONTIGUOUS_BACKWARD), lambda ctx: (ctx.contiguous(),)),
(UPat(Ops.RESHAPE, name="ret"), lambda ctx, ret: (ctx.reshape(ret.src[0].shape), None)),
(UPat(Ops.EXPAND, name="ret"), lambda ctx, ret:
(ctx.cast(sum_acc_dtype(ctx.dtype))._rop(Ops.ADD, tuple(range(len(ret.marg))))
.reshape(ret.src[0].shape).cast(ctx.dtype), None)),
(UPat(Ops.EXPAND), lambda ctx: (ctx, None)),
(UPat(Ops.PAD, name="ret"), lambda ctx, ret: (ctx.shrink(tuple([(p[0], s+p[0]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
(UPat(Ops.SHRINK, name="ret"), lambda ctx, ret: (ctx.pad(tuple([(p[0], s-p[0]-p[1]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
(UPat(Ops.PERMUTE, name="ret"), lambda ctx, ret: (ctx.permute(argsort(ret.marg)),)),
@@ -119,6 +112,9 @@ def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp
assert len(lgrads) == len(t0.src), f"got {len(lgrads)} gradient, expected {len(t0.src)}"
for k,v in zip(t0.src, lgrads):
if v is None: continue
# a shaped edge's gradient is summed to its source's shape
if k._shape is not None and v._shape is not None and k._shape != v._shape:
v = v.cast(sum_acc_dtype(v.dtype))._rop(Ops.ADD, broadcast_axes(k.shape, v.shape)).reshape(k.shape).cast(v.dtype)
if k in grads and grads[k].op is not Ops.NOOP:
if v.op is Ops.TUPLE and grads[k].op is Ops.TUPLE:
grads[k] = UOp.maketuple(*(p + n if (p.op is not Ops.NOOP and n.op is not Ops.NOOP) else
+2 -2
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Self, Sequence
from tinygrad.uop import Ops
from tinygrad.helpers import prod, argfix, argsort, flatten, dedup, make_tuple, ceildiv, round_up, all_int
from tinygrad.uop.ops import resolve, smax, _align_left, _broadcast_shape
from tinygrad.uop.ops import resolve, smax, _align_left, _broadcast_shape, broadcast_axes
if TYPE_CHECKING:
from tinygrad.uop.ops import sint
@@ -125,7 +125,7 @@ class MovementMixin:
raise ValueError(f"cannot broadcast {self.shape} to {new_shape=}")
# EXPAND only adds dims on the left. squeeze 1s that need expanding, EXPAND on left, permute back.
n_left = len(new_shape) - len(self.shape)
expand_at = tuple(i for i, s in enumerate(self.shape) if resolve(s == 1, default=False) and resolve(new_shape[n_left+i] != 1))
expand_at = tuple(i-n_left for i in broadcast_axes(self.shape, new_shape) if i >= n_left)
kept = tuple(i for i in range(len(self.shape)) if i not in expand_at)
squeezed = self.reshape(tuple(self.shape[i] for i in kept))
expanded = squeezed._mop(Ops.EXPAND, arg=new_shape[:n_left] + tuple(new_shape[n_left+i] for i in expand_at))
+4 -17
View File
@@ -11,7 +11,7 @@ from tinygrad.helpers import all_int, argfix, argsort, ceildiv, flatten, flat_to
from tinygrad.helpers import resolve_pool_pads, round_up, IMAGE, FLOAT16, WINO
if TYPE_CHECKING:
from tinygrad.uop.ops import sint, UOp
from tinygrad.uop.ops import sint
ReductionStr = Literal["mean", "sum", "none"]
@@ -110,8 +110,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
consecutive = dims == list(range(dims[0], dims[0] + len(dims)))
if v is None and len(dims) > 1 and consecutive and all_int(ishp := tuple(x.shape[d] for d in dims)):
strides = tuple(prod(ishp[i+1:]) for i in range(len(dims)))
try: linear_idx = type(self).usum(*[t._broadcast_to(big_shape) * s for t, s in zip(tensors, strides)])
except ValueError as err: raise IndexError(f"cannot broadcast indices: {err}") from err
linear_idx = type(self).usum(*[t * s for t, s in zip(tensors, strides)])
valid = type(self).uprod(*[(t >= 0) & (t < s) for t, s in zip(tensors, ishp)])
pre, post = x.shape[:dims[0]], x.shape[dims[-1]+1:]
x = x.reshape(pre + (prod(ishp),) + post)[tuple([slice(None)] * len(pre)) + (valid.where(linear_idx, 0),)]
@@ -285,8 +284,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
pads = tuple((smax(pB,0), smax(pA,0)) for pB,pA in pX) if has_neg else pX
base = MovementMixin.pad(X, pads)
if value == 0: return base
if value is not Invalid: base = base.cast(least_upper_dtype(base.dtype, dtypes.from_py(value)))
return MovementMixin.pad(X.const_like(1).cast(dtypes.bool), pads).where(base, base.const_like(value))
return MovementMixin.pad(X.const_like(1).cast(dtypes.bool), pads).where(base, value)
def _pad_circular(self, pX:tuple[tuple[sint, sint], ...]) -> Self:
# shrink first for negative pads, then wrap the non-negative remainder
@@ -357,17 +355,6 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
if mode in {"reflect", "replicate"}: return self._pad_reflect_replicate(pX, mode)
raise NotImplementedError(f"{mode=} is not supported")
def _broadcasted(self, y:Self|ConstType|UOp, reverse:bool=False) -> tuple[Self, Self]:
if not isinstance(y, type(self)): y = self.ufix(y)
x, y = (self, y) if not reverse else (y, self)
# ValueError: unsized ptr has shape (-1,) which can't broadcast; RuntimeError: shape mismatch
try:
out_shape = _broadcast_shape(x.shape, y.shape)
x, y = x._broadcast_to(out_shape), y._broadcast_to(out_shape)
except (RuntimeError, ValueError): pass
if x.dtype == y.dtype: return x, y
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
def dot(self, w:Self, dtype:DTypeLike|None=None) -> Self:
"""
Performs dot product between two tensors.
@@ -842,7 +829,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
last_dim_size = x.shape[-1]
x_unsqueezed = x.unsqueeze(-2).expand((None,)*(self.ndim-1)+(last_dim_size, None))
x_cummax = x.cummax(-1)[0].detach()
mask = type(self).ones(last_dim_size, last_dim_size, buffer=False).tril()
mask = type(self).ones(last_dim_size, last_dim_size, buffer=False, dtype=dtypes.bool).tril()
ret = mask.where(x_unsqueezed - x_cummax.unsqueeze(-1), self.dtype.min).exp().sum(-1).log() + x_cummax
return ret.transpose(-1, axis)
+1 -1
View File
@@ -11,7 +11,7 @@ class RandMixin(OpMixin):
@staticmethod
def _threefry_random_bits(key, counts0, counts1):
x = (counts1.cast(dtypes.uint64) << 32) | counts0.cast(dtypes.uint64)
x = x.threefry((key[1]._broadcast_to(x.shape).cast(dtypes.uint64) << 32) | key[0]._broadcast_to(x.shape).cast(dtypes.uint64))
x = x.threefry((key[1].cast(dtypes.uint64) << 32) | key[0].cast(dtypes.uint64))
return (x & 0xffffffff).cast(dtypes.uint32).cat(((x >> 32) & 0xffffffff).cast(dtypes.uint32))
@classmethod
+2 -2
View File
@@ -348,12 +348,12 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
# each device owns [offset, offset+local_vocab_size) of the global vocabulary
dnum = UOp.variable("_device_num", 0, ndev-1)
offset = dnum * local_vocab_size
global_token_id = idx_flat[i].cast(dtypes.index)
global_token_id = idx_flat[i].cast(dtypes.weakint)
local_token_id = (global_token_id - offset).clip(0, grad_weight.shape[0]-1)
in_range = (global_token_id >= offset) & (global_token_id < (offset + local_vocab_size)) & j_ok
grad_val = in_range.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
else:
local_token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.index)
local_token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.weakint)
grad_val = j_ok.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
# atomic scatter-add: grad_weight[token_id, j] += grad_emb_flat[i, j]
if device in ("CPU", "NULL"): atomic_arg = "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
+3 -1
View File
@@ -617,6 +617,8 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
def Add(x:Tensor,y:Tensor, broadcast=None, axis=None): return x + y
def Sub(x:Tensor|int,y:Tensor): return x - y # some test has input as int
def Div(x:Tensor,y:Tensor): return x.div(y, rounding_mode='trunc' if dtypes.is_int(x.dtype) else None)
# ONNX Pow is (T, T1) -> T, the output takes the base dtype while Tensor.pow promotes base and exponent
def Pow(x:Tensor,y:Tensor): return x.pow(y).round().cast(x.dtype) if dtypes.is_int(x.dtype) else x.pow(y)
def Less(x:Tensor,y:Tensor): return x < y
def LessOrEqual(x:Tensor,y:Tensor): return x <= y
def Greater(x:Tensor,y:Tensor): return x > y
@@ -1297,7 +1299,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
return {
# Tensor ops
**{op: getattr(Tensor, op.lower()) for op in ("Neg", "Reciprocal", "Pow", "Sqrt", "Sign", "Abs", "Exp", "Log", "Mish", "Sin", "Cos", "Tan",
**{op: getattr(Tensor, op.lower()) for op in ("Neg", "Reciprocal", "Sqrt", "Sign", "Abs", "Exp", "Log", "Mish", "Sin", "Cos", "Tan",
"Asin", "Acos", "Atan", "Relu", "Sigmoid", "MatMul", "Floor", "Ceil", "IsNaN", "Softplus", "HardSwish", "Where", "Mul", "Sinh", "Cosh",
"Tanh", "Softsign", "Asinh", "Acosh", "Atanh", "Elu", "Celu", "Selu", "Round", "Erf")},
# Implemented ops
+4 -3
View File
@@ -39,9 +39,10 @@ class Estimates:
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.scalar().itemsize)
if u.op is Ops.RANGE:
mult_stack.append(mults)
mults *= cast(sint, u.src[0].ssimplify())
# SPECIAL are already counted in mults
mults = mults.substitute({x:x.const_like(0) for x in mults.toposort() if x.op is Ops.SPECIAL}) if isinstance(mults, UOp) else mults
if u.dtype is not dtypes.void: # unbounded loop, unknown trip count
mults *= cast(sint, u.src[0].ssimplify())
# SPECIAL are already counted in mults
mults = mults.substitute({x:x.const_like(0) for x in mults.toposort() if x.op is Ops.SPECIAL}) if isinstance(mults, UOp) else mults
elif u.op is Ops.END: mults = mult_stack.pop(-1)
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
+17 -4
View File
@@ -12,9 +12,11 @@ base_rewrite = PatternMatcher([
# local/reg buffers
(UPat(Ops.BUFFER, name="x"), lambda ctx,x: ctx.render_buffer(x)),
# range/if/endif
# range/loop/if/endif
(UPat(Ops.RANGE, dtypes.void, name="x"), lambda ctx,x: "for (;;) {"),
(UPat(Ops.RANGE, name="x"),
lambda ctx,x: f"for ({ctx.render_dtype(x.dtype)} {ctx[x]} = 0; {ctx[x]} < {ctx[x.src[0]]}; {ctx[x]}++) {{"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE), UPat(name="c", dtype=dtypes.bool))), lambda ctx,c: f" if (!({ctx[c]})) {{ break; }}\n}}"),
(UPat(Ops.IF, name="x"), lambda ctx,x: f"if ({ctx[x.src[0]]}) {{"),
(UPat((Ops.ENDIF, Ops.END)), lambda ctx: "}"),
@@ -99,7 +101,8 @@ def uops_to_dtypes(uops:list[UOp]) -> list[tuple[DType, int]]:
return dedup((u.dtype, u.max_numel()) for u in uops if u.addrspace in (AddrSpace.ALU, None) and u.dtype != dtypes.void and u._shape is not None)
def _wmma_name(u:UOp) -> str:
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}"
# sanitize spaces in DType.name (int8 = "signed char")
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}".replace(" ", "_")
# (name, dims, dtype_in, dtype_out, device, threads, upcast_sizes)
def wmma_args(uops:list[UOp]):
@@ -226,14 +229,14 @@ class CStyleLanguage(Renderer):
if u.op in {Ops.ENDIF, Ops.END}: depth -= 1
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG) or \
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
r[u] = l
else:
if u.op not in {Ops.RANGE, Ops.STORE, Ops.BUFFER} and u.dtype != dtypes.void:
l = f"{self.render_type(u)} {r[u]} = {l}" + (";" if u.op is not Ops.SPECIAL else "")
kernel.append(" "*depth + l)
kernel.append("\n".join(" "*depth + line for line in l.split("\n")))
if prefix: c[prefix] += 1 # if it was used, increment
if u.op in {Ops.IF, Ops.RANGE}: depth += 1
del self.r
@@ -565,6 +568,11 @@ class HIPRenderer(CStyleLanguage):
# #define __WMMA_16_16_16_half_half __builtin_amdgcn_wmma_f16_16x16x16_f16_w32_gfx12
elif self.tensor_cores == tc.amd_rdna4:
prefix.append(f"#define __{name} __builtin_amdgcn_wmma_{type_map[dtype_out]}_16x16x16_{type_map[dtype_in]}_w32_gfx12")
elif dtype_out == dtypes.int32:
prefix.append("typedef int wmma_int4 __attribute__((ext_vector_type(4)));\n"+
f"static inline __attribute__((device)) int8 __{name}"+"""(signed_char16 a, signed_char16 b, int8 c) {
return __builtin_amdgcn_wmma_i32_16x16x16_iu8_w32(true, __builtin_bit_cast(wmma_int4, a),
true, __builtin_bit_cast(wmma_int4, b), c, false);\n}""")
elif dtype_out == dtypes.float:
prefix.append(f"#define __{name} __builtin_amdgcn_wmma_f32_16x16x16_{'f16' if dtype_in == dtypes.half else 'bf16'}_w32")
else: prefix.append(f"static inline __attribute__((device)) half8 __{name}"+"""(half16 a, half16 b, half8 c) {
@@ -589,3 +597,8 @@ class QCOMCLRenderer(OpenCLRenderer):
def supported_dtypes(self):
return {d for d in Renderer.supported_dtypes(self)
if (d != dtypes.float16 or (bool(IMAGE) and bool(FLOAT16))) and d not in dtypes.fp8s+(dtypes.bfloat16,dtypes.double)}
# QCOM's load vectorizer emits invalid IR for vectorized bool loads ("Range types must match load type"), type bool buffers as uchar
def _render_dtype(self, dtype:DType, sz:int=1, addrspace=AddrSpace.ALU, mutable=True, override_ptr=False, shape=None):
if dtype == dtypes.bool and addrspace == AddrSpace.GLOBAL: dtype = dtypes.uint8
return super()._render_dtype(dtype, sz, addrspace, mutable, override_ptr, shape)
+12 -3
View File
@@ -35,7 +35,7 @@ def lcast(input_type:DType, output_type:DType):
def render_wmma_amd(ctx, wmma: UOp, cdna=False) -> str:
dt_map = {dtypes.half: "f16", dtypes.float: "f32", dtypes.ushort: "bf16.1k" if cdna else "bf16", dtypes.bfloat16: "bf16.1k" if cdna else "bf16",
dtypes.fp8e4m3: ".fp8.fp8", dtypes.fp8e5m2: ".bf8.bf8"}
dtypes.fp8e4m3: ".fp8.fp8", dtypes.fp8e5m2: ".bf8.bf8", dtypes.int8: "iu8", dtypes.int32: "i32"}
# https://github.com/llvm/llvm-project/blob/main/clang/test/CodeGenOpenCL/builtins-amdgcn-mfma.cl
N,M,K = wmma.arg[0]
if cdna:
@@ -44,9 +44,10 @@ def render_wmma_amd(ctx, wmma: UOp, cdna=False) -> str:
f".{N}x{M}x{K}{dt_map[wmma.arg[1]]}(" + ", ".join([f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]) + ", i32 0, i32 0, i32 0)"
# https://github.com/llvm/llvm-project/blob/main/llvm/test/CodeGen/AMDGPU/GlobalISel/llvm.amdgcn.wmma_32.ll
# example: %wmma0 = call <8 x float> @llvm.amdgcn.wmma.f32.16x16x16.f16(<16 x half> %v99,<16 x half> %v100,<8 x float> %v101)
args = [f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]
if wmma.arg[1] == dtypes.int8: args = ["i1 true", args[0], "i1 true", args[1], args[2]] # iu8 flags A/B signed
return f" {ctx[wmma]} = call {ldt(wmma.dtype, wmma.max_numel())} @llvm.amdgcn.wmma.{dt_map[wmma.src[-1].dtype]}.16x16x16." + \
f"{dt_map[wmma.src[0].dtype]}(" + ", ".join([f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]) + (", i1 false)" \
if wmma.dtype != dtypes.float else ")")
f"{dt_map[wmma.arg[1]]}(" + ", ".join(args) + (", i1 false)" if wmma.dtype != dtypes.float else ")")
# llvm ops, lop[<dtype>][<op>]
unsigned_lop = { Ops.ADD: "add", Ops.MUL: "mul", Ops.CDIV: "udiv", Ops.CMOD: "urem",
@@ -95,6 +96,11 @@ base_rewrite = PatternMatcher([
(UPat(Ops.WHERE, name="x"), lambda ctx,x:
f" {ctx[x]} = select {ldt(x.src[0].dtype)} {ctx[x.src[0]]}, {ldt(x.src[1].dtype)} {ctx[x.src[1]]}, {ldt(x.src[2].dtype)} {ctx[x.src[2]]}"),
# loop (a RANGE with no src is an unbounded loop header)
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx,l: f" br label %loop_{ctx[l][1:]}\nloop_{ctx[l][1:]}:"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx,l,c:
f" br i1 {ctx[c]}, label %loop_{ctx[l][1:]}, label %loop_exit_{ctx[l][1:]}\nloop_exit_{ctx[l][1:]}:"),
# range
(UPat(Ops.RANGE, name="r"), lambda ctx,r:
f" br label %loop_entry_{range_str(r)}\n"
@@ -261,6 +267,9 @@ exit: %packed = phi i32 [%packed_bf8, %do_bf8], [%packed_fp8, %do_fp8]\n %trunc
])
if target.arch in {"gfx1100", "gfx1151"}:
self.extra_matcher += PatternMatcher([
(UPat(Ops.WMMA, name="x", dtype=dtypes.int32), lambda x: x.replace(
src=(x.src[0].bitcast(dtypes.uint32), x.src[1].bitcast(dtypes.uint32), x.src[2]))
if x.src[0].dtype == dtypes.int8 and x.src[0].max_numel() == 16 else None),
(UPat(Ops.WMMA, name="x", dtype=dtypes.half), lambda x: UOp(Ops.STACK, src=tuple(x.replace(
src=(x.src[0], x.src[1], UOp(Ops.STACK, src=tuple(x.src[2].index(j//2) if j%2 == 0 else UOp.const(x.src[2].dtype, 0.0)
for j in range(x.max_numel()*2)))),
+4
View File
@@ -116,6 +116,9 @@ string_rewrite = PatternMatcher([
# simple
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
f".shared .align 16 .b8 local{x.arg.slot}[{x.max_numel()*x.dtype.itemsize}];", f"mov.u64 {ctx.r[x]}, local{x.arg.slot}[0];"]),
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx, l: f"WAITLOOP_{ctx.uops.index(l)}:"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx, l, c:
f"@{ctx.r[c]} bra WAITLOOP_{ctx.uops.index(l)};"),
(UPat(Ops.RANGE, name="r"), lambda ctx, r: [
f"mov.u32 {ctx.r[r]}, -1;",
f"bra END_{ctx.r[r][1:]};",
@@ -211,6 +214,7 @@ class PTXRenderer(Renderer):
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
if u.op is Ops.RANGE and u.dtype == dtypes.void: prefix = None # loop headers don't have a register
if prefix: r[u] = ssa(prefix, u, dtype)
l: str|list[str]|None = string_rewrite.rewrite(u, ctx=self)
+8 -5
View File
@@ -48,7 +48,6 @@ class PythonProgram:
st = time.perf_counter()
warp = list(itertools.product(*[range(x) for x in local_size[::-1]]))
warp_size = len(warp)
void_ops = {Ops.END, Ops.BARRIER, Ops.IF, Ops.ENDIF, Ops.SINK, Ops.NOOP, Ops.GROUP, Ops.STORE}
for idxs in itertools.product(*[range(x) for x in global_size[::-1]]):
values: dict[UOp, Any] = {}
pbufs: list[memoryview] = list(bufs)
@@ -57,11 +56,15 @@ class PythonProgram:
i = 0
while i < len(self.uops):
u = self.uops[i]
src_values = [values[v] for v in u.src if v.op not in void_ops]
src_dtypes = [v.dtype for v in u.src if v.op not in void_ops]
src_values = [values[v] for v in u.src if v.dtype is not dtypes.void]
src_dtypes = [v.dtype for v in u.src if v.dtype is not dtypes.void]
if getenv("TRACE"): print(i, u.op, u.dtype, u.arg, src_values, src_dtypes)
if u.op is Ops.END:
i = self.uop_to_index[u.src[1]]
if len(u.src) == 3:
# conditional backedge on a loop: jump back while the condition is true
if values[u.src[2]][0]: i = self.uop_to_index[u.src[1]]
else: i += 1
else: i = self.uop_to_index[u.src[1]]
continue
if u.op is Ops.IF:
exec_masks.append([x and y for x,y in zip(exec_masks[-1], src_values[0])])
@@ -71,7 +74,7 @@ class PythonProgram:
exec_masks.pop()
i += 1
continue
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP):
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP) or (u.op is Ops.RANGE and u.dtype == dtypes.void):
# in the python emulator, the warp is always in sync
i += 1
continue
+21 -8
View File
@@ -2,7 +2,7 @@ from typing import Iterator
import functools, itertools
from dataclasses import dataclass, field, replace
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, profile_matches
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, profile_matches, broadcast_axes
from tinygrad.uop.ops import consumer_map_from_toposort, gate_kernel_sink
from tinygrad.uop.symbolic import symbolic, pm_simplify_valid, pm_drop_and_clauses
from tinygrad.helpers import argsort, all_same, cpu_profile, PCONTIG, colored, Context, SPEC
@@ -52,16 +52,22 @@ class IndexingContext:
range_idx: Iterator[int] = field(default_factory=itertools.count)
def new_range(self, s:sint, axistype:AxisType=AxisType.LOOP) -> UOp:
if isinstance(s, UOp) and s.op is Ops.RANGE: return s
# if a range has a 1 src, it's the same as UOp.const(dtypes.index, 0)
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(dtypes.index, 0)
# if a range has a 1 src, it's the same as UOp.const(dtypes.weakint, 0)
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(dtypes.weakint, 0)
def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
if x.op not in GroupOp.Broadcastable: return rngs
baxes, nleft = broadcast_axes(src.shape, x.shape), len(x.shape)-len(src.shape)
return tuple(r.const_like(0) if j in baxes else r for j,r in enumerate(rngs) if j >= nleft)
def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
new_srcs = []
for i, s in enumerate(x.src):
new_src = s
src_rngs = broadcast_rngs(x, s, ctx.range_map[x][0]) if x in ctx.range_map else ()
# shape args of movement ops are at src[1:] and should not be indexed
if s.op in {Ops.PARAM, Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
if x in ctx.range_map and not (x.op in GroupOp.Movement and i > 0): new_src = new_src.index(*ctx.range_map[x][0])
if x in ctx.range_map and not (x.op in GroupOp.Movement and i > 0): new_src = new_src.index(*src_rngs)
elif s in ctx.realize_map:
realized_ranges = ctx.realize_map[s]
assert isinstance(realized_ranges, list), "realize map must contain range list"
@@ -77,7 +83,7 @@ def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
opts = BufferizeOpts(device=s.device, removable=removable) if len(ctx.range_map[s][1]) == len(realized_ranges) else \
BufferizeOpts(device=s.device, addrspace=AddrSpace.LOCAL, removable=removable)
new_src = UOp(Ops.STAGE, src=(new_src,)+closed_ranges, arg=opts)
if x in ctx.range_map: new_src = new_src.index(*[r for i,r in enumerate(ctx.range_map[x][0]) if i in realized_ranges])
if x in ctx.range_map: new_src = new_src.index(*[r for i,r in enumerate(src_rngs) if i in realized_ranges])
new_srcs.append(new_src)
return new_srcs
@@ -136,7 +142,7 @@ def _apply_reshape(in_shape:tuple[sint,...], out_shape:tuple[sint, ...], urngs:U
for s,src in list(zip(out_shape, urngs.src))[::-1]:
axes_in.append(acc*src)
acc *= s
combined_axes = UOp.const(dtypes.index, 0).usum(axes_in)
combined_axes = UOp.const(dtypes.weakint, 0).usum(axes_in)
axes_out:list[UOp] = []
for s in in_shape[::-1]:
axes_out.append(combined_axes % s)
@@ -188,15 +194,21 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
# treat MSTACK/MSELECT like SINK
if x.op in {Ops.MSTACK, Ops.MSELECT}: continue
if x.dtype == dtypes.index: continue # TODO: why do I need this?
if x.dtype == dtypes.weakint: continue # TODO: why do I need this?
ending_ranges[x] = sum([ending_ranges.get(u, []) for u in consumer_map[x]], [])
# ranges the consumers iterate that this node broadcasts over
ended = [rctx.range_map[c][0][i] for c in consumer_map[x] if c in rctx.range_map and c.op in GroupOp.Broadcastable
for i in broadcast_axes(x.shape, c.shape)]
broadcast_ending_ranges = list(UOp.sink(*ended).ranges)
# fusion decision: REDUCE before the broadcast
if x.op is Ops.REDUCE: ending_ranges[x] += broadcast_ending_ranges
# *** the ranges on the output are
# 1. new if this op is realized
# 2. from the single consumer if this op only has one consumer
# 3. potentially new if this op has 2+ consumers
consumer_rngs = [rctx.range_map[c][0] for c in consumer_map[x] if c in rctx.range_map]
consumer_rngs = [broadcast_rngs(c, x, rctx.range_map[c][0]) for c in consumer_map[x] if c in rctx.range_map]
if x in rctx.realize_map:
# if this is in the realize_map, we create new ranges (at the output)
out_rngs = tuple(rctx.new_range(s) for s in x.shape)
@@ -248,6 +260,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
if len(_realize_axis):
rctx.realize_map[x] = _realize_axis
out_rngs = tuple([(rctx.new_range(x.shape[i]) if i in _realize_axis else r) for i,r in enumerate(out_rngs)])
ending_ranges[x] += broadcast_ending_ranges
# TODO: some ops don't have shape, enable this after the `.st` property is removed
#assert len(out_rngs) == len(x.shape), \
+1 -1
View File
@@ -56,7 +56,7 @@ def memory_plan_rewrite(linear:UOp, held_bufs:set[UOp]|None=None) -> UOp:
arenas = {key: UOp.new_buffer(key[0], sz, dtypes.int8) for key, sz in arena_sizes.items()}
replace_map:dict[UOp, UOp] = {}
for buf_uop, offset in offsets.items():
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(dtypes.index, offset)), buf_uop.max_numel())
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(dtypes.weakint, offset)), buf_uop.max_numel())
if DEBUG >= 1 and (omem:=sum(nbytes.values()) / 1e6) != (nmem:=sum(arena_sizes.values()) / 1e6):
print(f"memory reduced from {omem:.2f} MB -> {nmem:.2f} MB, {len(first_appearance)} -> {len(arenas)} bufs")
+10 -13
View File
@@ -1,5 +1,5 @@
from tinygrad.helpers import all_same, prod, getenv, ALLREDUCE_CAST
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, graph_rewrite
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, graph_rewrite, broadcast_axes, _broadcast_shape
from tinygrad.dtype import dtypes
from tinygrad.schedule.allreduce import handle_allreduce
@@ -47,20 +47,17 @@ def shard_srcs(msrcs:tuple[UOp, ...], axis:int) -> list[UOp]:
assert all_same(devices), f"all buffers must have the same device {devices}"
dcount = len(devices[0])
out_shape = _broadcast_shape(*[x.shape for x in msrcs])
srcs:list[UOp] = []
for mlb in msrcs:
if mlb.axis is None:
# no axis, shard it
assert mlb.op is not Ops.MULTI
srcs.append(mlb._shard(axis, dcount))
src_axis = axis - (len(out_shape)-len(mlb.shape))
if mlb.axis == src_axis:
# same axis, just copy through
srcs.append(mlb.src[0])
else:
assert mlb.op is Ops.MULTI
if mlb.axis == axis:
# same axis, just copy through
srcs.append(mlb.src[0])
else:
# axis mismatch, copy to all devices, and shard it correctly
srcs.append(copy_multi(mlb, mlb.device)._shard(axis, dcount))
# otherwise every device gets the full copy, sharded iff this src has the axis (broadcast srcs stay whole)
full = mlb if mlb.axis is None else copy_multi(mlb, mlb.device)
srcs.append(full if axis in broadcast_axes(mlb.shape, out_shape) else full._shard(src_axis, dcount))
return srcs
def alu_multi(root:UOp):
@@ -148,7 +145,7 @@ def rewrite_into_function(call:UOp):
def param_to_multi(p:UOp):
if p.axis is None: return None
return UOp.param(p.arg.slot, p.dtype, p.shard_shape, p.device, p.arg.vmin_vmax, p.arg.name, p.arg.addrspace).multi(p.axis)
return UOp.param(p.arg.slot, p.dtype, p.shard_shape, p.device, p.arg.vmin_vmax, p.arg.multiple_of, p.arg.name, p.arg.addrspace).multi(p.axis)
# NOTE: this is the same pattern as unrolled ranges
multi_pm = PatternMatcher([
+2 -2
View File
@@ -78,7 +78,7 @@ def split_reduceop(reduce:UOp, x:UOp):
# split is moved to the end to provide maximum locality for the second phase reduce.
# get expanded by rangeifying the UOp x
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else UOp.const(dtypes.index, 0) for i,s in enumerate(x.shape)])
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else UOp.const(dtypes.weakint, 0) for i,s in enumerate(x.shape)])
range_nums = [y.arg[0] for y in indexed.substitute({x.base:UOp(Ops.NOOP, x.base.dtype)}, extra_pm=pm_mops).ranges]
is_expanded = [i not in range_nums for i in range(len(x.shape))]
@@ -470,7 +470,7 @@ to_define_global = PatternMatcher([
(UPat(Ops.STORE, name="x"), find_bufs),
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
(UPat(Ops.PARAM, name="v"), lambda v:
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype)
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype, multiple_of=v.arg.multiple_of)
if v.arg.name is not None and v.arg.vmin_vmax is not None else None),
# this renumbers the params
+3 -29
View File
@@ -6,7 +6,7 @@ if TYPE_CHECKING: import numpy
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtype, to_dtype, strong_dtype, _from_np_dtype, _to_np_dtype, PyConst
from tinygrad.helpers import all_int, getenv, fully_flatten, fetch, Metadata, TRACEMETA, TracingKey
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable, _broadcast_shape
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable
from tinygrad.mixin.rand import RandMixin
from tinygrad.schedule import create_linear_with_vars
from tinygrad.device import Buffer, canonicalize_device
@@ -71,8 +71,8 @@ class Tensor(RandMixin):
# create a UOp from the different types of inputs
if isinstance(data, UOp):
# if data is dtype.index that means that this is a symbolic int and we need to lower it to something we can make a Tensor out of
if data.dtype == dtypes.index: data = _index_to_concrete_int(data)
# if data is dtype.weakint that means that this is a symbolic int and we need to lower it to something we can make a Tensor out of
if data.dtype == dtypes.weakint: data = _index_to_concrete_int(data)
elif data is None:
data = UOp.const(_dtype or dtypes.default_float, 0)
elif isinstance(data, get_args(ConstType)):
@@ -489,32 +489,6 @@ class Tensor(RandMixin):
def __delitem__(self, indices) -> None:
raise TypeError("Tensor does not support deleting items")
# ***** broadcasted elementwise ops *****
def where(self:Tensor, x:Tensor|ConstType|sint, y:Tensor|ConstType|sint) -> Tensor:
"""
Returns a tensor of elements selected from either `x` or `y`, depending on `self`.
`output_i = x_i if self_i else y_i`.
```python exec="true" source="above" session="tensor" result="python"
cond = Tensor([[True, True, False], [True, False, False]])
print(cond.where(1, 3).numpy())
```
```python exec="true" source="above" session="tensor" result="python"
Tensor.manual_seed(42)
cond = Tensor.randn(2, 3)
print(cond.numpy())
```
```python exec="true" source="above" session="tensor" result="python"
print((cond > 0).where(cond, -float("inf")).numpy())
```
"""
if isinstance(x, Tensor): x, y = x._broadcasted(y)
elif isinstance(y, Tensor): y, x = y._broadcasted(x)
else: x, y = self.ufix(x)._broadcasted(y)
out_shape = _broadcast_shape(self.shape, x.shape)
return self.cast(dtypes.bool)._broadcast_to(out_shape)._apply_uop(UOp.where, x._broadcast_to(out_shape), y._broadcast_to(out_shape))
# ***** op wrappers *****
# unlike Tensors, UOps are immutable, so these don't go in mixin
+4 -2
View File
@@ -11,6 +11,8 @@ def fold_divmod_general(d: UOp) -> UOp|None:
if y.vmin==y.vmax==0: raise ZeroDivisionError(f"{'Division' if d.op is Ops.FLOORDIV else 'Mod'} by zero trying to rewrite {x.alu(d.op, y)}")
# x//y is constant
if (xdiv:=x//y).vmin == xdiv.vmax: return x - xdiv.vmin*y if d.op is Ops.FLOORMOD else xdiv.const_like(xdiv.vmin)
# PARAM // c is irreducible
if x.op is Ops.PARAM and y.op is Ops.CONST and x.arg.multiple_of % y.arg == 0: return d.const_like(0) if d.op is Ops.FLOORMOD else None
# split uops for the rest of the processing
x_peeled, const = x.pop_const()
@@ -98,9 +100,9 @@ div_and_mod_symbolic = PatternMatcher([
# (x//c+a)//d -> (x+a*c)//(c*d) for c>0, d>0
((UPat.var("x")//UPat.cvar("c") + UPat.cvar("a"))//UPat.cvar("d"), lambda x,c,a,d: (x+a*c)//(c*d) if d.vmin>0 else None),
# (x+c)//d -> (x+c%d)//d + c//d for d>0 (split out the multiple of d in the constant)
((UPat.var("x", dtypes.index)+UPat.cvar("c"))//UPat.cvar("d"),
((UPat.var("x", dtypes.weakint)+UPat.cvar("c"))//UPat.cvar("d"),
lambda x,c,d: (x+c.arg%d.arg)//d + c.arg//d.arg if c.arg%d.arg!=c.arg and d.arg>0 else None),
# ** 2. Slow Rules **
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), dtypes.index, name="d"), lambda d: fold_divmod_general(d)),
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), dtypes.weakint, name="d"), lambda d: fold_divmod_general(d)),
])
+7 -1
View File
@@ -16,5 +16,11 @@ mop_cleanup = PatternMatcher([
lambda src,stk: src if stk.shape == src.shape and list(range(len(stk.src))) == [x.src[1].arg for x in stk.src] else None),
# const INDEX into STACK is src
(UPat(Ops.INDEX, src=(UPat(Ops.STACK, name="a"), UPat.cvar("i")), name="idx", allow_any_len=True),
lambda a,i,idx: a.src[i.arg] if len(idx.src) <= 2 else a.src[i.arg].index(*idx.src[2:])),
lambda a,i,idx: a.src[i.arg] if len(idx.src) <= 2 else a.src[i.arg].index(*idx.src[2:])),
# INDEX on INDEX is INDEX
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"),
lambda idx1,idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:]) if all(x.shape == () for x in idx1.src[1:]+idx2.src[1:]) else None),
# INDEX on shaped INDEX (TODO: this can be more generic)
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx1_arg"))),), allow_any_len=True, name="idx2"),
lambda buf,idx1_arg,idx2: buf.index(idx1_arg.index(*idx2.src[1:])) if len(idx1_arg.shape) == len(idx2.src[1:]) else None),
])
+55 -55
View File
@@ -24,12 +24,13 @@ class ParamArg:
slot: int
dtype: DType
vmin_vmax: tuple[PyConst, PyConst]|None = None
multiple_of: int|None = None
name: str|None = None
addrspace: AddrSpace|None = AddrSpace.GLOBAL
axis: int|None = None
device: str|tuple[str, ...]|None = None
def __repr__(self):
fields = (("vmin_vmax", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None))
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None))
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
return f"ParamArg({', '.join(args)})"
axis_letters = {AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L", AxisType.UPCAST: "u",
@@ -73,6 +74,10 @@ def _broadcast_shape(*shapes:tuple[sint, ...]) -> tuple[sint, ...]:
raise IndexError(f"shape mismatch: objects cannot be broadcast to a single shape {shapes}")
ret.append(rest[0] if rest else 1)
return tuple(ret)
def broadcast_axes(src_shape:tuple[sint, ...], out_shape:tuple[sint, ...]) -> tuple[int, ...]:
# out axes that are added or expanded
if (nleft:=len(out_shape)-len(src_shape)) < 0: raise RuntimeError(f"cannot broadcast {src_shape} into {out_shape}")
return tuple(range(nleft)) + tuple(nleft+i for i,s in enumerate(src_shape) if resolve(s == 1, default=False) and resolve(out_shape[nleft+i] != 1))
def ssimplify(uop:sint): return uop.ssimplify() if isinstance(uop, UOp) else uop
def sym_infer(uop: UOp|int, var_vals: dict[str, int]) -> int: return uop.sym_infer(var_vals) if isinstance(uop, UOp) else uop
@@ -88,8 +93,8 @@ def multirange_str(rngs:Iterable[UOp], color=False, pad=None) -> str:
def shape_to_shape_arg(arg:tuple[sint, ...]) -> UOp:
if len(arg) == 0: return UOp(Ops.STACK)
elif len(arg) == 1: return UOp.const(dtypes.index, arg[0])
else: return UOp(Ops.STACK, src=tuple(UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in arg))
elif len(arg) == 1: return UOp.const(dtypes.weakint, arg[0])
else: return UOp(Ops.STACK, src=tuple(UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in arg))
def consumer_map_from_toposort(lst:Iterable[UOp]):
ret: dict[UOp, dict[UOp, None]] = {}
@@ -100,7 +105,6 @@ def consumer_map_from_toposort(lst:Iterable[UOp]):
return ret
def promo_dtype(src:tuple[UOp,...]) -> DType:
# TODO: delete this once we merge index and weakint
dts = [x.dtype for x in src]
return dts[0] if all_same(dts) else least_upper_dtype(*dts)
@@ -109,7 +113,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
match op:
case Ops.STORE | Ops.CALL | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | \
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.WAIT | Ops.REWRITE_ERROR:
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.REWRITE_ERROR:
# always void
return dtypes.void
case Ops.CUSTOM | Ops.CUSTOMI | Ops.INS | Ops.PYLITERAL:
@@ -125,9 +129,9 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
case Ops.CMPLT | Ops.CMPNE | Ops.CMPEQ:
return dtypes.bool
case Ops.SIN | Ops.LOG2 | Ops.EXP2 | Ops.SQRT | Ops.RECIPROCAL:
return dtypes.weakfloat if src[0].dtype == dtypes.weakint else least_upper_float(src[0].dtype)
return least_upper_float(src[0].dtype)
case Ops.WHERE:
assert src[0].dtype == dtypes.bool, f"where first arg isn't bool, it's {src[0].dtype}"
if src[0].dtype != dtypes.bool: raise RuntimeError(f"where cond must be bool, got {src[0].dtype}")
return promo_dtype(src[1:])
case Ops.STACK:
if len(src) == 0: return dtypes.void
@@ -162,7 +166,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
# derived from the value. order matters: bool is an int subclass, ConstFloat is a float subclass
if isinstance(arg, InvalidType): return dtypes.bool # Invalid is the lattice bottom, typed by its consumer
if isinstance(arg, bool): return dtypes.bool
if isinstance(arg, int): return dtypes.weakint
if isinstance(arg, int): return None
if isinstance(arg, float): return dtypes.weakfloat
raise TypeError(f"no dtype for CONST with arg {arg}")
if op in GroupOp.Unary: return src[0].dtype
@@ -530,7 +534,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if len(srcs) == 1 and isinstance(srcs[0], UOp): return srcs[0]
return UOp(Ops.GROUP, src=tuple([x for x in srcs if x is not None]))
def index(self, *srcs:UOp|int|None, **kwargs):
new_srcs: list[UOp] = [UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in srcs if x is not None]
new_srcs: list[UOp] = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in srcs if x is not None]
if len(new_srcs) == 1 and new_srcs[0].op is Ops.CONST and self.op is Ops.STACK: return self.src[new_srcs[0].arg]
return UOp(Ops.INDEX, src=(self,)+tuple(new_srcs), **kwargs)
def __getitem__(self, idx):
@@ -542,11 +546,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
bounds = tuple((s.start or 0, s.stop if s.stop is not None else self.shape[i]) if isinstance(s, slice) else (0, self.shape[i])
for i, s in enumerate(idx))
src = self.shrink(bounds)
non_slice_args = [UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in idx if not isinstance(x, slice)]
non_slice_args = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx if not isinstance(x, slice)]
if not non_slice_args: return src # all dims are slices, no indexing needed
perm = src.permute(tuple([i for i in range(src.ndim) if i not in slice_idx] + slice_idx))
return perm.index(*non_slice_args)
return self.index(*[UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in idx])
return self.index(*[UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx])
@property
def _uop(self) -> UOp: return self
@classmethod
@@ -559,8 +563,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def ufix(self, x):
if isinstance(x, UOp): return x
# float self keeps its dtype for any scalar, int self only for int/Invalid scalars
if dtypes.is_float(self.dtype) or (dtypes.is_int(self.dtype) and isinstance(x, (int, InvalidType))): return self.const_like(x)
return self.const_like(x, dtypes.from_py(x))
dtype = self.dtype if dtypes.is_float(self.dtype) or (dtypes.is_int(self.dtype) and isinstance(x, (int, InvalidType))) else dtypes.from_py(x)
return UOp.const(dtype, x)
def broadcast(self, count:int):
if count == 1: return self
return UOp(Ops.STACK, src=(self,)*count)
@@ -571,19 +575,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def end(self, *src:UOp): return UOp(Ops.END, src=(self,)+src) if len(src) else self
def after(self, *src:UOp, **kwargs): return UOp(Ops.AFTER, src=(self,)+src, **kwargs) if len(src) else self
def barrier(self, *src:UOp): return UOp(Ops.BARRIER, src=(self,)+src)
def wait(self, **kwargs): return UOp(Ops.WAIT, src=(self,), **kwargs)
def ins(self, arg, **kwargs): return UOp(Ops.INS, kwargs.pop("dtype", self.dtype), kwargs.pop("src", self.src), arg, kwargs.pop("tag", self.tag))
def contract(self, *rngs:UOp):
assert all(x.arg[-1] == AxisType.UPCAST for x in rngs), "all contract ranges must be upcast"
return UOp.stack(*[self.substitute(dict(zip(rngs, [r.const_like(i) for r,i in zip(rngs, idx)])))
for idx in itertools.product(*[range(int(r.vmax)+1) for r in rngs])])
def alu(self, op, *src:UOp, **kwargs):
all_srcs = (self, *src)
# broadcast shaped operands to a common shape (None and () are falsy, so only real shapes participate)
if (shapes := [s for x in all_srcs if (s:=x._shape)]) and not all_same(shapes):
out_shape = _broadcast_shape(*shapes)
all_srcs = tuple(x._broadcast_to(out_shape) if x._shape else x for x in all_srcs)
return UOp(op, src=all_srcs, **kwargs)
def alu(self, op, *src:UOp, **kwargs): return UOp(op, src=(self, *src), **kwargs)
@staticmethod
def const(dtype:DType, b:ConstLike, shape:tuple[sint, ...]|None=None):
if isinstance(b, UOp): return b.cast(dtype)
@@ -595,10 +592,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
ret = UOp(Ops.CONST, dtype, arg=dtype.const(b), src=())
return ret._mop(Ops.EXPAND, arg=shape) if shape is not None and shape != () and ret.shape != shape else ret
@staticmethod
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.index, src=(), **kwargs):
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
@staticmethod
def special(end:sint, name:str, dtype=dtypes.index): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, dtypes.void, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.LOOP)+arg)
@staticmethod
def special(end:sint, name:str, dtype=dtypes.weakint): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
@staticmethod
def wmma(a:UOp, b:UOp, acc:UOp, dims:tuple[int, int, int], device:str, threads:int, tc_upcast_axes=None):
# dtype_in is stored in the arg (not derived from src[0].dtype) because bitcast rewrites change src dtypes
@@ -614,7 +613,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
ret = UOp(Ops.REDUCE, src=(self.permute(perm),), arg=(op, len(reduce_axis)))
return ret.reshape(tuple(s for i,s in enumerate(self.shape) if i not in axis)) if axis != reduce_axis else ret
@staticmethod
def invalid(): return UOp.const(dtypes.index, Invalid)
def invalid(): return UOp.const(dtypes.weakint, Invalid)
def valid(self, cond):
return cond.where(self, self.const_like(Invalid))
def get_idx(self) -> UOp:
@@ -663,10 +662,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
return in_tuple.src[self.arg].axis if in_tuple.op is Ops.TUPLE else None
if self.op is Ops.PARAM: return self.arg.axis
# NOTE: they all have to share an axis, we always choose [-1]
if self.op in GroupOp.ALU: return axes[-1] if (axes := dedup([x.axis for x in self.src if x.axis is not None])) else None
# STACK adds a leading axis
if self.op is Ops.STACK: return axes[-1]+1 if (axes := dedup([x.axis for x in self.src if x.axis is not None])) else None
# NOTE: they all have to share an axis, we always choose [-1]. src axes are right-aligned into the output shape
if self.op in GroupOp.ALU.union({Ops.STACK}):
return axes[-1] if (axes := dedup([x.axis+len(self.shape)-len(x.shape) for x in self.src if x.axis is not None])) else None
if len(self.src) == 0: return None
src_axis = self.src[0].axis
if self.op is Ops.SHRINK and src_axis is not None and self.marg[src_axis] != (0, self.src[0].shape[src_axis]):
@@ -811,10 +809,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op is Ops.BUFFER: return self.arg.addrspace
if self.op in {Ops.SPECIAL, Ops.RANGE}: return AddrSpace.ALU
if self.op is Ops.LOAD: return AddrSpace.ALU # LOAD brings things into the ALU
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT}:
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT, Ops.END}:
return self.src[0].addrspace
if self.op in GroupOp.Movement: return self.src[0].addrspace
if self.op in {Ops.STACK, Ops.WMMA} or self.op in GroupOp.Elementwise:
if self.op in {Ops.STACK, Ops.WMMA, Ops.GROUP} or self.op in GroupOp.Elementwise:
ad = [x.addrspace for x in self.src if x.addrspace is not None]
if not len(ad) or not all_same(ad): return None
return ad[0]
@@ -915,9 +913,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# *** uop Variable stuff ***
@staticmethod
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.index) -> UOp:
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.weakint, multiple_of:int=1) -> UOp:
return UOp(Ops.PARAM, src=(shape_to_shape_arg(()),),
arg=ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), addrspace=AddrSpace.ALU))
arg=ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU))
@property
def expr(self) -> str:
assert self.op is Ops.PARAM
@@ -926,6 +924,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
assert self.op is Ops.PARAM and self.addrspace is AddrSpace.ALU, f"op is {self.op}, need PARAM"
uval = self.const_like(val) if isinstance(val, int) else val
assert self.vmin <= uval.vmin and uval.vmax <= self.vmax, f"bind {val} not in range [{self.vmin}, {self.vmax}]"
assert uval.divides(self.arg.multiple_of) is not None, f"bind {val} not divisible by {self.arg.multiple_of}"
return UOp(Ops.BIND, src=(self, uval))
def unbind(self) -> tuple[Variable, int]:
assert self.op is Ops.BIND and self.src[0].op is Ops.PARAM and self.src[1].op is Ops.CONST, f"can't unbind {self}"
@@ -948,6 +947,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op is Ops.STACK: return math.gcd(*[x.const_factor() for x in self.src])
if self.op is Ops.ADD: return math.gcd(self.src[0].const_factor(), self.src[1].const_factor())
if self.op is Ops.MUL: return self.src[0].arg if self.src[0].op is Ops.CONST else self.src[1].arg if self.src[1].op is Ops.CONST else 1
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self.arg.multiple_of
return 1
def divides(self, v:int) -> UOp|None:
if v==1: return self
@@ -959,6 +959,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op is Ops.MUL:
if (d0:=self.src[0].divides(v)) is not None: return d0 * self.src[1]
if (d1:=self.src[1].divides(v)) is not None: return self.src[0] * d1
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self // v if self.arg.multiple_of%v == 0 else None
return None # generic None if we aren't sure
def pop_const(self, op=Ops.ADD) -> tuple[UOp, PyConst]: # NOTE: assume Invalid ALU is resolved
return (self.src[0], self.src[1].arg) if self.op is op and self.src[1].op is Ops.CONST else (self, identity_element(op, self.dtype))
@@ -1024,7 +1025,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op is Ops.WHERE and dtypes.is_int(self.dtype): return min(self.src[1].vmin, self.src[2].vmin), max(self.src[1].vmax, self.src[2].vmax)
# NOTE: returned UOp is assumed to be CONST
if self.op is Ops.PARAM and self.arg.vmin_vmax is not None: return self.arg.vmin_vmax
if self.op in (Ops.RANGE, Ops.SPECIAL): return 0, (self.src[0]-1).vmax
if self.op in (Ops.RANGE, Ops.SPECIAL) and self.dtype is not dtypes.void: return 0, (self.src[0]-1).vmax
if self.op is Ops.BIND: return self.src[0]._min_max # ignore the bound value
if self.op is Ops.STACK: return min(x.vmin for x in self.src), max(x.vmax for x in self.src)
if self.op is Ops.CONST and self.arg is not Invalid: return self.arg, self.arg
@@ -1033,7 +1034,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# a cast to unsigned keeps exact bounds when the source fits
# TODO: can do more based on new dtype window
if dtypes.is_unsigned(self.dtype) and 0 <= self.src[0].vmin and self.src[0].vmax <= self.dtype.max: return self.src[0]._min_max
if self.dtype in dtypes.floats+dtypes.sints+(dtypes.index,):
if self.dtype in dtypes.floats+dtypes.sints+(dtypes.weakint,):
return max(self.dtype.min, self.src[0].vmin), min(self.src[0].vmax, self.dtype.max)
return self.dtype.min, self.dtype.max
@@ -1087,17 +1088,16 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# TODO: this should replace placeholder
@staticmethod
def param(slot:int, dtype:DType, shape:tuple[sint, ...]|None=None, device=None, vmin_vmax:tuple[PyConst, PyConst]|None=None, name=None,
addrspace=AddrSpace.GLOBAL, axis:int|None=None):
def param(slot:int, dtype:DType, shape:tuple[sint, ...]|None=None, device=None, vmin_vmax:tuple[PyConst, PyConst]|None=None,
multiple_of:int|None=None, name=None, addrspace=AddrSpace.GLOBAL, axis:int|None=None):
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create param for weak dtype {dtype}")
if shape is not None and axis is not None and isinstance(device, tuple):
shape = tuple(s*len(device) if i == axis else s for i,s in enumerate(shape))
src: tuple[UOp, ...] = (UOp(Ops.NOOP) if shape is None else shape_to_shape_arg(shape),)
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, name, addrspace, axis, device))
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, multiple_of, name, addrspace, axis, device))
def param_like(self, slot:int):
addrspace = self.addrspace if self.addrspace is not None else AddrSpace.GLOBAL
if self.op is Ops.BIND:
return UOp.param(slot, self.dtype, self._shape, self.device, cast(tuple[int, int], self._min_max), self.src[0].expr, addrspace)
if self.op is Ops.BIND: return self.src[0].replace(arg=replace(self.src[0].arg, slot=slot, addrspace=addrspace))
return UOp.param(slot, self.dtype, self.shard_shape if self.axis is not None else self._shape, self.device, addrspace=addrspace, axis=self.axis)
@staticmethod
@@ -1672,7 +1672,7 @@ def graph_rewrite(sink:UOp, pm:PatternMatcher, ctx=None, bottom_up=False, name=N
rewrite_ctx = RewriteContext(pm if not bottom_up else None, pm if bottom_up else bpm, ctx, enter_calls)
return rewrite_ctx.walk_rewrite(sink) if walk else rewrite_ctx.unified_rewrite(sink)
def sint_to_uop(x:sint, dtype=dtypes.index) -> UOp: return UOp.const(dtype, x) if isinstance(x, int) else x.cast(dtype)
def sint_to_uop(x:sint, dtype=dtypes.weakint) -> UOp: return UOp.const(dtype, x) if isinstance(x, int) else x.cast(dtype)
def to_max_shape(shape:tuple[sint, ...]) -> tuple[int, ...]: return tuple(int(x.vmax) if isinstance(x, UOp) else x for x in shape)
def select_dtype(u:UOp):
@@ -1682,22 +1682,22 @@ def lower_alu_dtype(u:UOp, x:UOp, y:UOp, dt:DType) -> UOp:
return src[0].alu(u.op, *src[1:]).cast(u.dtype)
pm_lower_index_dtype = PatternMatcher([
# There are no Unary ops at this point in symbolic, those are introduced later
(UPat(Ops.CONST, dtype=dtypes.index, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype) if u.arg!=Invalid else None),
(UPat(Ops.CONST, dtype=dtypes.weakint, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype) if u.arg!=Invalid else None),
# Binary can widen the dtype, WHERE cannot
(UPat(GroupOp.Binary, name="u", src=(UPat.var("x").cast(dtypes.index), UPat.var("y").cast(dtypes.index))),
(UPat(GroupOp.Binary, name="u", src=(UPat.var("x").cast(dtypes.weakint), UPat.var("y").cast(dtypes.weakint))),
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(select_dtype(u), x.dtype, y.dtype))),
(UPat(Ops.WHERE, dtypes.index, src=(UPat(), UPat.var("x").cast(dtypes.index), UPat.var("y").cast(dtypes.index)), name="u"),
(UPat(Ops.WHERE, dtypes.weakint, src=(UPat(), UPat.var("x").cast(dtypes.weakint), UPat.var("y").cast(dtypes.weakint)), name="u"),
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(x.dtype, y.dtype))),
(UPat(Ops.RANGE, src=(UPat.var("end").cast(dtypes.index)), name="r"), lambda r,end: r.replace(dtype=end.dtype, src=(end,)).cast(dtypes.index)),
(UPat(Ops.STACK, src=UPat().cast(dtypes.index), name="v"),
lambda v: v.replace(dtype=(dt:=select_dtype(v)), src=tuple(s.src[0].cast(dt) for s in v.src)).cast(dtypes.index)),
(UPat(Ops.RANGE, src=(UPat.var("end").cast(dtypes.weakint)), name="r"), lambda r,end: r.replace(dtype=end.dtype, src=(end,)).cast(dtypes.weakint)),
(UPat(Ops.STACK, src=UPat().cast(dtypes.weakint), name="v"),
lambda v: v.replace(dtype=(dt:=select_dtype(v)), src=tuple(s.src[0].cast(dt) for s in v.src)).cast(dtypes.weakint)),
# special can only be int32
(UPat(Ops.SPECIAL, src=(UPat.var("var").cast(dtypes.index),), name="u"),
lambda u,var: u.replace(dtype=dtypes.int, src=(var,)).cast(dtypes.index)),
(UPat(Ops.PARAM, dtype=dtypes.index, name="u"),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.index) if u.addrspace == AddrSpace.ALU else None),
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.index), UPat.cvar("val").cast(dtypes.index))),
lambda var,val: var.bind(val).cast(dtypes.index)),
(UPat(Ops.SPECIAL, src=(UPat.var("var").cast(dtypes.weakint),), name="u"),
lambda u,var: u.replace(dtype=dtypes.int, src=(var,)).cast(dtypes.weakint)),
(UPat(Ops.PARAM, dtype=dtypes.weakint, name="u"),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.weakint), UPat.cvar("val").cast(dtypes.weakint))),
lambda var,val: var.bind(val).cast(dtypes.weakint)),
# remove hanging casts
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast()),), lambda buf,idx: buf.index(idx)),
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast(), UPat.var("slen", dtypes.ints).cast(),), name="shrink"),
@@ -1713,7 +1713,7 @@ pm_lower_index_dtype = PatternMatcher([
UPat.var("gate").where(UPat.var("idx_x", dtypes.ints).cast(), UPat(Ops.CONST, arg=Invalid)))),
lambda buf,idx_x,idx_y,gate: buf.index(idx_y.valid(gate), idx_x.valid(gate), dtype=dtypes.float)),
(UPat((Ops.SINK, Ops.NOOP, Ops.END), name="n"),
lambda n: n.replace(src=tuple(s.src[0] if s.op is Ops.CAST and s.dtype == dtypes.index else s for s in n.src))),
lambda n: n.replace(src=tuple(s.src[0] if s.op is Ops.CAST and s.dtype == dtypes.weakint else s for s in n.src))),
])
def _index_to_concrete_int(u:UOp) -> UOp: return graph_rewrite(u.sink(), pm_lower_index_dtype).src[0]
@@ -1734,8 +1734,8 @@ pm_unbind = PatternMatcher([(UPat(Ops.BIND, name="x"), do_unbind)])
# ctx is source UOp for which we are finding a contiguous view for. used in contiguous_view_offset
pm_contiguous_view_offset = PatternMatcher([
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(dtypes.index, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(dtypes.index, 0)),
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(dtypes.weakint, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(dtypes.weakint, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE)+UPat.cvar('c'))), lambda c: c),
(UPat(Ops.INDEX, src=(UPat(), UPat.cvar('c'))), lambda ctx, c: c if resolve(ctx.numel() == 1, False) else None),
])
+2 -1
View File
@@ -34,6 +34,7 @@ def strip_binary_parens(x:UOp, left:str, right:str, code_for_op) -> str:
renderer = PatternMatcher([
(UPat(Ops.PARAM, name="x"), lambda x: x.arg.name if x.arg.name is not None else f"p{x.arg.slot}"),
(UPat((Ops.SPECIAL), name="x"), lambda x: x.arg),
(UPat(Ops.RANGE, dtypes.void, name="x"), lambda x: f"loop{x.arg[0]}"),
(UPat(Ops.RANGE, name="x"), lambda x: f"r{range_str(x)}"),
(UPat(Ops.CONST, name="x"), lambda x: str(x.arg)),
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({str(x.dtype)[7:]})({ctx[x.src[0]]})"),
@@ -88,7 +89,7 @@ pm_pyrender_extra = PatternMatcher([
# NOTE: range has srcs sometimes after control flow
(UPat(Ops.RANGE, src=(UPat(Ops.CONST, name="c"),), allow_any_len=True, name="x"), lambda ctx,x,c:
"UOp.range("+', '.join([str(c.arg)] + [repr(y) for y in x.arg])+
(f', src={srcs(ctx, x.src[1:])}' if len(x.src) > 1 else '')+(', dtype='+str(x.dtype) if x.dtype is not dtypes.index else '')+")"),
(f', src={srcs(ctx, x.src[1:])}' if len(x.src) > 1 else '')+(', dtype='+str(x.dtype) if x.dtype is not dtypes.weakint else '')+")"),
# TODO: index shouldn't mismatch dtype
(UPat(Ops.INDEX, src=(UPat(), UPat()), allow_any_len=True, name="x"), lambda ctx,x:
f"{ctx[x.src[0]]}.index({ctx[x.src[1]]}, "+''.join([f"{ctx[xx]}, " for xx in x.src[2:]])+
+13 -13
View File
@@ -74,12 +74,15 @@ spec_shared = PatternMatcher([
# CAST
(UPat((Ops.BITCAST, Ops.CAST), src=(UPat(),), name="x"), lambda x: isinstance(x.arg, DType)),
# RANGE can be in the big graph now
# RANGE can be in the big graph now. a void RANGE is a bound-less loop header, the arg is an axis id like RANGE
(UPat(Ops.RANGE, src=(UPat.var("x"),), allow_any_len=True, name="rng"), lambda rng,x:
rng.dtype == x.dtype and isinstance(rng.arg, tuple) and len(rng.arg) >= 2 and \
all(isinstance(ra, int) for ra in rng.arg[0:-1]) and isinstance(rng.arg[-1], AxisType)),
(UPat(Ops.INDEX, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(dtypes.is_int(y.dtype) for y in x.src[1:]) or None),
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:])),
# END closes RANGEs
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:]) or None),
# a loop-ended END requires a trailing bool condition for the backedge (loop again while true)
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void), UPat(dtype=dtypes.bool))), lambda: True),
# PARAM
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
@@ -103,9 +106,6 @@ spec_shared = PatternMatcher([
# BARRIER (on any length). TODO: this should only be in spec_program
(UPat(Ops.BARRIER, dtypes.void), lambda: True),
# WAIT until a condition evaluates to true.
(UPat(Ops.WAIT, dtypes.void, src=(UPat(dtype=dtypes.bool),)), lambda: True),
# assembly instruction
(UPat(Ops.INS), lambda: True),
@@ -134,11 +134,11 @@ spec_tensor = PatternMatcher([
# BUFFER
(UPat(Ops.BUFFER, src=(UPat(),), name="buf"), lambda buf:
(isinstance(buf.dtype, DType) and buf.src[0].dtype == dtypes.index and is_device(buf.arg.device))
(isinstance(buf.dtype, DType) and buf.src[0].dtype == dtypes.weakint and is_device(buf.arg.device))
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
# Tensor variable bindings
(UPat(Ops.BIND, (dtypes.int, dtypes.index,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.index,))), arg=None), lambda: True),
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.weakint,))), arg=None), lambda: True),
# custom function
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
@@ -153,10 +153,10 @@ spec_tensor = PatternMatcher([
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), valid_gettuple),
# SPECIAL is index before index lowering. custom_kernel currently has this
(UPat(Ops.SPECIAL, src=(UPat.var("x", dtypes.index),), name="s"), lambda s,x: s.dtype == x.dtype and isinstance(s.arg, str)),
(UPat(Ops.SPECIAL, src=(UPat.var("x", dtypes.weakint),), name="s"), lambda s,x: s.dtype == x.dtype and isinstance(s.arg, str)),
# inputs to movement ops
(UPat({Ops.ADD, Ops.MUL, Ops.CDIV, Ops.FLOORDIV}, dtype=dtypes.index), lambda: True),
(UPat({Ops.ADD, Ops.MUL, Ops.CDIV, Ops.FLOORDIV}, dtype=dtypes.weakint), lambda: True),
# movement ops
(UPat((Ops.RESHAPE, Ops.EXPAND), src=(UPat(), UPat())), lambda: True),
@@ -166,7 +166,7 @@ spec_tensor = PatternMatcher([
# REDUCE has arg=(op, num_axes), src[1:] are ranges after lowering
(UPat(Ops.REDUCE, src=(UPat(),), allow_any_len=True, name="x"),
lambda x: isinstance(x.arg, tuple) and len(x.arg) == 2 and x.arg[0] in GroupOp.Reduce
and isinstance(x.arg[1], int) and all(y.dtype in (dtypes.index, dtypes.int) for y in x.src[1:])),
and isinstance(x.arg[1], int) and all(y.dtype in (dtypes.weakint, dtypes.int) for y in x.src[1:])),
# COPY. TODO: this should not have allow_any_len, but something is adding ranges
(UPat(Ops.COPY, name="copy", src=(UPat.var("x"),), allow_any_len=True), lambda copy,x: copy.dtype == x.dtype and is_device(copy.arg)),
@@ -198,7 +198,7 @@ spec_tensor = PatternMatcher([
# these ops can exist in programs but not the tensor spec. example: LOAD
spec_program = PatternMatcher([
# index and weak dtypes are not allowed in programs
(UPat(GroupOp.All, (dtypes.index, dtypes.weakint, dtypes.weakfloat)), lambda: False),
(UPat(GroupOp.All, (dtypes.weakint, dtypes.weakfloat)), lambda: False),
# allow special SHRINK
(UPat(Ops.SHRINK, src=(UPat((Ops.PARAM, Ops.BUFFER, Ops.AFTER)), UPat(), UPat(Ops.CONST))), lambda: True),
@@ -231,7 +231,7 @@ spec_full = PatternMatcher([
# SLICE on BUFFER is allowed if BUFFER is
(UPat(Ops.SLICE, src=(UPat(GroupOp.Movement.union({Ops.BUFFER, Ops.PARAM, Ops.STAGE, Ops.AFTER})),
UPat(Ops.CONST, dtype=dtypes.index)), allow_any_len=True, name="bv"),
UPat(Ops.CONST, dtype=dtypes.weakint)), allow_any_len=True, name="bv"),
lambda bv: isinstance(bv.arg, int)),
(UPat(Ops.CALL, dtypes.void, src=(UPat((Ops.SLICE,)),), allow_any_len=True), lambda: True),
@@ -246,7 +246,7 @@ spec_full = PatternMatcher([
(UPat((Ops.LOAD, Ops.STORE)), lambda: True),
# while BIND is being casted
(UPat(Ops.BIND, (dtypes.int, dtypes.index), (UPat(), UPat()), arg=None), lambda: True),
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint), (UPat(), UPat()), arg=None), lambda: True),
])+spec_tensor+spec_program+spec_hcq
# **** pyrender (move this) ****
+18 -18
View File
@@ -66,7 +66,7 @@ def fold_add_divmod_recombine(x:UOp) -> UOp|None:
# an invalid index is cond.where(idx, Invalid) in index. the consumer reads cond back off the WHERE with UOp.get_valid,
# so casts and comparisons of a gated index can drop the gate: when the index is invalid the result is never used
invalid_idx_gate = UPat().where(UPat.var("x"), UPat(Ops.CONST, dtypes.index, arg=Invalid))
invalid_idx_gate = UPat().where(UPat.var("x"), UPat(Ops.CONST, dtypes.weakint, arg=Invalid))
pm_index_invalid = PatternMatcher([
(invalid_idx_gate.cast(name="cast"), lambda x,cast: x.cast(cast.dtype)),
(UPat(GroupOp.Comparison, src=(invalid_idx_gate, UPat.var("y")), name="alu"), lambda x,y,alu: x.alu(alu.op,y)),
@@ -110,14 +110,14 @@ symbolic_simple = propagate_invalid + PatternMatcher([
# ** self folding **
(UPat.var("x") + 0, lambda x: x), # x+0 -> x
(UPat.var("x") * 1, lambda x: x), # x*1 -> x
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)) ^ 0, lambda x: x), # x^0 -> x
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) ^ 0, lambda x: x), # x^0 -> x
(UPat.var("x") // UPat.var("x"), lambda x: x.const_like(1)), # x//x -> 1
(UPat.var("x") // 1, lambda x: x), # x//1 -> x
(UPat.var("x") // -1, lambda x: -x), # x//-1 -> -x
((UPat.var("x") ^ UPat.var("y")) ^ UPat.var("y"), lambda x,y: x), # (x^y)^y -> x
((UPat.var() % UPat.var("y")).named("base") % UPat.var("y"), lambda base,y: base), # (x%y)%y = -> x%y (rewritten with base for speed)
# variations of (x%c)+(x//c)*c = x
(UPat(Ops.ADD, dtype=dtypes.index, name="x"), fold_add_divmod_recombine),
(UPat(Ops.ADD, dtype=dtypes.weakint, name="x"), fold_add_divmod_recombine),
(UPat.var("x", dtype=dtypes.bool) & UPat.cvar("c"), lambda x,c: x if c.arg else c),
(UPat.var("x", dtype=dtypes.bool) | UPat.cvar("c"), lambda x,c: c if c.arg else x),
(UPat(GroupOp.Idempotent, src=(UPat.var("x"), UPat.var("x"))), lambda x: x),
@@ -125,9 +125,9 @@ symbolic_simple = propagate_invalid + PatternMatcher([
(UPat.var("x", dtype=dtypes.bool).where(UPat.const(dtypes.bool, True), UPat.const(dtypes.bool, False)), lambda x: x),
(UPat.var("x", dtype=dtypes.bool).where(UPat.const(dtypes.bool, False), UPat.const(dtypes.bool, True)), lambda x: x.logical_not()),
# CAST(bool -> int) != const — CAST(True)=1, CAST(False)=0, so fold based on const value
(UPat.var("x", dtype=dtypes.bool).cast(dtypes.ints+(dtypes.weakint, dtypes.index)) != UPat.cvar("c"),
(UPat.var("x", dtype=dtypes.bool).cast(dtypes.ints+(dtypes.weakint,)) != UPat.cvar("c"),
lambda x,c: x if c.arg == 0 else x.logical_not() if c.arg == 1 else x.const_like(True)),
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)).trunc(), lambda x: x),
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)).trunc(), lambda x: x),
# ** zero folding **
(UPat.var("x") < UPat.var("x"), lambda x: x.const_like(False, dtypes.bool)), # x < x -> False
(UPat.var("x") % UPat.var("x"), lambda x: x.const_like(0)), # x%x -> 0
@@ -139,7 +139,7 @@ symbolic_simple = propagate_invalid + PatternMatcher([
lambda x,mask,k: x >> k.arg if mask.arg | ((1 << k.arg) - 1) == -1 else None),
((UPat.var("x") & UPat.cvar("mask")) // UPat.cvar("c"),
lambda x,mask,c: x // c.arg if c.arg > 0 and c.arg & (c.arg-1) == 0 and mask.arg | (c.arg-1) == -1 else None),
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)) != UPat.var("x"),
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) != UPat.var("x"),
lambda x: x.const_like(False, dtypes.bool)), # x != x -> False (only ints)
# ** constant folding **
(UPat(GroupOp.Unary, src=(UPat((Ops.CONST, Ops.STACK)),), name="a"), fold_const_alu),
@@ -211,7 +211,7 @@ def canonicalize_simplex(X:UOp) -> UOp|None:
commutative = PatternMatcher([
# ** COMMUTATIVE flipping (only for index) **
# NOTE: this can break merging vector math by only flipping some of them
(UPat(GroupOp.Commutative, dtype=dtypes.index, name='x'), lambda x:
(UPat(GroupOp.Commutative, dtype=dtypes.weakint, name='x'), lambda x:
x.replace(src=x.src[::-1]) if x.src[1].tuplize < x.src[0].tuplize and not x.src[0].tuplize < x.src[1].tuplize else None),
])
@@ -229,7 +229,7 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
((UPat.var("y") + UPat.var("x")) + UPat.var("x"), lambda y,x: y+x*2),
((UPat.var("x") / UPat.var("x2")) / UPat.var("x3"), lambda x,x2,x3: x/(x2*x3) if x2 is not x3 else None), # (x/x2)/x3 -> x/(x2*x3)
(-1 * (UPat.var("x") + UPat.cvar("c")), lambda x,c: (-x)+(-c)), # -(x+c) -> -x + -c
(UPat.cvar("y") * (UPat.var("x", dtype=dtypes.index) + UPat.cvar("c")), lambda x,y,c: (y*x)+(y*c)), # y*(x+c) -> y*x + y*c
(UPat.cvar("y") * (UPat.var("x", dtype=dtypes.weakint) + UPat.cvar("c")), lambda x,y,c: (y*x)+(y*c)), # y*(x+c) -> y*x + y*c
# ** where folding **
(UPat.var("cond", dtype=dtypes.bool).logical_not().where(UPat.var("t"), UPat.var("f")),
lambda cond, t, f: cond.where(f,t) if f.arg is not Invalid else None),
@@ -255,35 +255,35 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
((UPat.var("x") // UPat.cvar("c1")) // UPat.cvar("c2"), lambda x,c1,c2: x//(c1*c2) if c2.vmin>0 else None),
# ** lt **
# c0*x<c1 for positive int c0,c1
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.index))<UPat.cvar("c1"),
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
lambda x,c0,c1: x<math.ceil(c1.arg/c0.arg) if c0.arg > 0 and c1.arg > 0 else None),
# c0*x<c1 for negative int c0 and non-positive c1
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.index))<UPat.cvar("c1"),
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
lambda x,c0,c1: (-x)<(-(math.floor(-c1.arg/-c0.arg))) if c0.arg < 0 and c0.arg != -1 and c1.arg <= 0 else None),
# x//d<c -> x<c*d for d>0
((UPat.var("x", dtype=dtypes.index)//UPat.cvar("d"))<UPat.cvar("c"),
((UPat.var("x", dtype=dtypes.weakint)//UPat.cvar("d"))<UPat.cvar("c"),
lambda x,d,c: x<(c.arg*d.arg) if d.arg > 0 else None),
# ** move add/mul consts to end (NOTE: this is still happening before constant folding) **
((UPat.var("x") + UPat.cvar("c1")) + UPat.var("y"), lambda x,c1,y: (x+y)+c1),
((UPat.var("x") * UPat.cvar("c1")) * UPat.var("y"), lambda x,c1,y: (x*y)*c1),
# *** rules from symbolic ***
# generic lt folding
(UPat.var("x", dtypes.index)<UPat.cvar("c"), lambda x,c: lt_folding(x, c.arg) if 0 < c.arg else None),
(UPat.var("x", dtypes.index)*-1 < UPat.var("y")*-1, lambda x,y: y<x),
(UPat.var("x", dtypes.weakint)<UPat.cvar("c"), lambda x,c: lt_folding(x, c.arg) if 0 < c.arg else None),
(UPat.var("x", dtypes.weakint)*-1 < UPat.var("y")*-1, lambda x,y: y<x),
# canonicalize a simplex with positive coefficients > 0. NOTE: not x < 1 means x > 0
((UPat.var("x", dtypes.index)<1).ne(True), lambda x: (newx<1).ne(True) if (newx:=canonicalize_simplex(x)) is not None else None),
((UPat.var("x", dtypes.weakint)<1).ne(True), lambda x: (newx<1).ne(True) if (newx:=canonicalize_simplex(x)) is not None else None),
# a range mod its own upper bound is just the range
(UPat(Ops.RANGE, src=UPat.var("end"), name="r")%UPat.var("end"), lambda r,end: r),
(UPat(Ops.RANGE, src=UPat.var("end"), name="r")//UPat.var("end"), lambda r,end: r.const_like(0)),
# cast/long folding
# if the intermediate cast doesnt narrow we can do it in one cast
(UPat.var('x').cast(name="a").cast(name="b"), lambda x,a,b: x.cast(b.dtype) if can_lossless_cast(x.dtype, a.dtype) else None),
(UPat.var('x', dtypes.ints+(dtypes.weakint, dtypes.index)).cast(dtypes.ints+(dtypes.weakint, dtypes.index), name="a").cast(name="b"),
(UPat.var('x', dtypes.ints+(dtypes.weakint,)).cast(dtypes.ints+(dtypes.weakint,), name="a").cast(name="b"),
lambda x,a,b: x.cast(b.dtype) if a.dtype.min<=x.vmin and x.vmax<=a.dtype.max else None),
# try to do math in int instead of long
(UPat(GroupOp.Binary, src=(UPat.var("x", dtypes.long), UPat.var("y", dtypes.long)), name="u"), lambda u,x,y:
x.cast(dtypes.int).alu(u.op, y.cast(dtypes.int)).cast(u.dtype) if not any(v.overflows(dtypes.int) for v in (u,x,y)) else None),
((UPat.var("x", dtypes.index) + UPat.cvar("c")).cast(dtypes.sints, name="cast"), lambda x,c,cast:x.cast(cast.dtype)+c.cast(cast.dtype)),
((UPat.var("x", dtypes.weakint) + UPat.cvar("c")).cast(dtypes.sints, name="cast"), lambda x,c,cast:x.cast(cast.dtype)+c.cast(cast.dtype)),
# only RANGE/IF/STORE/KERNEL have side effects
(UPat(Ops.AFTER, name="x"), lambda x: x.replace(src=(x.src[0],)+
tuple(dedup(flatten([(y,) if y.op in {Ops.RANGE, Ops.STORE, Ops.CALL, Ops.FUNCTION, Ops.BARRIER, Ops.END, Ops.LINEAR, Ops.STAGE}
@@ -399,7 +399,7 @@ pm_move_where_on_load = PatternMatcher([
])
def gated_given_valid(cond:UOp, x:UOp, i:UOp) -> UOp|None:
if x.dtype is not dtypes.index: return None
if x.dtype is not dtypes.weakint: return None
# Skip if x contains DIV/MOD AND IMAGE mode is enabled -> image index e.g. openpilot
if IMAGE.value > 0 and x.op_in_backward_slice_with_self(Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD): return None
return cond.where(uop_given_valid(cond, x, try_simplex=False), i)
@@ -463,5 +463,5 @@ sym = symbolic+pm_simplify_valid+PatternMatcher([
# ** combine terms (opinionated) **
(-1 * (UPat.var("x") + UPat.var("y")), lambda x,y: (-x)+(-y)), # -(x+y) -> -x + -y
# (x+y)*c -> x*c+y*c. only for int, float has inf*0=nan issue
((UPat.var("x", dtypes.index) + UPat.var("y")) * UPat.cvar("c"), lambda x,y,c: x*c+y*c),
((UPat.var("x", dtypes.weakint) + UPat.var("y")) * UPat.cvar("c"), lambda x,y,c: x*c+y*c),
])+pm_clean_up_group_sink
+6 -6
View File
@@ -31,21 +31,21 @@ z3_renderer = PatternMatcher([
(UPat(Ops.PARAM, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0])),
(UPat(Ops.RANGE, name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
# loads are variables bounded by the min/max of the dtype. non-pointer INDEX is also a LOAD
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.index,), name="x"), lambda x,ctx:
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx:
create_bounded(f"load{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
(UPat((Ops.LOAD, Ops.INDEX), dtypes.bool), lambda ctx: (z3.Bool(f"load{len(ctx[1])}", ctx=ctx[0]), None)),
# constants
(UPat(Ops.CONST, arg=Invalid), lambda ctx: (z3.Int("Invalid", ctx=ctx[0]), None)),
(UPat(Ops.CONST, dtypes.ints+(dtypes.index,), name="x"), lambda x,ctx: (z3.IntVal(x.arg, ctx=ctx[0]), None)),
(UPat(Ops.CONST, dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx: (z3.IntVal(x.arg, ctx=ctx[0]), None)),
(UPat(Ops.CONST, dtypes.bool, name="x"), lambda x,ctx: (z3.BoolVal(x.arg, ctx=ctx[0]), None)),
# casts from floats create new variables
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,), src=(UPat(dtype=dtypes.floats),), name="x"), lambda x,ctx:
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat(dtype=dtypes.floats),), name="x"), lambda x,ctx:
create_bounded(f"cast{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
# A comparison between floats introduces a new bool variable
(UPat(GroupOp.Comparison, src=UPat(dtype=dtypes.floats)), lambda ctx: (z3.Bool(f"float_cmp{len(ctx[1])}", ctx=ctx[0]), None)),
# casts from bool/int to int/bool
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,),src=(UPat.var("x", dtypes.bool),)), lambda x,ctx: (z3.If(ctx[1][x], 1, 0), None)),
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,), src=(UPat.var("x", dtypes.ints+(dtypes.index,)),)), lambda x,ctx: (ctx[1][x], None)),
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,),src=(UPat.var("x", dtypes.bool),)), lambda x,ctx: (z3.If(ctx[1][x], 1, 0), None)),
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat.var("x", dtypes.ints+(dtypes.weakint,)),)), lambda x,ctx: (ctx[1][x], None)),
(UPat(Ops.CAST, dtypes.bool, name="x"), lambda x,ctx: (ctx[1][x.src[0]]!=0, None)),
(UPat(GroupOp.ALU, name="x"), lambda x,ctx: (z3_alu[x.op](*(ctx[1][s] for s in x.src)), None)),
])
@@ -53,7 +53,7 @@ z3_renderer = PatternMatcher([
def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
# gate on upstream AFTER/BUFFER, but keep INDEX as an unknown LOAD
lst = list(UOp.sink(*uops).toposort(gate=lambda x: x.op not in {Ops.AFTER, Ops.BUFFER} and \
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.index) or x.op is Ops.SINK)))[:-1]
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.weakint) or x.op is Ops.SINK)))[:-1]
z3map: dict[UOp, z3.ExprRef] = {}
for u in lst:
# NOTE: we skip STACK here, it can't actually be accessed
+1
View File
@@ -47,6 +47,7 @@ def decode_profile(data:bytes) -> dict:
return {"dur":total_dur, "peak":global_peak, "layout":layout, "markers":markers}
def to_str(k:str, v) -> str:
if isinstance(v, str): return f"{k} {v}"
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}"
+1 -1
View File
@@ -75,7 +75,7 @@ const layoutUOp = (g, { graph, change }, opts) => {
if (!opts.showIndexing) {
for (const n of g.nodes()) {
const node = g.node(n);
if (node.label.includes("dtypes.index")) g.removeNode(n);
if (node.label.includes("dtypes.weakint")) g.removeNode(n);
}
}
// optionally remove node srcs, track affected nodes
+2 -2
View File
@@ -237,8 +237,8 @@ def timeline_layout(data:VizData, dev_events:list[tuple[int, int, float, DevEven
if (ref:=data.ref_map.get(name)) is not None and ref < len(data.ctxs):
name = data.ctxs[ref]["name"]
if (ki:=data.ctxs[ref].get("ki")) is not None and ki.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)
for est_key,est_val in (("FLOPS", ki.estimates.ops), ("B/s mem", ki.estimates.mem), ("B/s lds", ki.estimates.lds)):
with soft_err(lambda _: fmt.update({est_key:"ERR"})): fmt[est_key] = int(sym_infer(est_val, ei.arg['var_vals'])/(dur*1e-6))
key = ei.key
elif isinstance(e.name, TracingKey):
name = e.name.display_name